resultsSearchQueryBuilder.tsx 9.2 KB

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