resultsSearchQueryBuilder.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import {useCallback, useMemo} from 'react';
  2. import omit from 'lodash/omit';
  3. import {fetchSpanFieldValues, fetchTagValues} from 'sentry/actionCreators/tags';
  4. import {
  5. STATIC_FIELD_TAGS,
  6. STATIC_FIELD_TAGS_SET,
  7. STATIC_FIELD_TAGS_WITHOUT_ERROR_FIELDS,
  8. STATIC_FIELD_TAGS_WITHOUT_TRACING,
  9. STATIC_FIELD_TAGS_WITHOUT_TRANSACTION_FIELDS,
  10. STATIC_SEMVER_TAGS,
  11. STATIC_SPAN_TAGS,
  12. } from 'sentry/components/events/searchBarFieldConstants';
  13. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  14. import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';
  15. import type {
  16. CallbackSearchState,
  17. FilterKeySection,
  18. } from 'sentry/components/searchQueryBuilder/types';
  19. import {t} from 'sentry/locale';
  20. import {SavedSearchType, type TagCollection} from 'sentry/types/group';
  21. import {defined} from 'sentry/utils';
  22. import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
  23. import type {Field} from 'sentry/utils/discover/fields';
  24. import {
  25. ALL_INSIGHTS_FILTER_KEY_SECTIONS,
  26. COMBINED_DATASET_FILTER_KEY_SECTIONS,
  27. ERRORS_DATASET_FILTER_KEY_SECTIONS,
  28. isAggregateField,
  29. isEquation,
  30. isMeasurement,
  31. parseFunction,
  32. } from 'sentry/utils/discover/fields';
  33. import {
  34. DiscoverDatasets,
  35. DiscoverDatasetsToDatasetMap,
  36. } from 'sentry/utils/discover/types';
  37. import {
  38. DEVICE_CLASS_TAG_VALUES,
  39. FieldKey,
  40. FieldKind,
  41. isDeviceClass,
  42. } from 'sentry/utils/fields';
  43. import type Measurements from 'sentry/utils/measurements/measurements';
  44. import {getMeasurements} from 'sentry/utils/measurements/measurements';
  45. import useApi from 'sentry/utils/useApi';
  46. import useOrganization from 'sentry/utils/useOrganization';
  47. import usePageFilters from 'sentry/utils/usePageFilters';
  48. import useTags from 'sentry/utils/useTags';
  49. import {isCustomMeasurement} from 'sentry/views/dashboards/utils';
  50. const getFunctionTags = (fields: readonly Field[] | undefined) => {
  51. if (!fields?.length) {
  52. return [];
  53. }
  54. return fields.reduce((acc, item) => {
  55. if (
  56. !STATIC_FIELD_TAGS_SET.has(item.field) &&
  57. !isEquation(item.field) &&
  58. !isCustomMeasurement(item.field)
  59. ) {
  60. const parsedFunction = parseFunction(item.field);
  61. if (parsedFunction) {
  62. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  63. acc[parsedFunction.name] = {
  64. key: parsedFunction.name,
  65. name: parsedFunction.name,
  66. kind: FieldKind.FUNCTION,
  67. };
  68. }
  69. }
  70. return acc;
  71. }, {});
  72. };
  73. const getMeasurementTags = (
  74. measurements: Parameters<
  75. React.ComponentProps<typeof Measurements>['children']
  76. >[0]['measurements'],
  77. customMeasurements:
  78. | Parameters<React.ComponentProps<typeof Measurements>['children']>[0]['measurements']
  79. | undefined
  80. ) => {
  81. const measurementsWithKind = Object.keys(measurements).reduce((tags, key) => {
  82. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  83. tags[key] = {
  84. ...measurements[key],
  85. kind: FieldKind.MEASUREMENT,
  86. };
  87. return tags;
  88. }, {});
  89. if (!customMeasurements) {
  90. return measurementsWithKind;
  91. }
  92. return Object.keys(customMeasurements).reduce((tags, key) => {
  93. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  94. tags[key] = {
  95. ...customMeasurements[key],
  96. kind: FieldKind.MEASUREMENT,
  97. };
  98. return tags;
  99. }, measurementsWithKind);
  100. };
  101. export const getHasTag = (tags: TagCollection) => ({
  102. key: FieldKey.HAS,
  103. name: 'Has property',
  104. values: Object.keys(tags).sort((a, b) => {
  105. return a.toLowerCase().localeCompare(b.toLowerCase());
  106. }),
  107. predefined: true,
  108. kind: FieldKind.FIELD,
  109. });
  110. type Props = {
  111. customMeasurements?: CustomMeasurementCollection;
  112. dataset?: DiscoverDatasets;
  113. fields?: readonly Field[];
  114. includeSessionTagsValues?: boolean;
  115. includeTransactions?: boolean;
  116. omitTags?: string[];
  117. onChange?: (query: string, state: CallbackSearchState) => void;
  118. onSearch?: (query: string) => void;
  119. placeholder?: string;
  120. projectIds?: number[] | readonly number[];
  121. query?: string;
  122. recentSearches?: SavedSearchType;
  123. searchSource?: string;
  124. supportedTags?: TagCollection | undefined;
  125. };
  126. const EXCLUDED_FILTER_KEYS = [FieldKey.ENVIRONMENT, FieldKey.TOTAL_COUNT];
  127. function ResultsSearchQueryBuilder(props: Props) {
  128. const {
  129. omitTags,
  130. fields,
  131. projectIds,
  132. includeSessionTagsValues,
  133. customMeasurements,
  134. dataset,
  135. includeTransactions = true,
  136. placeholder,
  137. } = props;
  138. const api = useApi();
  139. const organization = useOrganization();
  140. const {selection} = usePageFilters();
  141. const tags = useTags();
  142. const filteredTags = useMemo(() => {
  143. return omitTags && omitTags.length > 0
  144. ? omit(tags, omitTags, EXCLUDED_FILTER_KEYS)
  145. : omit(tags, EXCLUDED_FILTER_KEYS);
  146. }, [tags, omitTags]);
  147. const placeholderText = useMemo(() => {
  148. return placeholder ?? t('Search for events, users, tags, and more');
  149. }, [placeholder]);
  150. const measurements = useMemo(() => getMeasurements(), []);
  151. const functionTags = useMemo(() => getFunctionTags(fields), [fields]);
  152. // Returns array of tag values that substring match `query`; invokes `callback`
  153. // with data when ready
  154. const getEventFieldValues = useCallback(
  155. async (tag: any, query: any): Promise<string[]> => {
  156. const projectIdStrings = (projectIds as Array<Readonly<number>>)?.map(String);
  157. if (isAggregateField(tag.key) || isMeasurement(tag.key)) {
  158. // We can't really auto suggest values for aggregate fields
  159. // or measurements, so we simply don't
  160. return Promise.resolve([]);
  161. }
  162. // device.class is stored as "numbers" in snuba, but we want to suggest high, medium,
  163. // and low search filter values because discover maps device.class to these values.
  164. if (isDeviceClass(tag.key)) {
  165. return Promise.resolve(DEVICE_CLASS_TAG_VALUES);
  166. }
  167. const fetchPromise =
  168. dataset === DiscoverDatasets.SPANS_INDEXED
  169. ? fetchSpanFieldValues({
  170. api,
  171. endpointParams: normalizeDateTimeParams(selection.datetime),
  172. orgSlug: organization.slug,
  173. fieldKey: tag.key,
  174. search: query,
  175. projectIds: projectIdStrings,
  176. })
  177. : fetchTagValues({
  178. api,
  179. endpointParams: normalizeDateTimeParams(selection.datetime),
  180. orgSlug: organization.slug,
  181. tagKey: tag.key,
  182. search: query,
  183. projectIds: projectIdStrings,
  184. // allows searching for tags on transactions as well
  185. includeTransactions,
  186. // allows searching for tags on sessions as well
  187. includeSessions: includeSessionTagsValues,
  188. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  189. dataset: dataset ? DiscoverDatasetsToDatasetMap[dataset] : undefined,
  190. });
  191. try {
  192. const results = await fetchPromise;
  193. return results.filter(({name}) => defined(name)).map(({name}) => name);
  194. } catch (error) {
  195. throw new Error('Unable to fetch event field values');
  196. }
  197. },
  198. [
  199. api,
  200. organization,
  201. selection.datetime,
  202. projectIds,
  203. includeTransactions,
  204. includeSessionTagsValues,
  205. dataset,
  206. ]
  207. );
  208. const getTagList: TagCollection = useMemo(() => {
  209. const measurementsWithKind = getMeasurementTags(measurements, customMeasurements);
  210. const orgHasPerformanceView = organization.features.includes('performance-view');
  211. const combinedTags: TagCollection =
  212. dataset === DiscoverDatasets.ERRORS
  213. ? Object.assign({}, functionTags, STATIC_FIELD_TAGS_WITHOUT_TRANSACTION_FIELDS)
  214. : dataset === DiscoverDatasets.TRANSACTIONS ||
  215. dataset === DiscoverDatasets.METRICS_ENHANCED
  216. ? Object.assign(
  217. {},
  218. measurementsWithKind,
  219. functionTags,
  220. STATIC_SPAN_TAGS,
  221. STATIC_FIELD_TAGS_WITHOUT_ERROR_FIELDS
  222. )
  223. : orgHasPerformanceView
  224. ? Object.assign(
  225. {},
  226. measurementsWithKind,
  227. functionTags,
  228. STATIC_SPAN_TAGS,
  229. STATIC_FIELD_TAGS
  230. )
  231. : Object.assign({}, STATIC_FIELD_TAGS_WITHOUT_TRACING);
  232. Object.assign(combinedTags, filteredTags, STATIC_SEMVER_TAGS);
  233. combinedTags.has = getHasTag(combinedTags);
  234. return combinedTags;
  235. }, [
  236. measurements,
  237. dataset,
  238. customMeasurements,
  239. functionTags,
  240. filteredTags,
  241. organization.features,
  242. ]);
  243. const filterKeySections = useMemo(() => {
  244. const customTagsSection: FilterKeySection = {
  245. value: 'custom_fields',
  246. label: 'Custom Tags',
  247. children: Object.keys(filteredTags),
  248. };
  249. if (
  250. dataset === DiscoverDatasets.TRANSACTIONS ||
  251. dataset === DiscoverDatasets.METRICS_ENHANCED
  252. ) {
  253. return [...ALL_INSIGHTS_FILTER_KEY_SECTIONS, customTagsSection];
  254. }
  255. if (dataset === DiscoverDatasets.ERRORS) {
  256. return [...ERRORS_DATASET_FILTER_KEY_SECTIONS, customTagsSection];
  257. }
  258. return [...COMBINED_DATASET_FILTER_KEY_SECTIONS, customTagsSection];
  259. }, [filteredTags, dataset]);
  260. return (
  261. <SearchQueryBuilder
  262. placeholder={placeholderText}
  263. filterKeys={getTagList}
  264. initialQuery={props.query ?? ''}
  265. onSearch={props.onSearch}
  266. onChange={props.onChange}
  267. searchSource={props.searchSource || 'eventsv2'}
  268. filterKeySections={filterKeySections}
  269. getTagValues={getEventFieldValues}
  270. recentSearches={props.recentSearches ?? SavedSearchType.EVENT}
  271. showUnsubmittedIndicator
  272. />
  273. );
  274. }
  275. export default ResultsSearchQueryBuilder;