From 98882597a7925be8f3d790a28e7197415db8a9be Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Sun, 20 Apr 2025 17:19:19 -0700 Subject: [PATCH 1/4] feat(feature flags): Add basic Suspect Flag table to the Feature Flag Distributions flyout --- static/app/components/events/eventDrawer.tsx | 4 + .../flagsDistributionDrawer.tsx | 45 ++--- .../groupDistributions/suspectTable.tsx | 160 ++++++++++++++++++ .../groupFeatureFlags/flagDrawerContent.tsx | 11 ++ .../groupTags/tagDistribution.tsx | 17 +- 5 files changed, 204 insertions(+), 33 deletions(-) create mode 100644 static/app/views/issueDetails/groupDistributions/suspectTable.tsx diff --git a/static/app/components/events/eventDrawer.tsx b/static/app/components/events/eventDrawer.tsx index 3b906b8f27a8c1..d8d122e0f374b4 100644 --- a/static/app/components/events/eventDrawer.tsx +++ b/static/app/components/events/eventDrawer.tsx @@ -69,6 +69,10 @@ export const EventDrawerBody = styled(DrawerBody)` overscroll-behavior: contain; /* Move the scrollbar to the left edge */ scroll-margin: 0 ${space(2)}; + display: flex; + gap: ${space(2)}; + flex-direction: column; + height: fit-content; /* makes drawer resize work better with flex */ direction: rtl; * { direction: ltr; diff --git a/static/app/views/issueDetails/groupDistributions/flagsDistributionDrawer.tsx b/static/app/views/issueDetails/groupDistributions/flagsDistributionDrawer.tsx index c19da14be1c722..976ffdc0e68319 100644 --- a/static/app/views/issueDetails/groupDistributions/flagsDistributionDrawer.tsx +++ b/static/app/views/issueDetails/groupDistributions/flagsDistributionDrawer.tsx @@ -2,11 +2,13 @@ import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import AnalyticsArea from 'sentry/components/analyticsArea'; +import {Flex} from 'sentry/components/container/flex'; import {ButtonBar} from 'sentry/components/core/button/buttonBar'; import {Checkbox} from 'sentry/components/core/checkbox'; import {EventDrawerBody, EventNavigator} from 'sentry/components/events/eventDrawer'; import FeatureFlagSort from 'sentry/components/events/featureFlags/featureFlagSort'; import {OrderBy, SortBy} from 'sentry/components/events/featureFlags/utils'; +import {IconSentry} from 'sentry/icons/iconSentry'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Group} from 'sentry/types/group'; @@ -56,14 +58,14 @@ export default function Flags({group, organization, setTab}: Props) { const sortByOptions = enableSuspectFlags ? [ - { - label: t('Suspiciousness'), - value: SortBy.SUSPICION, - }, { label: t('Alphabetical'), value: SortBy.ALPHABETICAL, }, + { + label: t('Suspiciousness'), + value: SortBy.SUSPICION, + }, ] : [ { @@ -73,10 +75,6 @@ export default function Flags({group, organization, setTab}: Props) { ]; const orderByOptions = enableSuspectFlags ? [ - { - label: t('High to Low'), - value: OrderBy.HIGH_TO_LOW, - }, { label: t('A-Z'), value: OrderBy.A_TO_Z, @@ -85,6 +83,10 @@ export default function Flags({group, organization, setTab}: Props) { label: t('Z-A'), value: OrderBy.Z_TO_A, }, + { + label: t('High to Low'), + value: OrderBy.HIGH_TO_LOW, + }, ] : [ { @@ -145,19 +147,6 @@ export default function Flags({group, organization, setTab}: Props) { )} - {!tagKey && showSuspectSandboxUI && ( - - - - )} {tagKey ? ( @@ -165,6 +154,20 @@ export default function Flags({group, organization, setTab}: Props) { ) : ( + {showSuspectSandboxUI && ( + + + + )} + + Threshold: + + + ) : null; + + if (isPending) { + return ( + + + {t('Suspect')} + {debugThresholdInput} + + {t('Loading...')} + + ); + } + + const susFlags = displayFlags.filter(flag => (flag.suspect.score ?? 0) > threshold); + + if (!susFlags.length) { + return ( + + + {t('Suspect')} + {debugThresholdInput} + + {t('Nothing suspicious')} + + ); + } + + return ( + + + {t('Suspect')} + {debugThresholdInput} + + + + {susFlags.map(flag => { + const topValue = flag.topValues[0]; + + const projPercentage = Math.round((flag.suspect.baselinePercent ?? 0) * 100); + const displayProjPercent = + projPercentage < 1 ? '<1%' : `${projPercentage.toFixed(0)}%`; + + return ( + + {/* TODO: why is flag.name transformed to TitleCase? */} + + {flag.key} + + + + {toRoundedPercent((topValue?.count ?? 0) / flag.totalValues)} + + {topValue?.value} + vs + + {t('%s in project', displayProjPercent)} + + + ); + })} + + + ); +} + +const TagHeader = styled('h5')` + display: flex; + justify-content: space-between; + align-items: center; + + margin-bottom: ${space(0.5)}; + font-size: ${p => p.theme.fontSizeMedium}; + font-weight: ${p => p.theme.fontWeightBold}; +`; + +const progressBarWidth = '45px'; // Prevent percentages from overflowing +const TagValueGrid = styled('ul')` + display: grid; + grid-template-columns: 4fr ${progressBarWidth} auto auto max-content auto; + + margin: 0; + padding: 0; + list-style: none; +`; + +const TagValueRow = styled('li')` + display: grid; + grid-template-columns: subgrid; + grid-column: 1 / -1; + grid-column-gap: ${space(1)}; + align-items: center; + padding: ${space(0.25)} ${space(0.75)}; + border-radius: ${p => p.theme.borderRadius}; + color: ${p => p.theme.textColor}; + font-variant-numeric: tabular-nums; + + &:nth-child(2n) { + background-color: ${p => Color(p.theme.gray300).alpha(0.1).toString()}; + } +`; + +const Name = styled('div')` + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +`; + +const Subtext = styled('span')` + color: ${p => p.theme.subText}; +`; +const RightAligned = styled('span')` + text-align: right; +`; diff --git a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerContent.tsx b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerContent.tsx index 717e062a18fa9f..9b5a70c3fe5b5e 100644 --- a/static/app/views/issueDetails/groupFeatureFlags/flagDrawerContent.tsx +++ b/static/app/views/issueDetails/groupFeatureFlags/flagDrawerContent.tsx @@ -10,6 +10,7 @@ import type {Group} from 'sentry/types/group'; import {trackAnalytics} from 'sentry/utils/analytics'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; +import SuspectTable from 'sentry/views/issueDetails/groupDistributions/suspectTable'; import FlagDetailsLink from 'sentry/views/issueDetails/groupFeatureFlags/flagDetailsLink'; import FlagDrawerCTA from 'sentry/views/issueDetails/groupFeatureFlags/flagDrawerCTA'; import useGroupFlagDrawerData from 'sentry/views/issueDetails/groupFeatureFlags/useGroupFlagDrawerData'; @@ -38,6 +39,9 @@ export default function FlagDrawerContent({ }: Props) { const organization = useOrganization(); + // If we're showing the suspect section at all + const enableSuspectFlags = organization.features.includes('feature-flag-suspect-flags'); + const {displayFlags, allGroupFlagCount, isPending, isError, refetch} = useGroupFlagDrawerData({ environments, @@ -85,6 +89,13 @@ export default function FlagDrawerContent({ ) : ( + {enableSuspectFlags ? ( + + ) : null} {displayFlags.map(flag => (
diff --git a/static/app/views/issueDetails/groupTags/tagDistribution.tsx b/static/app/views/issueDetails/groupTags/tagDistribution.tsx index c37b590d635fef..6205440a11b009 100644 --- a/static/app/views/issueDetails/groupTags/tagDistribution.tsx +++ b/static/app/views/issueDetails/groupTags/tagDistribution.tsx @@ -28,7 +28,7 @@ export function TagDistribution({tag}: {tag: GroupTag}) { - {tag.key} + {tag.key} @@ -100,28 +100,22 @@ export function TagBar({ } const TagPanel = styled('div')` - display: block; + display: flex; + flex-direction: column; + gap: ${space(0.5)}; border-radius: ${p => p.theme.borderRadius}; border: 1px solid ${p => p.theme.border}; padding: ${space(1)}; `; const TagHeader = styled('h5')` - grid-area: header; - display: flex; - justify-content: space-between; - margin-bottom: ${space(0.5)}; color: ${p => p.theme.textColor}; -`; - -const TagTitle = styled('div')` font-size: ${p => p.theme.fontSizeMedium}; font-weight: ${p => p.theme.fontWeightBold}; ${p => p.theme.overflowEllipsis} `; -// The 40px is a buffer to prevent percentages from overflowing -const progressBarWidth = '45px'; +const progressBarWidth = '45px'; // Prevent percentages from overflowing const TagValueContent = styled('div')` display: grid; grid-template-columns: 4fr auto ${progressBarWidth}; @@ -144,7 +138,6 @@ const TagValue = styled('div')` text-overflow: ellipsis; overflow: hidden; white-space: nowrap; - margin-right: ${space(0.5)}; `; const TagBarPlaceholder = styled('div')` From ef154bdfe89ac9f8faafab40c7bf49cc7618e52f Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 21 Apr 2025 10:58:43 -0700 Subject: [PATCH 2/4] fix crazy margin --- .../components/group/tagFacets/tagFacetsDistributionMeter.tsx | 4 ++-- static/app/views/issueDetails/groupTags/tagDistribution.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx b/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx index 31976c2d10fa0d..6f82d0ba6c501a 100644 --- a/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx +++ b/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx @@ -246,7 +246,7 @@ function TagFacetsDistributionMeter({
e.preventDefault()}> - setExpanded(!expanded)}> + setExpanded(!expanded)}> {renderTitle()} {renderSegments()} @@ -264,7 +264,7 @@ const TagSummary = styled('div')` `; const TagHeader = styled('span')<{clickable?: boolean}>` - ${p => (p.clickable ? 'cursor: pointer' : null)}; + cursor: pointer; `; const SegmentBar = styled('div')` diff --git a/static/app/views/issueDetails/groupTags/tagDistribution.tsx b/static/app/views/issueDetails/groupTags/tagDistribution.tsx index 6205440a11b009..25439ce88f8c20 100644 --- a/static/app/views/issueDetails/groupTags/tagDistribution.tsx +++ b/static/app/views/issueDetails/groupTags/tagDistribution.tsx @@ -112,6 +112,7 @@ const TagHeader = styled('h5')` color: ${p => p.theme.textColor}; font-size: ${p => p.theme.fontSizeMedium}; font-weight: ${p => p.theme.fontWeightBold}; + margin: 0; ${p => p.theme.overflowEllipsis} `; From 087f30b42de1d6acd134edd66eac3128fdb0b721 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 21 Apr 2025 11:01:19 -0700 Subject: [PATCH 3/4] split into another PR --- .../components/group/tagFacets/tagFacetsDistributionMeter.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx b/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx index 6f82d0ba6c501a..31976c2d10fa0d 100644 --- a/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx +++ b/static/app/components/group/tagFacets/tagFacetsDistributionMeter.tsx @@ -246,7 +246,7 @@ function TagFacetsDistributionMeter({
e.preventDefault()}> - setExpanded(!expanded)}> + setExpanded(!expanded)}> {renderTitle()} {renderSegments()} @@ -264,7 +264,7 @@ const TagSummary = styled('div')` `; const TagHeader = styled('span')<{clickable?: boolean}>` - cursor: pointer; + ${p => (p.clickable ? 'cursor: pointer' : null)}; `; const SegmentBar = styled('div')` From 7a2c278fb37298caf115d74d64c511af60c96834 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 22 Apr 2025 08:26:40 -0700 Subject: [PATCH 4/4] account for the outlier value being "false" and the baseline needs to be inverted --- .../views/issueDetails/groupDistributions/suspectTable.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/groupDistributions/suspectTable.tsx b/static/app/views/issueDetails/groupDistributions/suspectTable.tsx index 54aaeda87505d5..2463c213cafe7e 100644 --- a/static/app/views/issueDetails/groupDistributions/suspectTable.tsx +++ b/static/app/views/issueDetails/groupDistributions/suspectTable.tsx @@ -83,7 +83,11 @@ export default function SuspectTable({debugSuspectScores, environments, group}: {susFlags.map(flag => { const topValue = flag.topValues[0]; - const projPercentage = Math.round((flag.suspect.baselinePercent ?? 0) * 100); + const pct = + topValue?.value === 'true' + ? (flag.suspect.baselinePercent ?? 0) + : 100 - (flag.suspect.baselinePercent ?? 0); + const projPercentage = Math.round(pct * 100); const displayProjPercent = projPercentage < 1 ? '<1%' : `${projPercentage.toFixed(0)}%`;