Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(issues): add feature flags to issue stream search suggestions #83968

Merged
merged 15 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion static/app/actionCreators/tags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
useApiQuery,
type UseApiQueryOptions,
} from 'sentry/utils/queryClient';
import type {Dataset} from 'sentry/views/alerts/rules/metric/types';
import {Dataset} from 'sentry/views/alerts/rules/metric/types';

const MAX_TAGS = 1000;

Expand Down Expand Up @@ -212,6 +212,62 @@ export function fetchSpanFieldValues({
});
}

/**
* Fetch feature flag values for an organization. This is done by querying ERRORS with useFlagsBackend=1.
* This only returns feature flags, not other tags.
*
* The `projectIds` argument can be used to subset projects.
*/
export function fetchFeatureFlagValues({
api,
orgSlug,
tagKey,
endpointParams,
projectIds,
search,
sort,
}: {
api: Client;
orgSlug: string;
tagKey: string;
endpointParams?: Query;
projectIds?: string[];
search?: string;
sort?: '-last_seen' | '-count';
}): Promise<TagValue[]> {
const url = `/organizations/${orgSlug}/tags/${tagKey}/values/`;

const query: Query = {};
query.dataset = Dataset.ERRORS;
query.useFlagsBackend = '1';
aliu39 marked this conversation as resolved.
Show resolved Hide resolved

if (search) {
query.query = search;
}
if (projectIds) {
query.project = projectIds;
}
if (endpointParams) {
if (endpointParams.start) {
query.start = endpointParams.start;
}
if (endpointParams.end) {
query.end = endpointParams.end;
}
if (endpointParams.statsPeriod) {
query.statsPeriod = endpointParams.statsPeriod;
}
}
if (sort) {
query.sort = sort;
}

return api.requestPromise(url, {
method: 'GET',
query,
});
}

type FetchOrganizationTagsParams = {
orgSlug: string;
dataset?: Dataset;
Expand All @@ -222,13 +278,15 @@ type FetchOrganizationTagsParams = {
start?: string;
statsPeriod?: string | null;
useCache?: boolean;
useFlagsBackend?: boolean;
};

export const makeFetchOrganizationTags = ({
orgSlug,
dataset,
projectIds,
useCache = true,
useFlagsBackend = false,
statsPeriod,
start,
end,
Expand All @@ -238,6 +296,7 @@ export const makeFetchOrganizationTags = ({
query.dataset = dataset;
query.useCache = useCache ? '1' : '0';
query.project = projectIds;
query.useFlagsBackend = useFlagsBackend ? '1' : '0';

if (statsPeriod) {
query.statsPeriod = statsPeriod;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export function KeyDescription({size = 'sm', tag}: KeyDescriptionProps) {

const description =
fieldDefinition?.desc ??
(tag.kind === FieldKind.TAG ? t('A tag sent with one or more events') : null);
(tag.kind === FieldKind.TAG
? t('A tag sent with one or more events')
: tag.kind === FieldKind.FEATURE_FLAG
? t('A feature flag evaluated before an error event')
: null);

return (
<DescriptionWrapper size={size}>
Expand Down
1 change: 1 addition & 0 deletions static/app/utils/fields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {CONDITIONS_ARGUMENTS, WEB_VITALS_QUALITY} from '../discover/types';

export enum FieldKind {
TAG = 'tag',
FEATURE_FLAG = 'feature_flag',
MEASUREMENT = 'measurement',
BREAKDOWN = 'breakdown',
FIELD = 'field',
Expand Down
49 changes: 36 additions & 13 deletions static/app/views/issueList/searchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {useCallback, useMemo} from 'react';
import orderBy from 'lodash/orderBy';

import {fetchTagValues} from 'sentry/actionCreators/tags';
import {fetchFeatureFlagValues, fetchTagValues} from 'sentry/actionCreators/tags';
import {
SearchQueryBuilder,
type SearchQueryBuilderProps,
Expand All @@ -23,20 +23,30 @@ const getFilterKeySections = (tags: TagCollection): FilterKeySection[] => {
const allTags: Tag[] = Object.values(tags).filter(
tag => !EXCLUDED_TAGS.includes(tag.key)
);

const issueFields = orderBy(
allTags.filter(tag => tag.kind === FieldKind.ISSUE_FIELD),
['key']
).map(tag => tag.key);

const eventFields = orderBy(
allTags.filter(tag => tag.kind === FieldKind.EVENT_FIELD),
['key']
).map(tag => tag.key);

// TODO: flag[*] syntax not implemented yet by search backend.
const eventTags = orderBy(
allTags.filter(tag => tag.kind === FieldKind.TAG),
['totalValues', 'key'],
['desc', 'asc']
).map(tag => tag.key);

const eventFeatureFlags = orderBy(
allTags.filter(tag => tag.kind === FieldKind.FEATURE_FLAG),
['totalValues', 'key'],
['desc', 'asc']
).map(tag => tag.key);

return [
{
value: FieldKind.ISSUE_FIELD,
Expand All @@ -53,6 +63,11 @@ const getFilterKeySections = (tags: TagCollection): FilterKeySection[] => {
label: t('Event Tags'),
children: eventTags,
},
{
aliu39 marked this conversation as resolved.
Show resolved Hide resolved
value: FieldKind.FEATURE_FLAG,
label: t('Event Feature Flags'),
aliu39 marked this conversation as resolved.
Show resolved Hide resolved
children: eventFeatureFlags,
},
];
};

Expand Down Expand Up @@ -96,22 +111,30 @@ function IssueListSearchBar({
sort: '-count' as const,
};

const [eventsDatasetValues, issuePlatformDatasetValues] = await Promise.all([
fetchTagValues({
...fetchTagValuesPayload,
dataset: Dataset.ERRORS,
}),
fetchTagValues({
...fetchTagValuesPayload,
dataset: Dataset.ISSUE_PLATFORM,
}),
]);

return mergeAndSortTagValues(
const [eventsDatasetValues, issuePlatformDatasetValues, featureFlagValues] =
await Promise.all([
aliu39 marked this conversation as resolved.
Show resolved Hide resolved
fetchTagValues({
...fetchTagValuesPayload,
dataset: Dataset.ERRORS,
}),
fetchTagValues({
...fetchTagValuesPayload,
dataset: Dataset.ISSUE_PLATFORM,
}),
fetchFeatureFlagValues({
aliu39 marked this conversation as resolved.
Show resolved Hide resolved
...fetchTagValuesPayload,
}),
]);

const eventsAndIssuePlatformValues = mergeAndSortTagValues(
eventsDatasetValues,
issuePlatformDatasetValues,
'count'
);

return featureFlagValues
? mergeAndSortTagValues(eventsAndIssuePlatformValues, featureFlagValues, 'count')
: eventsAndIssuePlatformValues;
},
[
api,
Expand Down
44 changes: 41 additions & 3 deletions static/app/views/issueList/utils/useFetchIssueTags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type UseFetchIssueTagsParams = {
projectIds: string[];
enabled?: boolean;
end?: string;
includeFeatureFlags?: boolean;
keepPreviousData?: boolean;
start?: string;
statsPeriod?: string | null;
Expand Down Expand Up @@ -91,6 +92,7 @@ export const useFetchIssueTags = ({
keepPreviousData = false,
useCache = true,
enabled = true,
includeFeatureFlags = false,
...statsPeriodParams
}: UseFetchIssueTagsParams) => {
const {teams} = useLegacyStore(TeamStore);
Expand Down Expand Up @@ -122,6 +124,22 @@ export const useFetchIssueTags = ({
{}
);

// For now, feature flag keys (see `flags` column of the ERRORS dataset) are exposed from the tags endpoint,
// with query param useFlagsBackend=1. This is used for issue stream search suggestions.
const featureFlagTagsQuery = useFetchOrganizationTags(
{
orgSlug: org.slug,
projectIds,
dataset: Dataset.ERRORS,
useCache,
useFlagsBackend: true, // Queries `flags` column instead of tags. Response format is the same.
enabled: enabled && includeFeatureFlags, // Only make this query if includeFeatureFlags is true.
keepPreviousData,
...statsPeriodParams,
},
{}
);

const allTags = useMemo(() => {
const userTeams = teams.filter(team => team.isMember).map(team => `#${team.slug}`);
const usernames: string[] = members.map(getUsername);
Expand Down Expand Up @@ -151,6 +169,7 @@ export const useFetchIssueTags = ({

const eventsTags: Tag[] = eventsTagsQuery.data || [];
const issuePlatformTags: Tag[] = issuePlatformTagsQuery.data || [];
const featureFlagTags: Tag[] = featureFlagTagsQuery.data || [];

const allTagsCollection: TagCollection = eventsTags.reduce<TagCollection>(
(acc, tag) => {
Expand All @@ -169,6 +188,13 @@ export const useFetchIssueTags = ({
}
});

featureFlagTags.forEach(tag => {
// Wrap with "flags[]" to avoid collisions with other tags and fields.
tag.key = `flags[${tag.key}]`;
tag.name = `flags[${tag.name}]`;
allTagsCollection[tag.key] = {...tag, kind: FieldKind.FEATURE_FLAG};
});

for (const excludedTag of EXCLUDED_TAGS) {
delete allTagsCollection[excludedTag];
}
Expand All @@ -184,12 +210,24 @@ export const useFetchIssueTags = ({
...renamedTags,
...additionalTags,
};
}, [eventsTagsQuery.data, issuePlatformTagsQuery.data, members, teams]);
}, [
eventsTagsQuery.data,
issuePlatformTagsQuery.data,
featureFlagTagsQuery.data,
members,
teams,
]);

return {
tags: allTags,
isLoading: eventsTagsQuery.isPending || issuePlatformTagsQuery.isPending,
isError: eventsTagsQuery.isError || issuePlatformTagsQuery.isError,
isLoading:
eventsTagsQuery.isPending ||
issuePlatformTagsQuery.isPending ||
featureFlagTagsQuery.isPending,
isError:
eventsTagsQuery.isError ||
issuePlatformTagsQuery.isError ||
featureFlagTagsQuery.isError,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export function useIssueListFilterKeys() {
org: organization,
projectIds: pageFilters.projects.map(id => id.toString()),
keepPreviousData: true,
includeFeatureFlags: true,
start: pageFilters.datetime.start
? getUtcDateString(pageFilters.datetime.start)
: undefined,
Expand Down
Loading