searchBar.tsx 9.8 KB

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