Skip to content

Commit afa1a33

Browse files
authored
feat(feature flags): Add basic Suspect Flag table to the Feature Flag Distributions flyout (#89978)
**Sorting Options** | Tags | Flags | | --- | --- | | ![tags-default](https://github.com/user-attachments/assets/f93cf101-811b-498f-aa11-7ed81145cd4d) | ![SCR-20250421-ifuk](https://github.com/user-attachments/assets/8bac40d3-a460-45fd-9e1c-58ec5f9e7553) **Flags UI** This is a partial representation of the following cases: - Feature `feature-flag-suspect-flags` on/off -> controls the yellow table - Feature `suspect-scores-sandbox-ui` on/off -> controls the checkbox - LocalStorage `flag-drawer-show-suspicion-scores` on/off -> controls if we see all the debug info & number input | Case | User Facing | Internal Debugging | | --- | --- | --- | | No flags enabled | ![ff-dist-default](https://github.com/user-attachments/assets/784d6a77-7516-4c43-a5bd-b86bb6cecad2) | | Sus Table Empty | ![ff-dist-sus-table-empty](https://github.com/user-attachments/assets/78dc6303-0aa7-4214-92fa-0f9149ff2378) | | Sus Table w/ Data | ![ff-dist-sus-table](https://github.com/user-attachments/assets/b6eb47e3-dace-4ca7-86ce-daf85058e41c) | ![ff-dist-sus-table-debug-false](https://github.com/user-attachments/assets/a583cec0-519a-4d76-9376-dc17b3ae2a97) | Sus Table w/ debugging | | ![ff-dist-sus-table-debug-true](https://github.com/user-attachments/assets/eb4e305b-cff2-4258-8923-48c91dc2a034) Depends on #89962
1 parent 62a9e5e commit afa1a33

File tree

5 files changed

+209
-33
lines changed

5 files changed

+209
-33
lines changed

static/app/components/events/eventDrawer.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export const EventDrawerBody = styled(DrawerBody)`
6969
overscroll-behavior: contain;
7070
/* Move the scrollbar to the left edge */
7171
scroll-margin: 0 ${space(2)};
72+
display: flex;
73+
gap: ${space(2)};
74+
flex-direction: column;
75+
height: fit-content; /* makes drawer resize work better with flex */
7276
direction: rtl;
7377
* {
7478
direction: ltr;

static/app/views/issueDetails/groupDistributions/flagsDistributionDrawer.tsx

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import {Fragment, useState} from 'react';
22
import styled from '@emotion/styled';
33

44
import AnalyticsArea from 'sentry/components/analyticsArea';
5+
import {Flex} from 'sentry/components/container/flex';
56
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
67
import {Checkbox} from 'sentry/components/core/checkbox';
78
import {EventDrawerBody, EventNavigator} from 'sentry/components/events/eventDrawer';
89
import FeatureFlagSort from 'sentry/components/events/featureFlags/featureFlagSort';
910
import {OrderBy, SortBy} from 'sentry/components/events/featureFlags/utils';
11+
import {IconSentry} from 'sentry/icons/iconSentry';
1012
import {t} from 'sentry/locale';
1113
import {space} from 'sentry/styles/space';
1214
import type {Group} from 'sentry/types/group';
@@ -56,14 +58,14 @@ export default function Flags({group, organization, setTab}: Props) {
5658

5759
const sortByOptions = enableSuspectFlags
5860
? [
59-
{
60-
label: t('Suspiciousness'),
61-
value: SortBy.SUSPICION,
62-
},
6361
{
6462
label: t('Alphabetical'),
6563
value: SortBy.ALPHABETICAL,
6664
},
65+
{
66+
label: t('Suspiciousness'),
67+
value: SortBy.SUSPICION,
68+
},
6769
]
6870
: [
6971
{
@@ -73,10 +75,6 @@ export default function Flags({group, organization, setTab}: Props) {
7375
];
7476
const orderByOptions = enableSuspectFlags
7577
? [
76-
{
77-
label: t('High to Low'),
78-
value: OrderBy.HIGH_TO_LOW,
79-
},
8078
{
8179
label: t('A-Z'),
8280
value: OrderBy.A_TO_Z,
@@ -85,6 +83,10 @@ export default function Flags({group, organization, setTab}: Props) {
8583
label: t('Z-A'),
8684
value: OrderBy.Z_TO_A,
8785
},
86+
{
87+
label: t('High to Low'),
88+
value: OrderBy.HIGH_TO_LOW,
89+
},
8890
]
8991
: [
9092
{
@@ -145,26 +147,27 @@ export default function Flags({group, organization, setTab}: Props) {
145147
</ButtonBar>
146148
)}
147149
</EventNavigator>
148-
{!tagKey && showSuspectSandboxUI && (
149-
<EventDrawerBody>
150-
<Label>
151-
{t('Debug')}{' '}
152-
<Checkbox
153-
checked={debugSuspectScores}
154-
onChange={() => {
155-
setDebugSuspectScores(debugSuspectScores ? '0' : '1');
156-
}}
157-
/>
158-
</Label>
159-
</EventDrawerBody>
160-
)}
161150
<EventDrawerBody>
162151
{tagKey ? (
163152
<AnalyticsArea name="feature_flag_details">
164153
<FlagDetailsDrawerContent />
165154
</AnalyticsArea>
166155
) : (
167156
<AnalyticsArea name="feature_flag_distributions">
157+
{showSuspectSandboxUI && (
158+
<Flex>
159+
<Label>
160+
<IconSentry size="xs" />
161+
{t('Debug')}
162+
<Checkbox
163+
checked={debugSuspectScores}
164+
onChange={() => {
165+
setDebugSuspectScores(debugSuspectScores ? '0' : '1');
166+
}}
167+
/>
168+
</Label>
169+
</Flex>
170+
)}
168171
<FlagDrawerContent
169172
debugSuspectScores={debugSuspectScores}
170173
environments={environments}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import styled from '@emotion/styled';
2+
import Color from 'color';
3+
4+
import {Flex} from 'sentry/components/container/flex';
5+
import {Alert} from 'sentry/components/core/alert';
6+
import {NumberInput} from 'sentry/components/core/input/numberInput';
7+
import {Tooltip} from 'sentry/components/core/tooltip';
8+
import {OrderBy, SortBy} from 'sentry/components/events/featureFlags/utils';
9+
import {IconSentry} from 'sentry/icons/iconSentry';
10+
import {t} from 'sentry/locale';
11+
import {space} from 'sentry/styles/space';
12+
import type {Group} from 'sentry/types/group';
13+
import toRoundedPercent from 'sentry/utils/number/toRoundedPercent';
14+
import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
15+
import useGroupFlagDrawerData from 'sentry/views/issueDetails/groupFeatureFlags/useGroupFlagDrawerData';
16+
import {TagBar} from 'sentry/views/issueDetails/groupTags/tagDistribution';
17+
18+
interface Props {
19+
debugSuspectScores: boolean;
20+
environments: string[];
21+
group: Group;
22+
}
23+
24+
const SUSPECT_SCORE_LOCAL_STATE_KEY = 'flag-drawer-suspicion-score-threshold';
25+
const SUSPECT_SCORE_THRESHOLD = 7;
26+
27+
export default function SuspectTable({debugSuspectScores, environments, group}: Props) {
28+
const [threshold, setThreshold] = useLocalStorageState(
29+
SUSPECT_SCORE_LOCAL_STATE_KEY,
30+
SUSPECT_SCORE_THRESHOLD
31+
);
32+
33+
const {displayFlags, isPending} = useGroupFlagDrawerData({
34+
environments,
35+
group,
36+
orderBy: OrderBy.HIGH_TO_LOW,
37+
search: '',
38+
sortBy: SortBy.SUSPICION,
39+
});
40+
41+
const debugThresholdInput = debugSuspectScores ? (
42+
<Flex gap={space(0.5)} align="center">
43+
<IconSentry size="xs" />
44+
Threshold:
45+
<NumberInput value={threshold} onChange={setThreshold} size="xs" />
46+
</Flex>
47+
) : null;
48+
49+
if (isPending) {
50+
return (
51+
<Alert type="muted">
52+
<TagHeader>
53+
{t('Suspect')}
54+
{debugThresholdInput}
55+
</TagHeader>
56+
{t('Loading...')}
57+
</Alert>
58+
);
59+
}
60+
61+
const susFlags = displayFlags.filter(flag => (flag.suspect.score ?? 0) > threshold);
62+
63+
if (!susFlags.length) {
64+
return (
65+
<Alert type="muted">
66+
<TagHeader>
67+
{t('Suspect')}
68+
{debugThresholdInput}
69+
</TagHeader>
70+
{t('Nothing suspicious')}
71+
</Alert>
72+
);
73+
}
74+
75+
return (
76+
<Alert type="warning">
77+
<TagHeader>
78+
{t('Suspect')}
79+
{debugThresholdInput}
80+
</TagHeader>
81+
82+
<TagValueGrid>
83+
{susFlags.map(flag => {
84+
const topValue = flag.topValues[0];
85+
86+
const pct =
87+
topValue?.value === 'true'
88+
? (flag.suspect.baselinePercent ?? 0)
89+
: 100 - (flag.suspect.baselinePercent ?? 0);
90+
const projPercentage = Math.round(pct * 100);
91+
const displayProjPercent =
92+
projPercentage < 1 ? '<1%' : `${projPercentage.toFixed(0)}%`;
93+
94+
return (
95+
<TagValueRow key={flag.key}>
96+
{/* TODO: why is flag.name transformed to TitleCase? */}
97+
<Tooltip title={flag.key} showOnlyOnOverflow>
98+
<Name>{flag.key}</Name>
99+
</Tooltip>
100+
<TagBar percentage={((topValue?.count ?? 0) / flag.totalValues) * 100} />
101+
<RightAligned>
102+
{toRoundedPercent((topValue?.count ?? 0) / flag.totalValues)}
103+
</RightAligned>
104+
<span>{topValue?.value}</span>
105+
<Subtext>vs</Subtext>
106+
<RightAligned>
107+
<Subtext>{t('%s in project', displayProjPercent)}</Subtext>
108+
</RightAligned>
109+
</TagValueRow>
110+
);
111+
})}
112+
</TagValueGrid>
113+
</Alert>
114+
);
115+
}
116+
117+
const TagHeader = styled('h5')`
118+
display: flex;
119+
justify-content: space-between;
120+
align-items: center;
121+
122+
margin-bottom: ${space(0.5)};
123+
font-size: ${p => p.theme.fontSizeMedium};
124+
font-weight: ${p => p.theme.fontWeightBold};
125+
`;
126+
127+
const progressBarWidth = '45px'; // Prevent percentages from overflowing
128+
const TagValueGrid = styled('ul')`
129+
display: grid;
130+
grid-template-columns: 4fr ${progressBarWidth} auto auto max-content auto;
131+
132+
margin: 0;
133+
padding: 0;
134+
list-style: none;
135+
`;
136+
137+
const TagValueRow = styled('li')`
138+
display: grid;
139+
grid-template-columns: subgrid;
140+
grid-column: 1 / -1;
141+
grid-column-gap: ${space(1)};
142+
align-items: center;
143+
padding: ${space(0.25)} ${space(0.75)};
144+
border-radius: ${p => p.theme.borderRadius};
145+
color: ${p => p.theme.textColor};
146+
font-variant-numeric: tabular-nums;
147+
148+
&:nth-child(2n) {
149+
background-color: ${p => Color(p.theme.gray300).alpha(0.1).toString()};
150+
}
151+
`;
152+
153+
const Name = styled('div')`
154+
text-overflow: ellipsis;
155+
overflow: hidden;
156+
white-space: nowrap;
157+
`;
158+
159+
const Subtext = styled('span')`
160+
color: ${p => p.theme.subText};
161+
`;
162+
const RightAligned = styled('span')`
163+
text-align: right;
164+
`;

static/app/views/issueDetails/groupFeatureFlags/flagDrawerContent.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {Group} from 'sentry/types/group';
1010
import {trackAnalytics} from 'sentry/utils/analytics';
1111
import useOrganization from 'sentry/utils/useOrganization';
1212
import useProjects from 'sentry/utils/useProjects';
13+
import SuspectTable from 'sentry/views/issueDetails/groupDistributions/suspectTable';
1314
import FlagDetailsLink from 'sentry/views/issueDetails/groupFeatureFlags/flagDetailsLink';
1415
import FlagDrawerCTA from 'sentry/views/issueDetails/groupFeatureFlags/flagDrawerCTA';
1516
import useGroupFlagDrawerData from 'sentry/views/issueDetails/groupFeatureFlags/useGroupFlagDrawerData';
@@ -38,6 +39,9 @@ export default function FlagDrawerContent({
3839
}: Props) {
3940
const organization = useOrganization();
4041

42+
// If we're showing the suspect section at all
43+
const enableSuspectFlags = organization.features.includes('feature-flag-suspect-flags');
44+
4145
const {displayFlags, allGroupFlagCount, isPending, isError, refetch} =
4246
useGroupFlagDrawerData({
4347
environments,
@@ -85,6 +89,13 @@ export default function FlagDrawerContent({
8589
</StyledEmptyStateWarning>
8690
) : (
8791
<Fragment>
92+
{enableSuspectFlags ? (
93+
<SuspectTable
94+
debugSuspectScores={debugSuspectScores}
95+
environments={environments}
96+
group={group}
97+
/>
98+
) : null}
8899
<Container>
89100
{displayFlags.map(flag => (
90101
<div key={flag.key}>

static/app/views/issueDetails/groupTags/tagDistribution.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function TagDistribution({tag}: {tag: GroupTag}) {
2828
<TagPanel>
2929
<TagHeader>
3030
<Tooltip title={tag.key} showOnlyOnOverflow skipWrapper>
31-
<TagTitle>{tag.key}</TagTitle>
31+
{tag.key}
3232
</Tooltip>
3333
</TagHeader>
3434
<TagValueContent>
@@ -100,28 +100,23 @@ export function TagBar({
100100
}
101101

102102
const TagPanel = styled('div')`
103-
display: block;
103+
display: flex;
104+
flex-direction: column;
105+
gap: ${space(0.5)};
104106
border-radius: ${p => p.theme.borderRadius};
105107
border: 1px solid ${p => p.theme.border};
106108
padding: ${space(1)};
107109
`;
108110

109111
const TagHeader = styled('h5')`
110-
grid-area: header;
111-
display: flex;
112-
justify-content: space-between;
113-
margin-bottom: ${space(0.5)};
114112
color: ${p => p.theme.textColor};
115-
`;
116-
117-
const TagTitle = styled('div')`
118113
font-size: ${p => p.theme.fontSizeMedium};
119114
font-weight: ${p => p.theme.fontWeightBold};
115+
margin: 0;
120116
${p => p.theme.overflowEllipsis}
121117
`;
122118

123-
// The 40px is a buffer to prevent percentages from overflowing
124-
const progressBarWidth = '45px';
119+
const progressBarWidth = '45px'; // Prevent percentages from overflowing
125120
const TagValueContent = styled('div')`
126121
display: grid;
127122
grid-template-columns: 4fr auto ${progressBarWidth};
@@ -144,7 +139,6 @@ const TagValue = styled('div')`
144139
text-overflow: ellipsis;
145140
overflow: hidden;
146141
white-space: nowrap;
147-
margin-right: ${space(0.5)};
148142
`;
149143

150144
const TagBarPlaceholder = styled('div')`

0 commit comments

Comments
 (0)