searchBar.tsx 7.4 KB

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