searchBar.tsx 8.3 KB

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