searchBar.tsx 8.9 KB

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