resultsSearchQueryBuilder.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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. recentSearches?: SavedSearchType;
  120. searchSource?: string;
  121. supportedTags?: TagCollection | undefined;
  122. };
  123. const EXCLUDED_FILTER_KEYS = [FieldKey.ENVIRONMENT, FieldKey.TOTAL_COUNT];
  124. function ResultsSearchQueryBuilder(props: Props) {
  125. const {
  126. omitTags,
  127. fields,
  128. projectIds,
  129. includeSessionTagsValues,
  130. customMeasurements,
  131. dataset,
  132. includeTransactions = true,
  133. placeholder,
  134. } = props;
  135. const api = useApi();
  136. const organization = useOrganization();
  137. const {selection} = usePageFilters();
  138. const tags = useTags();
  139. const filteredTags = useMemo(() => {
  140. return omitTags && omitTags.length > 0
  141. ? omit(tags, omitTags, EXCLUDED_FILTER_KEYS)
  142. : omit(tags, EXCLUDED_FILTER_KEYS);
  143. }, [tags, omitTags]);
  144. const placeholderText = useMemo(() => {
  145. return placeholder ?? t('Search for events, users, tags, and more');
  146. }, [placeholder]);
  147. const measurements = useMemo(() => getMeasurements(), []);
  148. const functionTags = useMemo(() => getFunctionTags(fields), [fields]);
  149. // Returns array of tag values that substring match `query`; invokes `callback`
  150. // with data when ready
  151. const getEventFieldValues = useCallback(
  152. async (tag, query): Promise<string[]> => {
  153. const projectIdStrings = (projectIds as Readonly<number>[])?.map(String);
  154. if (isAggregateField(tag.key) || isMeasurement(tag.key)) {
  155. // We can't really auto suggest values for aggregate fields
  156. // or measurements, so we simply don't
  157. return Promise.resolve([]);
  158. }
  159. // device.class is stored as "numbers" in snuba, but we want to suggest high, medium,
  160. // and low search filter values because discover maps device.class to these values.
  161. if (isDeviceClass(tag.key)) {
  162. return Promise.resolve(DEVICE_CLASS_TAG_VALUES);
  163. }
  164. const fetchPromise =
  165. dataset === DiscoverDatasets.SPANS_INDEXED
  166. ? fetchSpanFieldValues({
  167. api,
  168. endpointParams: normalizeDateTimeParams(selection.datetime),
  169. orgSlug: organization.slug,
  170. fieldKey: tag.key,
  171. search: query,
  172. projectIds: projectIdStrings,
  173. })
  174. : fetchTagValues({
  175. api,
  176. endpointParams: normalizeDateTimeParams(selection.datetime),
  177. orgSlug: organization.slug,
  178. tagKey: tag.key,
  179. search: query,
  180. projectIds: projectIdStrings,
  181. // allows searching for tags on transactions as well
  182. includeTransactions,
  183. // allows searching for tags on sessions as well
  184. includeSessions: includeSessionTagsValues,
  185. dataset: dataset ? DiscoverDatasetsToDatasetMap[dataset] : undefined,
  186. });
  187. try {
  188. const results = await fetchPromise;
  189. return results.filter(({name}) => defined(name)).map(({name}) => name);
  190. } catch (error) {
  191. throw new Error('Unable to fetch event field values');
  192. }
  193. },
  194. [
  195. api,
  196. organization,
  197. selection.datetime,
  198. projectIds,
  199. includeTransactions,
  200. includeSessionTagsValues,
  201. dataset,
  202. ]
  203. );
  204. const getTagList: TagCollection = useMemo(() => {
  205. const measurementsWithKind = getMeasurementTags(measurements, customMeasurements);
  206. const orgHasPerformanceView = organization.features.includes('performance-view');
  207. const combinedTags: TagCollection =
  208. dataset === DiscoverDatasets.ERRORS
  209. ? Object.assign({}, functionTags, STATIC_FIELD_TAGS_WITHOUT_TRANSACTION_FIELDS)
  210. : dataset === DiscoverDatasets.TRANSACTIONS ||
  211. dataset === DiscoverDatasets.METRICS_ENHANCED
  212. ? Object.assign(
  213. {},
  214. measurementsWithKind,
  215. functionTags,
  216. STATIC_SPAN_TAGS,
  217. STATIC_FIELD_TAGS_WITHOUT_ERROR_FIELDS
  218. )
  219. : orgHasPerformanceView
  220. ? Object.assign(
  221. {},
  222. measurementsWithKind,
  223. functionTags,
  224. STATIC_SPAN_TAGS,
  225. STATIC_FIELD_TAGS
  226. )
  227. : Object.assign({}, STATIC_FIELD_TAGS_WITHOUT_TRACING);
  228. Object.assign(combinedTags, filteredTags, STATIC_SEMVER_TAGS);
  229. combinedTags.has = getHasTag(combinedTags);
  230. return combinedTags;
  231. }, [
  232. measurements,
  233. dataset,
  234. customMeasurements,
  235. functionTags,
  236. filteredTags,
  237. organization.features,
  238. ]);
  239. const filterKeySections = useMemo(() => {
  240. const customTagsSection: FilterKeySection = {
  241. value: 'custom_fields',
  242. label: 'Custom Tags',
  243. children: Object.keys(filteredTags),
  244. };
  245. if (
  246. dataset === DiscoverDatasets.TRANSACTIONS ||
  247. dataset === DiscoverDatasets.METRICS_ENHANCED
  248. ) {
  249. return [...ALL_INSIGHTS_FILTER_KEY_SECTIONS, customTagsSection];
  250. }
  251. if (dataset === DiscoverDatasets.ERRORS) {
  252. return [...ERRORS_DATASET_FILTER_KEY_SECTIONS, customTagsSection];
  253. }
  254. return [...COMBINED_DATASET_FILTER_KEY_SECTIONS, customTagsSection];
  255. }, [filteredTags, dataset]);
  256. return (
  257. <SearchQueryBuilder
  258. placeholder={placeholderText}
  259. filterKeys={getTagList}
  260. initialQuery={props.query ?? ''}
  261. onSearch={props.onSearch}
  262. onChange={props.onChange}
  263. searchSource={props.searchSource || 'eventsv2'}
  264. filterKeySections={filterKeySections}
  265. getTagValues={getEventFieldValues}
  266. recentSearches={props.recentSearches ?? SavedSearchType.EVENT}
  267. showUnsubmittedIndicator
  268. />
  269. );
  270. }
  271. export default ResultsSearchQueryBuilder;