eventSearch.tsx 5.7 KB

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