eventSearch.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import {useCallback, useMemo} from 'react';
  2. import orderBy from 'lodash/orderBy';
  3. import {fetchTagValues} from 'sentry/actionCreators/tags';
  4. import {
  5. SearchQueryBuilder,
  6. type SearchQueryBuilderProps,
  7. } from 'sentry/components/searchQueryBuilder';
  8. import type {FilterKeySection} from 'sentry/components/searchQueryBuilder/types';
  9. import {parseQueryBuilderValue} from 'sentry/components/searchQueryBuilder/utils';
  10. import {joinQuery, Token} from 'sentry/components/searchSyntax/parser';
  11. import {t} from 'sentry/locale';
  12. import type {Group, Tag, TagCollection} from 'sentry/types/group';
  13. import {
  14. FieldKind,
  15. getFieldDefinition,
  16. ISSUE_EVENT_PROPERTY_FIELDS,
  17. } from 'sentry/utils/fields';
  18. import useApi from 'sentry/utils/useApi';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import usePageFilters from 'sentry/utils/usePageFilters';
  22. import {Dataset} from 'sentry/views/alerts/rules/metric/types';
  23. import {ALL_EVENTS_EXCLUDED_TAGS} from 'sentry/views/issueDetails/groupEvents';
  24. import {
  25. type GroupTag,
  26. useGroupTags,
  27. } from 'sentry/views/issueDetails/groupTags/useGroupTags';
  28. import {
  29. mergeAndSortTagValues,
  30. useHasStreamlinedUI,
  31. } from 'sentry/views/issueDetails/utils';
  32. import {makeGetIssueTagValues} from 'sentry/views/issueList/utils/getIssueTagValues';
  33. interface EventSearchProps {
  34. environments: string[];
  35. group: Group;
  36. handleSearch: (value: string) => void;
  37. query: string;
  38. className?: string;
  39. queryBuilderProps?: Partial<SearchQueryBuilderProps>;
  40. }
  41. export function useEventQuery({group}: {group: Group}): string {
  42. const {selection} = usePageFilters();
  43. const location = useLocation();
  44. const environments = selection.environments;
  45. const {query: locationQuery} = location.query;
  46. let eventQuery = '';
  47. if (Array.isArray(locationQuery)) {
  48. eventQuery = locationQuery.join(' ');
  49. } else if (typeof locationQuery === 'string') {
  50. eventQuery = locationQuery;
  51. }
  52. const {data = []} = useGroupTags({
  53. groupId: group.id,
  54. environment: environments,
  55. });
  56. const filterKeys = useEventSearchFilterKeys(data);
  57. const parsedQuery = useMemo(
  58. () =>
  59. parseQueryBuilderValue(eventQuery, getFieldDefinition, {
  60. filterKeys,
  61. }) ?? [],
  62. [eventQuery, filterKeys]
  63. );
  64. // Removes invalid tokens from an issue stream query in an attempt to convert it to an event query.
  65. // For example: "is:unresolved browser.name:firefox" -> "browser.name:firefox"
  66. // Note: This is _probably_ not accounting for MANY invalid filters which could come in from the
  67. // issue stream. Will likely have to refine this in the future.
  68. const validQuery = parsedQuery.filter(token => {
  69. if (token.type === Token.FREE_TEXT) {
  70. return false;
  71. }
  72. if (token.type === Token.FILTER && !filterKeys.hasOwnProperty(token.key.text)) {
  73. return false;
  74. }
  75. return true;
  76. });
  77. return joinQuery(validQuery, false, true);
  78. }
  79. function useEventSearchFilterKeys(data: GroupTag[]): TagCollection {
  80. const filterKeys = useMemo<TagCollection>(() => {
  81. const tags = [
  82. ...data.map(tag => ({...tag, kind: FieldKind.TAG})),
  83. ...ISSUE_EVENT_PROPERTY_FIELDS.map(tag => ({
  84. key: tag,
  85. name: tag,
  86. kind: FieldKind.EVENT_FIELD,
  87. })),
  88. ].filter(tag => !ALL_EVENTS_EXCLUDED_TAGS.includes(tag.key));
  89. return tags.reduce<TagCollection>((acc, tag) => {
  90. acc[tag.key] = tag;
  91. return acc;
  92. }, {});
  93. }, [data]);
  94. return filterKeys;
  95. }
  96. function getFilterKeySections(tags: TagCollection): FilterKeySection[] {
  97. const allTags: Tag[] = Object.values(tags).filter(
  98. tag => !ALL_EVENTS_EXCLUDED_TAGS.includes(tag.key)
  99. );
  100. const eventFields = orderBy(
  101. allTags.filter(tag => tag.kind === FieldKind.EVENT_FIELD),
  102. ['key']
  103. ).map(tag => tag.key);
  104. const eventTags = orderBy(
  105. allTags.filter(tag => tag.kind === FieldKind.TAG),
  106. ['totalValues', 'key'],
  107. ['desc', 'asc']
  108. ).map(tag => tag.key);
  109. return [
  110. {
  111. value: FieldKind.EVENT_FIELD,
  112. label: t('Event Filters'),
  113. children: eventFields,
  114. },
  115. {
  116. value: FieldKind.TAG,
  117. label: t('Event Tags'),
  118. children: eventTags,
  119. },
  120. ];
  121. }
  122. export function EventSearch({
  123. className,
  124. query,
  125. group,
  126. environments,
  127. handleSearch,
  128. queryBuilderProps = {},
  129. }: EventSearchProps) {
  130. const api = useApi();
  131. const organization = useOrganization();
  132. const hasStreamlinedUI = useHasStreamlinedUI();
  133. const {data = []} = useGroupTags({
  134. groupId: group.id,
  135. environment: environments,
  136. });
  137. const filterKeys = useEventSearchFilterKeys(data);
  138. const tagValueLoader = useCallback(
  139. async (key: string, search: string) => {
  140. const orgSlug = organization.slug;
  141. const projectIds = [group.project.id];
  142. const [eventsDatasetValues, issuePlatformDatasetValues] = await Promise.all([
  143. fetchTagValues({
  144. api,
  145. orgSlug,
  146. tagKey: key,
  147. search,
  148. projectIds,
  149. dataset: Dataset.ERRORS,
  150. }),
  151. fetchTagValues({
  152. api,
  153. orgSlug,
  154. tagKey: key,
  155. search,
  156. projectIds,
  157. dataset: Dataset.ISSUE_PLATFORM,
  158. }),
  159. ]);
  160. return mergeAndSortTagValues(eventsDatasetValues, issuePlatformDatasetValues);
  161. },
  162. [api, group.project.id, organization.slug]
  163. );
  164. const getTagValues = useMemo(
  165. () => makeGetIssueTagValues(tagValueLoader),
  166. [tagValueLoader]
  167. );
  168. const filterKeySections = useMemo(() => getFilterKeySections(filterKeys), [filterKeys]);
  169. return (
  170. <SearchQueryBuilder
  171. initialQuery={query}
  172. onSearch={handleSearch}
  173. filterKeys={filterKeys}
  174. filterKeySections={filterKeySections}
  175. getTagValues={getTagValues}
  176. placeholder={hasStreamlinedUI ? t('Filter events\u2026') : t('Search events\u2026')}
  177. label={hasStreamlinedUI ? t('Filter events\u2026') : t('Search events')}
  178. searchSource={hasStreamlinedUI ? 'issue_details_header' : 'issue_events_tab'}
  179. className={className}
  180. showUnsubmittedIndicator
  181. {...queryBuilderProps}
  182. />
  183. );
  184. }