eventSearch.tsx 6.3 KB

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