From 9f6d9f910f1fd5350f1120df42c97d4718e68a5b Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:01:51 -0700 Subject: [PATCH 1/9] Implement sections for click fields and custom tags. Add URL to REPLAY_FIELDS --- static/app/utils/fields/index.ts | 7 +++ .../views/replays/list/replaySearchBar.tsx | 50 +++++++++++++++---- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/static/app/utils/fields/index.ts b/static/app/utils/fields/index.ts index c357e4084a7894..a2d560f9bf3f2c 100644 --- a/static/app/utils/fields/index.ts +++ b/static/app/utils/fields/index.ts @@ -1398,6 +1398,7 @@ export enum ReplayFieldKey { OS_VERSION = 'os.version', SEEN_BY_ME = 'seen_by_me', URLS = 'urls', + URL = 'url', VIEWED_BY_ME = 'viewed_by_me', } @@ -1451,6 +1452,7 @@ export const REPLAY_FIELDS = [ ReplayFieldKey.SEEN_BY_ME, FieldKey.TRACE, ReplayFieldKey.URLS, + ReplayFieldKey.URL, FieldKey.USER_EMAIL, FieldKey.USER_ID, FieldKey.USER_IP, @@ -1524,6 +1526,11 @@ const REPLAY_FIELD_DEFINITIONS: Record = { kind: FieldKind.FIELD, valueType: FieldValueType.BOOLEAN, }, + [ReplayFieldKey.URL]: { + desc: t('A url visited within the replay'), + kind: FieldKind.FIELD, + valueType: FieldValueType.STRING, + }, [ReplayFieldKey.URLS]: { desc: t('List of urls that were visited within the replay'), kind: FieldKind.FIELD, diff --git a/static/app/views/replays/list/replaySearchBar.tsx b/static/app/views/replays/list/replaySearchBar.tsx index ac6d22f9c801ae..f04e2f88bc8660 100644 --- a/static/app/views/replays/list/replaySearchBar.tsx +++ b/static/app/views/replays/list/replaySearchBar.tsx @@ -1,7 +1,9 @@ import {useCallback, useEffect, useMemo} from 'react'; +import {orderBy} from 'lodash'; import {fetchTagValues, loadOrganizationTags} from 'sentry/actionCreators/tags'; import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; +import type {FilterKeySection} from 'sentry/components/searchQueryBuilder/types'; import SmartSearchBar from 'sentry/components/smartSearchBar'; import {MAX_QUERY_LENGTH, NEGATION_OPERATOR, SEARCH_WILDCARD} from 'sentry/constants'; import {t} from 'sentry/locale'; @@ -55,7 +57,7 @@ const REPLAY_CLICK_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_CLICK * Merges a list of supported tags and replay search fields into one collection. */ function getReplaySearchTags(supportedTags: TagCollection): TagCollection { - const allTags = { + return { ...REPLAY_FIELDS_AS_TAGS, ...REPLAY_CLICK_FIELDS_AS_TAGS, ...Object.fromEntries( @@ -68,15 +70,42 @@ function getReplaySearchTags(supportedTags: TagCollection): TagCollection { ]) ), }; - - // A hack used to "sort" the dictionary for SearchQueryBuilder. - // Technically dicts are unordered but this works in dev. - // To guarantee ordering, we need to implement filterKeySections. - const keys = Object.keys(allTags); - keys.sort(); - return Object.fromEntries(keys.map(key => [key, allTags[key]])); } +const getFilterKeySections = ( + tags: TagCollection, + organization: Organization +): FilterKeySection[] => { + if (!organization.features.includes('search-query-builder-replays')) { + return []; + } + + const excludedTags = ['browser', 'device', 'os', 'user']; + const customTags: Tag[] = Object.values(tags).filter( + tag => + !excludedTags.includes(tag.key) && + !REPLAY_FIELDS.map(String).includes(tag.key) && + !REPLAY_CLICK_FIELDS.map(String).includes(tag.key) + ); + + const orderedTagKeys = orderBy(customTags, ['totalValues', 'key'], ['desc', 'asc']).map( + tag => tag.key + ); + + return [ + { + value: 'replay_click_field', + label: t('Replay Click Fields'), + children: Object.keys(REPLAY_CLICK_FIELDS_AS_TAGS), + }, + { + value: FieldKind.TAG, + label: t('Custom Tags'), + children: orderedTagKeys, + }, + ]; +}; + type Props = React.ComponentProps & { organization: Organization; pageFilters: PageFilters; @@ -95,6 +124,9 @@ function ReplaySearchBar(props: Props) { () => getReplaySearchTags(organizationTags), [organizationTags] ); + const filterKeySections = useMemo(() => { + return getFilterKeySections(organizationTags, organization); + }, [organizationTags, organization]); const getTagValues = useCallback( (tag: Tag, searchQuery: string): Promise => { @@ -165,7 +197,7 @@ function ReplaySearchBar(props: Props) { className={props.className} fieldDefinitionGetter={getReplayFieldDefinition} filterKeys={replayTags} - filterKeySections={undefined} + filterKeySections={filterKeySections} getTagValues={getTagValues} initialQuery={props.query ?? props.defaultQuery ?? ''} onSearch={onSearchWithAnalytics} From 203b8f14354e07e6b83ce81e206f2d0564515829 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:06:48 -0700 Subject: [PATCH 2/9] Rename replayTags to filterKeys to reduce confusion --- static/app/views/replays/list/replaySearchBar.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/static/app/views/replays/list/replaySearchBar.tsx b/static/app/views/replays/list/replaySearchBar.tsx index f04e2f88bc8660..2cf83ac91d7098 100644 --- a/static/app/views/replays/list/replaySearchBar.tsx +++ b/static/app/views/replays/list/replaySearchBar.tsx @@ -54,9 +54,11 @@ const REPLAY_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_FIELDS); const REPLAY_CLICK_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_CLICK_FIELDS); /** - * Merges a list of supported tags and replay search fields into one collection. + * Merges a list of supported tags and replay search properties + * (https://docs.sentry.io/concepts/search/searchable-properties/session-replay/) + * into one collection. */ -function getReplaySearchTags(supportedTags: TagCollection): TagCollection { +function getReplayFilterKeys(supportedTags: TagCollection): TagCollection { return { ...REPLAY_FIELDS_AS_TAGS, ...REPLAY_CLICK_FIELDS_AS_TAGS, @@ -120,8 +122,8 @@ function ReplaySearchBar(props: Props) { loadOrganizationTags(api, organization.slug, pageFilters); }, [api, organization.slug, pageFilters]); - const replayTags = useMemo( - () => getReplaySearchTags(organizationTags), + const filterKeys = useMemo( + () => getReplayFilterKeys(organizationTags), [organizationTags] ); const filterKeySections = useMemo(() => { @@ -196,7 +198,7 @@ function ReplaySearchBar(props: Props) { disallowLogicalOperators={undefined} // ^ className={props.className} fieldDefinitionGetter={getReplayFieldDefinition} - filterKeys={replayTags} + filterKeys={filterKeys} filterKeySections={filterKeySections} getTagValues={getTagValues} initialQuery={props.query ?? props.defaultQuery ?? ''} @@ -215,7 +217,7 @@ function ReplaySearchBar(props: Props) { Date: Wed, 14 Aug 2024 23:10:25 -0700 Subject: [PATCH 3/9] Directly query issue platform tags --- .../views/replays/list/replaySearchBar.tsx | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/static/app/views/replays/list/replaySearchBar.tsx b/static/app/views/replays/list/replaySearchBar.tsx index 2cf83ac91d7098..647bfb7d39a04e 100644 --- a/static/app/views/replays/list/replaySearchBar.tsx +++ b/static/app/views/replays/list/replaySearchBar.tsx @@ -1,7 +1,7 @@ -import {useCallback, useEffect, useMemo} from 'react'; +import {useCallback, useMemo} from 'react'; import {orderBy} from 'lodash'; -import {fetchTagValues, loadOrganizationTags} from 'sentry/actionCreators/tags'; +import {fetchTagValues, useFetchOrganizationTags} from 'sentry/actionCreators/tags'; import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder'; import type {FilterKeySection} from 'sentry/components/searchQueryBuilder/types'; import SmartSearchBar from 'sentry/components/smartSearchBar'; @@ -22,7 +22,7 @@ import { } from 'sentry/utils/fields'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import useApi from 'sentry/utils/useApi'; -import useTags from 'sentry/utils/useTags'; +import {Dataset} from 'sentry/views/alerts/rules/metric/types'; const SEARCH_SPECIAL_CHARS_REGEXP = new RegExp( `^${NEGATION_OPERATOR}|\\${SEARCH_WILDCARD}`, @@ -117,18 +117,39 @@ function ReplaySearchBar(props: Props) { const {organization, pageFilters} = props; const api = useApi(); const projectIds = pageFilters.projects; - const organizationTags = useTags(); - useEffect(() => { - loadOrganizationTags(api, organization.slug, pageFilters); - }, [api, organization.slug, pageFilters]); + const tagQuery = useFetchOrganizationTags( + { + orgSlug: organization.slug, + projectIds: projectIds.map(String), + dataset: Dataset.ISSUE_PLATFORM, + useCache: true, + enabled: true, + keepPreviousData: false, + start: pageFilters.datetime.start + ? getUtcDateString(pageFilters.datetime.start) + : undefined, + end: pageFilters.datetime.end + ? getUtcDateString(pageFilters.datetime.end) + : undefined, + statsPeriod: pageFilters.datetime.period, + }, + {} + ); + const issuePlatformTags: TagCollection = useMemo(() => { + return (tagQuery.data ?? []).reduce((acc, tag) => { + acc[tag.key] = {...tag, kind: FieldKind.TAG}; + return acc; + }, {}); + }, [tagQuery]); + // tagQuery.isLoading and tagQuery.isError are not used const filterKeys = useMemo( - () => getReplayFilterKeys(organizationTags), - [organizationTags] + () => getReplayFilterKeys(issuePlatformTags), + [issuePlatformTags] ); const filterKeySections = useMemo(() => { - return getFilterKeySections(organizationTags, organization); - }, [organizationTags, organization]); + return getFilterKeySections(issuePlatformTags, organization); + }, [issuePlatformTags, organization]); const getTagValues = useCallback( (tag: Tag, searchQuery: string): Promise => { From e25cc05749de941ba21bef0a3f6a85addedbe6d0 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:20:47 -0700 Subject: [PATCH 4/9] Apply excluded tags for all tab --- .../views/replays/list/replaySearchBar.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/static/app/views/replays/list/replaySearchBar.tsx b/static/app/views/replays/list/replaySearchBar.tsx index 647bfb7d39a04e..52d6da4bb629d1 100644 --- a/static/app/views/replays/list/replaySearchBar.tsx +++ b/static/app/views/replays/list/replaySearchBar.tsx @@ -52,6 +52,7 @@ function fieldDefinitionsToTagCollection(fieldKeys: string[]): TagCollection { const REPLAY_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_FIELDS); const REPLAY_CLICK_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_CLICK_FIELDS); +const EXCLUDED_TAGS = ['browser', 'device', 'os', 'user']; /** * Merges a list of supported tags and replay search properties @@ -63,13 +64,15 @@ function getReplayFilterKeys(supportedTags: TagCollection): TagCollection { ...REPLAY_FIELDS_AS_TAGS, ...REPLAY_CLICK_FIELDS_AS_TAGS, ...Object.fromEntries( - Object.keys(supportedTags).map(key => [ - key, - { - ...supportedTags[key], - kind: getReplayFieldDefinition(key)?.kind ?? FieldKind.TAG, - }, - ]) + Object.keys(supportedTags) + .filter(key => !EXCLUDED_TAGS.includes(key)) + .map(key => [ + key, + { + ...supportedTags[key], + kind: getReplayFieldDefinition(key)?.kind ?? FieldKind.TAG, + }, + ]) ), }; } @@ -82,10 +85,9 @@ const getFilterKeySections = ( return []; } - const excludedTags = ['browser', 'device', 'os', 'user']; const customTags: Tag[] = Object.values(tags).filter( tag => - !excludedTags.includes(tag.key) && + !EXCLUDED_TAGS.includes(tag.key) && !REPLAY_FIELDS.map(String).includes(tag.key) && !REPLAY_CLICK_FIELDS.map(String).includes(tag.key) ); From c1c4a054946e5ac30559ca74d4f887a7100c15c2 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:31:07 -0700 Subject: [PATCH 5/9] Unwrap pageFilters date filters --- .../views/replays/list/replaySearchBar.tsx | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/static/app/views/replays/list/replaySearchBar.tsx b/static/app/views/replays/list/replaySearchBar.tsx index 52d6da4bb629d1..07745fb5cf94be 100644 --- a/static/app/views/replays/list/replaySearchBar.tsx +++ b/static/app/views/replays/list/replaySearchBar.tsx @@ -119,6 +119,14 @@ function ReplaySearchBar(props: Props) { const {organization, pageFilters} = props; const api = useApi(); const projectIds = pageFilters.projects; + const start = pageFilters.datetime.start + ? getUtcDateString(pageFilters.datetime.start) + : undefined; + const end = pageFilters.datetime.end + ? getUtcDateString(pageFilters.datetime.end) + : undefined; + const statsPeriod = pageFilters.datetime.period; + const tagQuery = useFetchOrganizationTags( { orgSlug: organization.slug, @@ -127,13 +135,9 @@ function ReplaySearchBar(props: Props) { useCache: true, enabled: true, keepPreviousData: false, - start: pageFilters.datetime.start - ? getUtcDateString(pageFilters.datetime.start) - : undefined, - end: pageFilters.datetime.end - ? getUtcDateString(pageFilters.datetime.end) - : undefined, - statsPeriod: pageFilters.datetime.period, + start: start, + end: end, + statsPeriod: statsPeriod, }, {} ); @@ -162,13 +166,9 @@ function ReplaySearchBar(props: Props) { } const endpointParams = { - start: pageFilters.datetime.start - ? getUtcDateString(pageFilters.datetime.start) - : undefined, - end: pageFilters.datetime.end - ? getUtcDateString(pageFilters.datetime.end) - : undefined, - statsPeriod: pageFilters.datetime.period, + start: start, + end: end, + statsPeriod: statsPeriod, }; return fetchTagValues({ @@ -186,14 +186,7 @@ function ReplaySearchBar(props: Props) { } ); }, - [ - api, - organization.slug, - projectIds, - pageFilters.datetime.end, - pageFilters.datetime.period, - pageFilters.datetime.start, - ] + [api, organization.slug, projectIds, start, end, statsPeriod] ); const onSearch = props.onSearch; From ea2f8cb5d6d664b646ddf7084234ebb4f2b98d2b Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Thu, 15 Aug 2024 08:59:36 -0700 Subject: [PATCH 6/9] Add replay fields as 'Suggested' section --- static/app/views/replays/list/replaySearchBar.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/static/app/views/replays/list/replaySearchBar.tsx b/static/app/views/replays/list/replaySearchBar.tsx index 07745fb5cf94be..f27c8824178c43 100644 --- a/static/app/views/replays/list/replaySearchBar.tsx +++ b/static/app/views/replays/list/replaySearchBar.tsx @@ -97,14 +97,19 @@ const getFilterKeySections = ( ); return [ + { + value: 'replay_field', + label: t('Suggested'), + children: Object.keys(REPLAY_FIELDS_AS_TAGS), + }, { value: 'replay_click_field', - label: t('Replay Click Fields'), + label: t('Click Fields'), children: Object.keys(REPLAY_CLICK_FIELDS_AS_TAGS), }, { value: FieldKind.TAG, - label: t('Custom Tags'), + label: t('Tags'), children: orderedTagKeys, }, ]; From 3388a5b7f5b60bcb8b8591f8d00ea71b951c3123 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 27 Aug 2024 14:04:35 -0700 Subject: [PATCH 7/9] comment explaining excluded tags --- static/app/views/replays/list/replaySearchBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/replays/list/replaySearchBar.tsx b/static/app/views/replays/list/replaySearchBar.tsx index f27c8824178c43..bc21dda91e2348 100644 --- a/static/app/views/replays/list/replaySearchBar.tsx +++ b/static/app/views/replays/list/replaySearchBar.tsx @@ -52,7 +52,7 @@ function fieldDefinitionsToTagCollection(fieldKeys: string[]): TagCollection { const REPLAY_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_FIELDS); const REPLAY_CLICK_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_CLICK_FIELDS); -const EXCLUDED_TAGS = ['browser', 'device', 'os', 'user']; +const EXCLUDED_TAGS = ['browser', 'device', 'os', 'user']; // These are excluded from the display but still valid search queries. browser.name, device.name, etc are effectively the same and included from REPLAY_FIELDS. Displaying these would be redundant and confusing. /** * Merges a list of supported tags and replay search properties From 20c7a8ea01a35de74213fa2aaa60eb0fe279d21d Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 27 Aug 2024 14:06:42 -0700 Subject: [PATCH 8/9] Change to docstring --- static/app/views/replays/list/replaySearchBar.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/static/app/views/replays/list/replaySearchBar.tsx b/static/app/views/replays/list/replaySearchBar.tsx index bc21dda91e2348..1ec12a8a5b1aed 100644 --- a/static/app/views/replays/list/replaySearchBar.tsx +++ b/static/app/views/replays/list/replaySearchBar.tsx @@ -52,7 +52,12 @@ function fieldDefinitionsToTagCollection(fieldKeys: string[]): TagCollection { const REPLAY_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_FIELDS); const REPLAY_CLICK_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_CLICK_FIELDS); -const EXCLUDED_TAGS = ['browser', 'device', 'os', 'user']; // These are excluded from the display but still valid search queries. browser.name, device.name, etc are effectively the same and included from REPLAY_FIELDS. Displaying these would be redundant and confusing. +/** + * Excluded from the display but still valid search queries. browser.name, + * device.name, etc are effectively the same and included from REPLAY_FIELDS. + * Displaying these would be redundant and confusing. + */ +const EXCLUDED_TAGS = ['browser', 'device', 'os', 'user']; /** * Merges a list of supported tags and replay search properties From 77e253d0bd44ab1c9c24c06ab0f7d217533a9fa5 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:15:49 -0700 Subject: [PATCH 9/9] Fix lodash import --- static/app/views/replays/list/replaySearchBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/replays/list/replaySearchBar.tsx b/static/app/views/replays/list/replaySearchBar.tsx index 1ec12a8a5b1aed..a16a9bfcf426f4 100644 --- a/static/app/views/replays/list/replaySearchBar.tsx +++ b/static/app/views/replays/list/replaySearchBar.tsx @@ -1,5 +1,5 @@ import {useCallback, useMemo} from 'react'; -import {orderBy} from 'lodash'; +import orderBy from 'lodash/orderBy'; import {fetchTagValues, useFetchOrganizationTags} from 'sentry/actionCreators/tags'; import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';