resultsSearchQueryBuilder.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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. acc[parsedFunction.name] = {
  63. key: parsedFunction.name,
  64. name: parsedFunction.name,
  65. kind: FieldKind.FUNCTION,
  66. };
  67. }
  68. }
  69. return acc;
  70. }, {});
  71. };
  72. const getMeasurementTags = (
  73. measurements: Parameters<
  74. React.ComponentProps<typeof Measurements>['children']
  75. >[0]['measurements'],
  76. customMeasurements:
  77. | Parameters<React.ComponentProps<typeof Measurements>['children']>[0]['measurements']
  78. | undefined
  79. ) => {
  80. const measurementsWithKind = Object.keys(measurements).reduce((tags, key) => {
  81. tags[key] = {
  82. ...measurements[key],
  83. kind: FieldKind.MEASUREMENT,
  84. };
  85. return tags;
  86. }, {});
  87. if (!customMeasurements) {
  88. return measurementsWithKind;
  89. }
  90. return Object.keys(customMeasurements).reduce((tags, key) => {
  91. tags[key] = {
  92. ...customMeasurements[key],
  93. kind: FieldKind.MEASUREMENT,
  94. };
  95. return tags;
  96. }, measurementsWithKind);
  97. };
  98. export const getHasTag = (tags: TagCollection) => ({
  99. key: FieldKey.HAS,
  100. name: 'Has property',
  101. values: Object.keys(tags).sort((a, b) => {
  102. return a.toLowerCase().localeCompare(b.toLowerCase());
  103. }),
  104. predefined: true,
  105. kind: FieldKind.FIELD,
  106. });
  107. type Props = {
  108. customMeasurements?: CustomMeasurementCollection;
  109. dataset?: DiscoverDatasets;
  110. fields?: Readonly<Field[]>;
  111. includeSessionTagsValues?: boolean;
  112. includeTransactions?: boolean;
  113. omitTags?: string[];
  114. onChange?: (query: string, state: CallbackSearchState) => void;
  115. onSearch?: (query: string) => void;
  116. placeholder?: string;
  117. projectIds?: number[] | Readonly<number[]>;
  118. query?: string;
  119. searchSource?: string;
  120. supportedTags?: TagCollection | undefined;
  121. };
  122. const EXCLUDED_FILTER_KEYS = [FieldKey.ENVIRONMENT, FieldKey.TOTAL_COUNT];
  123. function ResultsSearchQueryBuilder(props: Props) {
  124. const {
  125. omitTags,
  126. fields,
  127. projectIds,
  128. includeSessionTagsValues,
  129. customMeasurements,
  130. dataset,
  131. includeTransactions = true,
  132. placeholder,
  133. } = props;
  134. const api = useApi();
  135. const organization = useOrganization();
  136. const {selection} = usePageFilters();
  137. const tags = useTags();
  138. const filteredTags = useMemo(() => {
  139. return omitTags && omitTags.length > 0
  140. ? omit(tags, omitTags, EXCLUDED_FILTER_KEYS)
  141. : omit(tags, EXCLUDED_FILTER_KEYS);
  142. }, [tags, omitTags]);
  143. const placeholderText = useMemo(() => {
  144. return placeholder ?? t('Search for events, users, tags, and more');
  145. }, [placeholder]);
  146. const measurements = useMemo(() => getMeasurements(), []);
  147. const functionTags = useMemo(() => getFunctionTags(fields), [fields]);
  148. // Returns array of tag values that substring match `query`; invokes `callback`
  149. // with data when ready
  150. const getEventFieldValues = useCallback(
  151. async (tag, query): Promise<string[]> => {
  152. const projectIdStrings = (projectIds as Readonly<number>[])?.map(String);
  153. if (isAggregateField(tag.key) || isMeasurement(tag.key)) {
  154. // We can't really auto suggest values for aggregate fields
  155. // or measurements, so we simply don't
  156. return Promise.resolve([]);
  157. }
  158. // device.class is stored as "numbers" in snuba, but we want to suggest high, medium,
  159. // and low search filter values because discover maps device.class to these values.
  160. if (isDeviceClass(tag.key)) {
  161. return Promise.resolve(DEVICE_CLASS_TAG_VALUES);
  162. }
  163. const fetchPromise =
  164. dataset === DiscoverDatasets.SPANS_INDEXED
  165. ? fetchSpanFieldValues({
  166. api,
  167. endpointParams: normalizeDateTimeParams(selection.datetime),
  168. orgSlug: organization.slug,
  169. fieldKey: tag.key,
  170. search: query,
  171. projectIds: projectIdStrings,
  172. })
  173. : fetchTagValues({
  174. api,
  175. endpointParams: normalizeDateTimeParams(selection.datetime),
  176. orgSlug: organization.slug,
  177. tagKey: tag.key,
  178. search: query,
  179. projectIds: projectIdStrings,
  180. // allows searching for tags on transactions as well
  181. includeTransactions: includeTransactions,
  182. // allows searching for tags on sessions as well
  183. includeSessions: includeSessionTagsValues,
  184. dataset: dataset ? DiscoverDatasetsToDatasetMap[dataset] : undefined,
  185. });
  186. try {
  187. const results = await fetchPromise;
  188. return results.filter(({name}) => defined(name)).map(({name}) => name);
  189. } catch (error) {
  190. throw new Error('Unable to fetch event field values');
  191. }
  192. },
  193. [
  194. api,
  195. organization,
  196. selection.datetime,
  197. projectIds,
  198. includeTransactions,
  199. includeSessionTagsValues,
  200. dataset,
  201. ]
  202. );
  203. const getTagList: TagCollection = useMemo(() => {
  204. const measurementsWithKind = getMeasurementTags(measurements, customMeasurements);
  205. const orgHasPerformanceView = organization.features.includes('performance-view');
  206. const combinedTags: TagCollection =
  207. dataset === DiscoverDatasets.ERRORS
  208. ? Object.assign({}, functionTags, STATIC_FIELD_TAGS_WITHOUT_TRANSACTION_FIELDS)
  209. : dataset === DiscoverDatasets.TRANSACTIONS ||
  210. dataset === DiscoverDatasets.METRICS_ENHANCED
  211. ? Object.assign(
  212. {},
  213. measurementsWithKind,
  214. functionTags,
  215. STATIC_SPAN_TAGS,
  216. STATIC_FIELD_TAGS_WITHOUT_ERROR_FIELDS
  217. )
  218. : 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, filteredTags, STATIC_SEMVER_TAGS);
  228. combinedTags.has = getHasTag(combinedTags);
  229. return combinedTags;
  230. }, [
  231. measurements,
  232. dataset,
  233. customMeasurements,
  234. functionTags,
  235. filteredTags,
  236. organization.features,
  237. ]);
  238. const filterKeySections = useMemo(() => {
  239. const customTagsSection: FilterKeySection = {
  240. value: 'custom_fields',
  241. label: 'Custom Tags',
  242. children: Object.keys(filteredTags),
  243. };
  244. if (
  245. dataset === DiscoverDatasets.TRANSACTIONS ||
  246. dataset === DiscoverDatasets.METRICS_ENHANCED
  247. ) {
  248. return [...ALL_INSIGHTS_FILTER_KEY_SECTIONS, customTagsSection];
  249. }
  250. if (dataset === DiscoverDatasets.ERRORS) {
  251. return [...ERRORS_DATASET_FILTER_KEY_SECTIONS, customTagsSection];
  252. }
  253. return [...COMBINED_DATASET_FILTER_KEY_SECTIONS, customTagsSection];
  254. }, [filteredTags, dataset]);
  255. return (
  256. <SearchQueryBuilder
  257. placeholder={placeholderText}
  258. filterKeys={getTagList}
  259. initialQuery={props.query ?? ''}
  260. onSearch={props.onSearch}
  261. onChange={props.onChange}
  262. searchSource={props.searchSource || 'eventsv2'}
  263. filterKeySections={filterKeySections}
  264. getTagValues={getEventFieldValues}
  265. recentSearches={SavedSearchType.EVENT}
  266. showUnsubmittedIndicator
  267. />
  268. );
  269. }
  270. export default ResultsSearchQueryBuilder;