searchBar.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import {useEffect, useMemo} from 'react';
  2. import memoize from 'lodash/memoize';
  3. import omit from 'lodash/omit';
  4. import {fetchSpanFieldValues, fetchTagValues} from 'sentry/actionCreators/tags';
  5. import type {SearchConfig} from 'sentry/components/searchSyntax/parser';
  6. import {defaultConfig} from 'sentry/components/searchSyntax/parser';
  7. import SmartSearchBar from 'sentry/components/smartSearchBar';
  8. import {NEGATION_OPERATOR, SEARCH_WILDCARD} from 'sentry/constants';
  9. import type {TagCollection} from 'sentry/types/group';
  10. import {SavedSearchType} from 'sentry/types/group';
  11. import type {Organization} from 'sentry/types/organization';
  12. import {defined} from 'sentry/utils';
  13. import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
  14. import type {Field} from 'sentry/utils/discover/fields';
  15. import {
  16. FIELD_TAGS,
  17. isAggregateField,
  18. isEquation,
  19. isMeasurement,
  20. SEMVER_TAGS,
  21. SPAN_OP_BREAKDOWN_FIELDS,
  22. TRACING_FIELDS,
  23. } from 'sentry/utils/discover/fields';
  24. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  25. import {
  26. DEVICE_CLASS_TAG_VALUES,
  27. FieldKey,
  28. FieldKind,
  29. isDeviceClass,
  30. } from 'sentry/utils/fields';
  31. import Measurements from 'sentry/utils/measurements/measurements';
  32. import useApi from 'sentry/utils/useApi';
  33. import withTags from 'sentry/utils/withTags';
  34. import {isCustomMeasurement} from 'sentry/views/dashboards/utils';
  35. const SEARCH_SPECIAL_CHARS_REGEXP = new RegExp(
  36. `^${NEGATION_OPERATOR}|\\${SEARCH_WILDCARD}`,
  37. 'g'
  38. );
  39. const STATIC_FIELD_TAGS_SET = new Set(Object.keys(FIELD_TAGS));
  40. const getFunctionTags = (fields: Readonly<Field[]> | undefined) => {
  41. if (!fields?.length) {
  42. return [];
  43. }
  44. return fields.reduce((acc, item) => {
  45. if (
  46. !STATIC_FIELD_TAGS_SET.has(item.field) &&
  47. !isEquation(item.field) &&
  48. !isCustomMeasurement(item.field)
  49. ) {
  50. acc[item.field] = {key: item.field, name: item.field, kind: FieldKind.FUNCTION};
  51. }
  52. return acc;
  53. }, {});
  54. };
  55. const getMeasurementTags = (
  56. measurements: Parameters<
  57. React.ComponentProps<typeof Measurements>['children']
  58. >[0]['measurements'],
  59. customMeasurements:
  60. | Parameters<React.ComponentProps<typeof Measurements>['children']>[0]['measurements']
  61. | undefined
  62. ) => {
  63. const measurementsWithKind = Object.keys(measurements).reduce((tags, key) => {
  64. tags[key] = {
  65. ...measurements[key],
  66. kind: FieldKind.MEASUREMENT,
  67. };
  68. return tags;
  69. }, {});
  70. if (!customMeasurements) {
  71. return measurementsWithKind;
  72. }
  73. return Object.keys(customMeasurements).reduce((tags, key) => {
  74. tags[key] = {
  75. ...customMeasurements[key],
  76. kind: FieldKind.MEASUREMENT,
  77. };
  78. return tags;
  79. }, measurementsWithKind);
  80. };
  81. const getSearchConfigFromCustomPerformanceMetrics = (
  82. customPerformanceMetrics?: CustomMeasurementCollection
  83. ): Partial<SearchConfig> => {
  84. if (!customPerformanceMetrics) {
  85. return {};
  86. }
  87. const searchConfigMap: Record<string, string[]> = {
  88. sizeKeys: [...defaultConfig.sizeKeys],
  89. durationKeys: [...defaultConfig.durationKeys],
  90. percentageKeys: [...defaultConfig.percentageKeys],
  91. numericKeys: [...defaultConfig.numericKeys],
  92. };
  93. Object.keys(customPerformanceMetrics).forEach(metricName => {
  94. const {fieldType} = customPerformanceMetrics[metricName];
  95. switch (fieldType) {
  96. case 'size':
  97. searchConfigMap.sizeKeys.push(metricName);
  98. break;
  99. case 'duration':
  100. searchConfigMap.durationKeys.push(metricName);
  101. break;
  102. case 'percentage':
  103. searchConfigMap.percentageKeys.push(metricName);
  104. break;
  105. default:
  106. searchConfigMap.numericKeys.push(metricName);
  107. }
  108. });
  109. const searchConfig = {
  110. sizeKeys: new Set(searchConfigMap.sizeKeys),
  111. durationKeys: new Set(searchConfigMap.durationKeys),
  112. percentageKeys: new Set(searchConfigMap.percentageKeys),
  113. numericKeys: new Set(searchConfigMap.numericKeys),
  114. };
  115. return searchConfig;
  116. };
  117. const STATIC_FIELD_TAGS = Object.keys(FIELD_TAGS).reduce((tags, key) => {
  118. tags[key] = {
  119. ...FIELD_TAGS[key],
  120. kind: FieldKind.FIELD,
  121. };
  122. return tags;
  123. }, {});
  124. const STATIC_FIELD_TAGS_WITHOUT_TRACING = omit(STATIC_FIELD_TAGS, TRACING_FIELDS);
  125. const STATIC_SPAN_TAGS = SPAN_OP_BREAKDOWN_FIELDS.reduce((tags, key) => {
  126. tags[key] = {name: key, kind: FieldKind.METRICS};
  127. return tags;
  128. }, {});
  129. const STATIC_SEMVER_TAGS = Object.keys(SEMVER_TAGS).reduce((tags, key) => {
  130. tags[key] = {
  131. ...SEMVER_TAGS[key],
  132. kind: FieldKind.FIELD,
  133. };
  134. return tags;
  135. }, {});
  136. export const getHasTag = (tags: TagCollection) => ({
  137. key: FieldKey.HAS,
  138. name: 'Has property',
  139. values: Object.keys(tags).sort((a, b) => {
  140. return a.toLowerCase().localeCompare(b.toLowerCase());
  141. }),
  142. predefined: true,
  143. kind: FieldKind.FIELD,
  144. });
  145. export type SearchBarProps = Omit<React.ComponentProps<typeof SmartSearchBar>, 'tags'> & {
  146. organization: Organization;
  147. tags: TagCollection;
  148. customMeasurements?: CustomMeasurementCollection;
  149. dataset?: DiscoverDatasets;
  150. fields?: Readonly<Field[]>;
  151. includeSessionTagsValues?: boolean;
  152. /**
  153. * Used to define the max height of the menu in px.
  154. */
  155. maxMenuHeight?: number;
  156. maxSearchItems?: React.ComponentProps<typeof SmartSearchBar>['maxSearchItems'];
  157. metricAlert?: boolean;
  158. omitTags?: string[];
  159. projectIds?: number[] | Readonly<number[]>;
  160. savedSearchType?: SavedSearchType;
  161. supportedTags?: TagCollection | undefined;
  162. };
  163. function SearchBar(props: SearchBarProps) {
  164. const {
  165. maxSearchItems,
  166. organization,
  167. tags,
  168. metricAlert = false,
  169. omitTags,
  170. supportedTags,
  171. fields,
  172. projectIds,
  173. includeSessionTagsValues,
  174. maxMenuHeight,
  175. customMeasurements,
  176. dataset,
  177. savedSearchType = SavedSearchType.EVENT,
  178. } = props;
  179. const api = useApi();
  180. const functionTags = useMemo(() => getFunctionTags(fields), [fields]);
  181. const tagsWithKind = useMemo(() => {
  182. return Object.keys(tags).reduce((acc, key) => {
  183. acc[key] = {
  184. ...tags[key],
  185. kind: FieldKind.TAG,
  186. };
  187. return acc;
  188. }, {});
  189. }, [tags]);
  190. useEffect(() => {
  191. // Clear memoized data on mount to make tests more consistent.
  192. getEventFieldValues.cache.clear?.();
  193. // eslint-disable-next-line react-hooks/exhaustive-deps
  194. }, [projectIds]);
  195. // Returns array of tag values that substring match `query`; invokes `callback`
  196. // with data when ready
  197. const getEventFieldValues = memoize(
  198. (tag, query, endpointParams): Promise<string[]> => {
  199. const projectIdStrings = (projectIds as Readonly<number>[])?.map(String);
  200. if (isAggregateField(tag.key) || isMeasurement(tag.key)) {
  201. // We can't really auto suggest values for aggregate fields
  202. // or measurements, so we simply don't
  203. return Promise.resolve([]);
  204. }
  205. // device.class is stored as "numbers" in snuba, but we want to suggest high, medium,
  206. // and low search filter values because discover maps device.class to these values.
  207. if (isDeviceClass(tag.key)) {
  208. return Promise.resolve(DEVICE_CLASS_TAG_VALUES);
  209. }
  210. const fetchPromise =
  211. dataset === DiscoverDatasets.SPANS_INDEXED
  212. ? fetchSpanFieldValues({
  213. api,
  214. orgSlug: organization.slug,
  215. fieldKey: tag.key,
  216. search: query,
  217. projectIds: projectIdStrings,
  218. endpointParams,
  219. })
  220. : fetchTagValues({
  221. api,
  222. orgSlug: organization.slug,
  223. tagKey: tag.key,
  224. search: query,
  225. projectIds: projectIdStrings,
  226. endpointParams,
  227. // allows searching for tags on transactions as well
  228. includeTransactions: true,
  229. // allows searching for tags on sessions as well
  230. includeSessions: includeSessionTagsValues,
  231. });
  232. return fetchPromise.then(
  233. results => results.filter(({name}) => defined(name)).map(({name}) => name),
  234. () => {
  235. throw new Error('Unable to fetch event field values');
  236. }
  237. );
  238. },
  239. ({key}, query) => `${key}-${query}`
  240. );
  241. const getTagList = (
  242. measurements: Parameters<
  243. React.ComponentProps<typeof Measurements>['children']
  244. >[0]['measurements']
  245. ) => {
  246. const measurementsWithKind = getMeasurementTags(measurements, customMeasurements);
  247. const orgHasPerformanceView = organization.features.includes('performance-view');
  248. // If it is not a metric alert search bar and supportedTags has a value, return supportedTags
  249. // If it is a metric alert search bar, combine supportedTags with getTagList tags
  250. if (metricAlert === false && supportedTags !== undefined) {
  251. return supportedTags;
  252. }
  253. const combinedTags: TagCollection = orgHasPerformanceView
  254. ? Object.assign(
  255. {},
  256. measurementsWithKind,
  257. functionTags,
  258. STATIC_SPAN_TAGS,
  259. STATIC_FIELD_TAGS
  260. )
  261. : Object.assign({}, STATIC_FIELD_TAGS_WITHOUT_TRACING);
  262. Object.assign(
  263. combinedTags,
  264. tagsWithKind,
  265. STATIC_FIELD_TAGS,
  266. STATIC_SEMVER_TAGS,
  267. supportedTags
  268. );
  269. combinedTags.has = getHasTag(combinedTags);
  270. const list =
  271. omitTags && omitTags.length > 0 ? omit(combinedTags, omitTags) : combinedTags;
  272. return list;
  273. };
  274. const customPerformanceMetricsSearchConfig = useMemo(
  275. () => getSearchConfigFromCustomPerformanceMetrics(customMeasurements),
  276. [customMeasurements]
  277. );
  278. return (
  279. <Measurements>
  280. {({measurements}) => (
  281. <SmartSearchBar
  282. hasRecentSearches
  283. savedSearchType={savedSearchType}
  284. projectIds={projectIds}
  285. onGetTagValues={getEventFieldValues}
  286. prepareQuery={query => {
  287. // Prepare query string (e.g. strip special characters like negation operator)
  288. return query.replace(SEARCH_SPECIAL_CHARS_REGEXP, '');
  289. }}
  290. maxSearchItems={maxSearchItems}
  291. excludedTags={[FieldKey.ENVIRONMENT, FieldKey.TOTAL_COUNT]}
  292. maxMenuHeight={maxMenuHeight ?? 300}
  293. {...customPerformanceMetricsSearchConfig}
  294. {...props}
  295. supportedTags={getTagList(measurements)}
  296. />
  297. )}
  298. </Measurements>
  299. );
  300. }
  301. export default withTags(SearchBar);