Skip to content

Commit

Permalink
feat(replay): implement search bar key sections and fetch tags from I…
Browse files Browse the repository at this point in the history
…P instead of discover (#76276)

Follow up to #75552. Implements
sections to make the typeahead easier to navigate.
[Screen
recording](https://drive.google.com/file/d/1HxV87JgZWaHHuA74EEdIOdbL1sDxNTzd/view?usp=sharing)

"Suggested" and "Click Fields" combined is = to our documented search
properties:
https://docs.sentry.io/concepts/search/searchable-properties/session-replay.
We always want to show these.

"Tags" are tags found in the Issue Platform dataset, sorted by times
seen in the dataset (popularity). Previously we were fetching these from
the TagStore which queries Discover. IP has a narrower set of more
relevant tags. Some more context is at
#75878
  • Loading branch information
aliu39 authored Aug 27, 2024
1 parent b52cb55 commit faeb0ee
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 46 deletions.
7 changes: 7 additions & 0 deletions static/app/utils/fields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1754,6 +1754,7 @@ export enum ReplayFieldKey {
OS_VERSION = 'os.version',
SEEN_BY_ME = 'seen_by_me',
URLS = 'urls',
URL = 'url',
VIEWED_BY_ME = 'viewed_by_me',
}

Expand Down Expand Up @@ -1807,6 +1808,7 @@ export const REPLAY_FIELDS = [
ReplayFieldKey.SEEN_BY_ME,
FieldKey.TRACE,
ReplayFieldKey.URLS,
ReplayFieldKey.URL,
FieldKey.USER_EMAIL,
FieldKey.USER_ID,
FieldKey.USER_IP,
Expand Down Expand Up @@ -1880,6 +1882,11 @@ const REPLAY_FIELD_DEFINITIONS: Record<ReplayFieldKey, FieldDefinition> = {
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,
Expand Down
152 changes: 106 additions & 46 deletions static/app/views/replays/list/replaySearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {useCallback, useEffect, useMemo} from 'react';
import {useCallback, useMemo} from 'react';
import orderBy from 'lodash/orderBy';

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';
import {MAX_QUERY_LENGTH, NEGATION_OPERATOR, SEARCH_WILDCARD} from 'sentry/constants';
import {t} from 'sentry/locale';
Expand All @@ -20,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}`,
Expand Down Expand Up @@ -50,33 +52,74 @@ function fieldDefinitionsToTagCollection(fieldKeys: string[]): TagCollection {

const REPLAY_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_FIELDS);
const REPLAY_CLICK_FIELDS_AS_TAGS = fieldDefinitionsToTagCollection(REPLAY_CLICK_FIELDS);
/**
* 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 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 {
const allTags = {
function getReplayFilterKeys(supportedTags: TagCollection): TagCollection {
return {
...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,
},
])
),
};

// 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 customTags: Tag[] = Object.values(tags).filter(
tag =>
!EXCLUDED_TAGS.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_field',
label: t('Suggested'),
children: Object.keys(REPLAY_FIELDS_AS_TAGS),
},
{
value: 'replay_click_field',
label: t('Click Fields'),
children: Object.keys(REPLAY_CLICK_FIELDS_AS_TAGS),
},
{
value: FieldKind.TAG,
label: t('Tags'),
children: orderedTagKeys,
},
];
};

type Props = React.ComponentProps<typeof SmartSearchBar> & {
organization: Organization;
pageFilters: PageFilters;
Expand All @@ -86,15 +129,43 @@ 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 replayTags = useMemo(
() => getReplaySearchTags(organizationTags),
[organizationTags]
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,
projectIds: projectIds.map(String),
dataset: Dataset.ISSUE_PLATFORM,
useCache: true,
enabled: true,
keepPreviousData: false,
start: start,
end: end,
statsPeriod: statsPeriod,
},
{}
);
const issuePlatformTags: TagCollection = useMemo(() => {
return (tagQuery.data ?? []).reduce<TagCollection>((acc, tag) => {
acc[tag.key] = {...tag, kind: FieldKind.TAG};
return acc;
}, {});
}, [tagQuery]);
// tagQuery.isLoading and tagQuery.isError are not used

const filterKeys = useMemo(
() => getReplayFilterKeys(issuePlatformTags),
[issuePlatformTags]
);
const filterKeySections = useMemo(() => {
return getFilterKeySections(issuePlatformTags, organization);
}, [issuePlatformTags, organization]);

const getTagValues = useCallback(
(tag: Tag, searchQuery: string): Promise<string[]> => {
Expand All @@ -105,13 +176,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({
Expand All @@ -129,14 +196,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;
Expand Down Expand Up @@ -164,8 +224,8 @@ function ReplaySearchBar(props: Props) {
disallowLogicalOperators={undefined} // ^
className={props.className}
fieldDefinitionGetter={getReplayFieldDefinition}
filterKeys={replayTags}
filterKeySections={undefined}
filterKeys={filterKeys}
filterKeySections={filterKeySections}
getTagValues={getTagValues}
initialQuery={props.query ?? props.defaultQuery ?? ''}
onSearch={onSearchWithAnalytics}
Expand All @@ -183,7 +243,7 @@ function ReplaySearchBar(props: Props) {
<SmartSearchBar
{...props}
onGetTagValues={getTagValues}
supportedTags={replayTags}
supportedTags={filterKeys}
placeholder={
props.placeholder ??
t('Search for users, duration, clicked elements, count_errors, and more')
Expand Down

0 comments on commit faeb0ee

Please sign in to comment.