Skip to content

Commit 8130023

Browse files
billyvgaliu39
authored andcommitted
feat(flags): Add feature flags to bubbles + drawer (#92367)
This adds feature flags to release bubbles + drawer. See https://linear.app/getsentry/issue/REPLAY-18/add-feature-flags-into-bubbles Depends on #92363 On issue details ![image](https://github.com/user-attachments/assets/103fa036-106a-4250-86dc-88fe02e7f63f) Inside of drawer ![image](https://github.com/user-attachments/assets/22f85313-8104-47d3-a95e-1eefe6c0c80a) --------- Co-authored-by: Andrew Liu <[email protected]>
1 parent 1df0db5 commit 8130023

18 files changed

+637
-94
lines changed

static/app/components/featureFlags/featureFlagsLogTable.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface FeatureFlagsLogTableProps {
2424
onResizeColumn?: (columnIndex: number, nextColumn: GridColumnOrder<ColumnKey>) => void;
2525
onRowMouseOut?: (dataRow: RawFlag, key: number) => void;
2626
onRowMouseOver?: (dataRow: RawFlag, key: number) => void;
27+
scrollable?: boolean;
2728
}
2829

2930
export function FeatureFlagsLogTable({
@@ -37,6 +38,7 @@ export function FeatureFlagsLogTable({
3738
onRowMouseOver,
3839
onRowMouseOut,
3940
highlightedRowKey,
41+
scrollable = false,
4042
}: FeatureFlagsLogTableProps) {
4143
const organization = useOrganization();
4244
const analyticsArea = useAnalyticsArea();
@@ -93,7 +95,7 @@ export function FeatureFlagsLogTable({
9395
onRowMouseOver={onRowMouseOver}
9496
onRowMouseOut={onRowMouseOut}
9597
highlightedRowKey={highlightedRowKey}
96-
scrollable={false}
98+
scrollable={scrollable}
9799
data-test-id="audit-log-table"
98100
/>
99101

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type {Event} from 'sentry/types/event';
2+
import {useApiQuery} from 'sentry/utils/queryClient';
3+
import useOrganization from 'sentry/utils/useOrganization';
4+
import {useGroup} from 'sentry/views/issueDetails/useGroup';
5+
6+
interface FetchGroupAndEventParams {
7+
enabled: boolean;
8+
eventId: string | undefined;
9+
groupId: string | undefined;
10+
}
11+
12+
export function useFetchGroupAndEvent({
13+
eventId,
14+
groupId,
15+
enabled,
16+
}: FetchGroupAndEventParams) {
17+
const organization = useOrganization();
18+
const {
19+
data: group,
20+
isPending: isGroupPending,
21+
isError: isGroupError,
22+
error: groupError,
23+
} = useGroup({
24+
groupId: groupId!,
25+
options: {enabled: enabled && Boolean(groupId && eventId)},
26+
});
27+
28+
const projectSlug = group?.project.slug;
29+
const {
30+
data: event,
31+
isPending: isEventPending,
32+
isError: isEventError,
33+
error: eventError,
34+
} = useApiQuery<Event>(
35+
[`/organizations/${organization.slug}/events/${projectSlug}:${eventId}/`],
36+
{
37+
staleTime: Infinity,
38+
enabled: enabled && Boolean(eventId && projectSlug && organization.slug),
39+
}
40+
);
41+
42+
return {
43+
event,
44+
group,
45+
eventFlags: event?.contexts?.flags?.values?.map(f => f.flag),
46+
47+
isPending: isGroupPending || isEventPending,
48+
isGroupPending,
49+
isEventPending,
50+
isError: isGroupError || isEventError,
51+
isGroupError,
52+
isEventError,
53+
error: groupError || eventError,
54+
groupError,
55+
eventError,
56+
};
57+
}

static/app/components/featureFlags/hooks/useFlagSeries.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export function useFlagSeries({event, flags}: FlagSeriesProps) {
7373

7474
return {
7575
seriesName: t('Feature Flags'),
76+
id: 'flag-lines',
7677
data: [],
7778
color: theme.blue200,
7879
markLine,
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import {useFetchGroupAndEvent} from 'sentry/components/featureFlags/hooks/useFetchGroupAndEvent';
2+
import {
3+
useOrganizationFlagLog,
4+
useOrganizationFlagLogInfinite,
5+
} from 'sentry/components/featureFlags/hooks/useOrganizationFlagLog';
6+
import type {Event} from 'sentry/types/event';
7+
import type {Group} from 'sentry/types/group';
8+
import {useApiQuery} from 'sentry/utils/queryClient';
9+
import useOrganization from 'sentry/utils/useOrganization';
10+
import {useGroup} from 'sentry/views/issueDetails/useGroup';
11+
12+
type FetchGroupAndEventParams = Parameters<typeof useFetchGroupAndEvent>[0];
13+
interface FlagsInEventParams extends FetchGroupAndEventParams {
14+
query: Record<string, any>;
15+
event?: Event;
16+
group?: Group;
17+
}
18+
19+
/**
20+
* Returns the feature flags that have been changed in a given time period and that appear on the event.
21+
*/
22+
export function useFlagsInEventPaginated({
23+
eventId,
24+
groupId,
25+
query,
26+
enabled,
27+
}: FlagsInEventParams) {
28+
const organization = useOrganization();
29+
const {
30+
group,
31+
isGroupPending,
32+
isGroupError,
33+
groupError,
34+
event,
35+
isEventPending,
36+
isEventError,
37+
eventError,
38+
eventFlags,
39+
isPending,
40+
isError,
41+
error,
42+
} = useFetchGroupAndEvent({
43+
eventId,
44+
groupId,
45+
enabled,
46+
});
47+
48+
const {
49+
data: rawFlagData,
50+
getResponseHeader,
51+
isPending: isFlagsPending,
52+
isError: isFlagsError,
53+
error: flagsError,
54+
} = useOrganizationFlagLog({
55+
organization,
56+
query: {
57+
...query,
58+
flag: eventFlags,
59+
},
60+
enabled: enabled && Boolean(eventFlags?.length),
61+
});
62+
const pageLinks = getResponseHeader?.('Link') ?? null;
63+
64+
return {
65+
flags: rawFlagData?.data ?? [],
66+
event,
67+
group,
68+
pageLinks,
69+
70+
isPending: isPending || isFlagsPending,
71+
isGroupPending,
72+
isEventPending,
73+
isFlagsPending,
74+
isError: isError || isFlagsError,
75+
isGroupError,
76+
isEventError,
77+
isFlagsError,
78+
error: error || flagsError,
79+
groupError,
80+
eventError,
81+
flagsError,
82+
};
83+
}
84+
85+
/**
86+
* Returns the feature flags that have been changed in a given time period and that appear on the event.
87+
*/
88+
export function useFlagsInEvent({
89+
eventId,
90+
groupId,
91+
group: groupProp,
92+
event: eventProp,
93+
query,
94+
enabled,
95+
}: FlagsInEventParams) {
96+
const organization = useOrganization();
97+
const {
98+
data: groupData,
99+
isPending: isGroupPending,
100+
isError: isGroupError,
101+
error: groupError,
102+
} = useGroup({
103+
groupId: groupId!,
104+
options: {enabled: enabled && Boolean(groupId && eventId && !groupProp)},
105+
});
106+
const group = groupProp ?? groupData;
107+
108+
const projectSlug = group?.project.slug;
109+
const {
110+
data: eventData,
111+
isPending: isEventPending,
112+
isError: isEventError,
113+
error: eventError,
114+
} = useApiQuery<Event>(
115+
[`/organizations/${organization.slug}/events/${projectSlug}:${eventId}/`],
116+
{
117+
staleTime: Infinity,
118+
enabled:
119+
enabled && Boolean(eventId && projectSlug && organization.slug) && !eventProp,
120+
}
121+
);
122+
const event = eventProp ?? eventData;
123+
124+
const eventFlags = event?.contexts?.flags?.values?.map(f => f.flag);
125+
126+
const {
127+
data: rawFlagData,
128+
isPending: isFlagsPending,
129+
isError: isFlagsError,
130+
error: flagsError,
131+
} = useOrganizationFlagLogInfinite({
132+
organization,
133+
query: {
134+
...query,
135+
flag: eventFlags,
136+
},
137+
enabled: enabled && Boolean(eventFlags?.length),
138+
});
139+
140+
return {
141+
flags: rawFlagData ?? [],
142+
event,
143+
group,
144+
145+
isPending: isGroupPending || isEventPending || isFlagsPending,
146+
isGroupPending,
147+
isEventPending,
148+
isFlagsPending,
149+
isError: isGroupError || isEventError || isFlagsError,
150+
isGroupError,
151+
isEventError,
152+
isFlagsError,
153+
error: groupError || eventError || flagsError,
154+
groupError,
155+
eventError,
156+
flagsError,
157+
};
158+
}

static/app/components/featureFlags/hooks/useIntersectionFlags.tsx

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1+
import {useEffect} from 'react';
2+
13
import type {RawFlagData} from 'sentry/components/featureFlags/utils';
24
import type {Organization} from 'sentry/types/organization';
3-
import {useApiQuery} from 'sentry/utils/queryClient';
5+
import {useApiQuery, useInfiniteApiQuery} from 'sentry/utils/queryClient';
6+
7+
interface Params {
8+
organization: Organization;
9+
query: Record<string, any>;
10+
enabled?: boolean;
11+
}
412

513
export function useOrganizationFlagLog({
614
organization,
715
query,
816
enabled: enabledParam = true,
9-
}: {
10-
organization: Organization;
11-
query: Record<string, any>;
12-
enabled?: boolean;
13-
}) {
17+
}: Params) {
1418
// Don't make the request if start = end. The backend returns 400 but we prefer an empty response.
1519
const enabled =
1620
(!query.start || !query.end || query.start !== query.end) && enabledParam;
@@ -23,3 +27,48 @@ export function useOrganizationFlagLog({
2327
}
2428
);
2529
}
30+
31+
interface InfiniteParams extends Params {
32+
maxPages?: number;
33+
}
34+
35+
/**
36+
* This is an analog to `useReleaseStats` where we fetch all pages of data so that we can render it on `<EventGraph />`.
37+
*/
38+
export function useOrganizationFlagLogInfinite({
39+
organization,
40+
query,
41+
enabled: enabledParam = true,
42+
maxPages = 10,
43+
}: InfiniteParams) {
44+
// Don't make the request if start = end. The backend returns 400 but we prefer an empty response.
45+
const enabled =
46+
(!query.start || !query.end || query.start !== query.end) && enabledParam;
47+
48+
const apiQuery = useInfiniteApiQuery<RawFlagData>({
49+
queryKey: [
50+
'infinite' as const,
51+
`/organizations/${organization.slug}/flags/logs/`,
52+
{query},
53+
],
54+
staleTime: 0,
55+
enabled,
56+
});
57+
58+
const currentNumberPages = apiQuery.data?.pages.length ?? 0;
59+
60+
useEffect(() => {
61+
if (
62+
!apiQuery.isFetching &&
63+
apiQuery.hasNextPage &&
64+
currentNumberPages + 1 < maxPages
65+
) {
66+
apiQuery.fetchNextPage();
67+
}
68+
}, [apiQuery, maxPages, currentNumberPages]);
69+
70+
return {
71+
...apiQuery,
72+
data: apiQuery.data?.pages.flatMap(([pageData]) => pageData.data),
73+
};
74+
}

static/app/components/gridEditable/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ class GridEditable<
478478
)}
479479
</Header>
480480
)}
481-
<Body style={bodyStyle}>
481+
<Body style={bodyStyle} showVerticalScrollbar={scrollable}>
482482
<Grid
483483
aria-label={ariaLabel}
484484
data-test-id="grid-editable"

0 commit comments

Comments
 (0)