Skip to content

Commit 6937875

Browse files
authored
feat(top-issues): show escalating top issues (#104568)
Show cluster stats on if issues within the cluster are new, escalating, or regressed <img width="591" height="329" alt="Screenshot 2025-12-09 at 9 05 07 AM" src="https://github.com/user-attachments/assets/4f709e89-9632-4727-9d50-9cb3308ef0f8" />
1 parent 4b10383 commit 6937875

File tree

1 file changed

+182
-3
lines changed

1 file changed

+182
-3
lines changed

static/app/views/issueList/pages/dynamicGrouping.tsx

Lines changed: 182 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import Redirect from 'sentry/components/redirect';
2727
import TimeSince from 'sentry/components/timeSince';
2828
import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
2929
import {
30+
IconArrow,
3031
IconCalendar,
3132
IconChevron,
3233
IconClock,
@@ -35,7 +36,9 @@ import {
3536
IconEllipsis,
3637
IconFire,
3738
IconFix,
39+
IconRefresh,
3840
IconSeer,
41+
IconStar,
3942
IconUpload,
4043
IconUser,
4144
} from 'sentry/icons';
@@ -190,8 +193,11 @@ function CompactIssuePreview({group}: {group: Group}) {
190193

191194
interface ClusterStats {
192195
firstSeen: string | null;
196+
hasRegressedIssues: boolean;
197+
isEscalating: boolean;
193198
isPending: boolean;
194199
lastSeen: string | null;
200+
newIssuesCount: number;
195201
totalEvents: number;
196202
totalUsers: number;
197203
}
@@ -222,6 +228,9 @@ function useClusterStats(groupIds: number[]): ClusterStats {
222228
totalUsers: 0,
223229
firstSeen: null,
224230
lastSeen: null,
231+
newIssuesCount: 0,
232+
hasRegressedIssues: false,
233+
isEscalating: false,
225234
isPending,
226235
};
227236
}
@@ -231,6 +240,19 @@ function useClusterStats(groupIds: number[]): ClusterStats {
231240
let earliestFirstSeen: Date | null = null;
232241
let latestLastSeen: Date | null = null;
233242

243+
// Calculate new issues (first seen within last week)
244+
const oneWeekAgo = new Date();
245+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
246+
let newIssuesCount = 0;
247+
248+
// Check for regressed issues
249+
let hasRegressedIssues = false;
250+
251+
// Calculate escalation by summing event stats across all issues
252+
// We'll compare the first half of the 24h stats to the second half
253+
let firstHalfEvents = 0;
254+
let secondHalfEvents = 0;
255+
234256
for (const group of groups) {
235257
totalEvents += parseInt(group.count, 10) || 0;
236258
totalUsers += group.userCount || 0;
@@ -240,6 +262,10 @@ function useClusterStats(groupIds: number[]): ClusterStats {
240262
if (!earliestFirstSeen || firstSeenDate < earliestFirstSeen) {
241263
earliestFirstSeen = firstSeenDate;
242264
}
265+
// Check if this issue is new (first seen within last week)
266+
if (firstSeenDate >= oneWeekAgo) {
267+
newIssuesCount++;
268+
}
243269
}
244270

245271
if (group.lastSeen) {
@@ -248,13 +274,39 @@ function useClusterStats(groupIds: number[]): ClusterStats {
248274
latestLastSeen = lastSeenDate;
249275
}
250276
}
277+
278+
// Check for regressed substatus
279+
if (group.substatus === GroupSubstatus.REGRESSED) {
280+
hasRegressedIssues = true;
281+
}
282+
283+
// Aggregate 24h stats for escalation detection
284+
const stats24h = group.stats?.['24h'];
285+
if (stats24h && stats24h.length > 0) {
286+
const midpoint = Math.floor(stats24h.length / 2);
287+
for (let i = 0; i < stats24h.length; i++) {
288+
const eventCount = stats24h[i]?.[1] ?? 0;
289+
if (i < midpoint) {
290+
firstHalfEvents += eventCount;
291+
} else {
292+
secondHalfEvents += eventCount;
293+
}
294+
}
295+
}
251296
}
252297

298+
// Determine if escalating: second half has >1.5x events compared to first half
299+
// Only consider escalating if there were events in the first half (avoid division by zero)
300+
const isEscalating = firstHalfEvents > 0 && secondHalfEvents > firstHalfEvents * 1.5;
301+
253302
return {
254303
totalEvents,
255304
totalUsers,
256305
firstSeen: earliestFirstSeen?.toISOString() ?? null,
257306
lastSeen: latestLastSeen?.toISOString() ?? null,
307+
newIssuesCount,
308+
hasRegressedIssues,
309+
isEscalating,
258310
isPending,
259311
};
260312
}, [groups, isPending]);
@@ -297,7 +349,13 @@ function ClusterIssues({groupIds}: {groupIds: number[]}) {
297349
);
298350
}
299351

300-
function ClusterCard({cluster}: {cluster: ClusterSummary}) {
352+
interface ClusterCardProps {
353+
cluster: ClusterSummary;
354+
filterByEscalating?: boolean;
355+
filterByRegressed?: boolean;
356+
}
357+
358+
function ClusterCard({cluster, filterByRegressed, filterByEscalating}: ClusterCardProps) {
301359
const api = useApi();
302360
const organization = useOrganization();
303361
const {selection} = usePageFilters();
@@ -403,6 +461,17 @@ function ClusterCard({cluster}: {cluster: ClusterSummary}) {
403461
];
404462
}, [cluster.error_type_tags, cluster.code_area_tags, cluster.service_tags]);
405463

464+
// Apply filters - hide card if it doesn't match active filters
465+
// Only filter once stats are loaded to avoid hiding cards prematurely
466+
if (!clusterStats.isPending) {
467+
if (filterByRegressed && !clusterStats.hasRegressedIssues) {
468+
return null;
469+
}
470+
if (filterByEscalating && !clusterStats.isEscalating) {
471+
return null;
472+
}
473+
}
474+
406475
return (
407476
<CardContainer>
408477
<CardHeader>
@@ -473,6 +542,41 @@ function ClusterCard({cluster}: {cluster: ClusterSummary}) {
473542
</StatItem>
474543
)}
475544
</ClusterStats>
545+
{!clusterStats.isPending &&
546+
(clusterStats.newIssuesCount > 0 ||
547+
clusterStats.hasRegressedIssues ||
548+
clusterStats.isEscalating) && (
549+
<ClusterStatusTags>
550+
{clusterStats.newIssuesCount > 0 && (
551+
<StatusTag color="purple">
552+
<IconStar size="xs" />
553+
<Text size="xs" bold>
554+
{tn(
555+
'%s new issue this week',
556+
'%s new issues this week',
557+
clusterStats.newIssuesCount
558+
)}
559+
</Text>
560+
</StatusTag>
561+
)}
562+
{clusterStats.hasRegressedIssues && (
563+
<StatusTag color="yellow">
564+
<IconRefresh size="xs" />
565+
<Text size="xs" bold>
566+
{t('Has regressed issues')}
567+
</Text>
568+
</StatusTag>
569+
)}
570+
{clusterStats.isEscalating && (
571+
<StatusTag color="red">
572+
<IconArrow direction="up" size="xs" />
573+
<Text size="xs" bold>
574+
{t('Escalating')}
575+
</Text>
576+
</StatusTag>
577+
)}
578+
</ClusterStatusTags>
579+
)}
476580
</CardHeader>
477581

478582
<TabSection>
@@ -633,6 +737,8 @@ function DynamicGrouping() {
633737
const [disableFilters, setDisableFilters] = useState(false);
634738
const [showDevTools, setShowDevTools] = useState(false);
635739
const [visibleClusterCount, setVisibleClusterCount] = useState(CLUSTERS_PER_PAGE);
740+
const [filterByRegressed, setFilterByRegressed] = useState(false);
741+
const [filterByEscalating, setFilterByEscalating] = useState(false);
636742

637743
// Fetch cluster data from API
638744
const {data: topIssuesResponse, isPending} = useApiQuery<TopIssuesResponse>(
@@ -959,6 +1065,30 @@ function DynamicGrouping() {
9591065
</Flex>
9601066
</Flex>
9611067
)}
1068+
1069+
<Flex direction="column" gap="sm">
1070+
<FilterLabel>{t('Filter by status')}</FilterLabel>
1071+
<Flex direction="column" gap="xs" style={{paddingLeft: 8}}>
1072+
<Flex gap="sm" align="center">
1073+
<Checkbox
1074+
checked={filterByRegressed}
1075+
onChange={e => setFilterByRegressed(e.target.checked)}
1076+
aria-label={t('Show only clusters with regressed issues')}
1077+
size="sm"
1078+
/>
1079+
<FilterLabel>{t('Has regressed issues')}</FilterLabel>
1080+
</Flex>
1081+
<Flex gap="sm" align="center">
1082+
<Checkbox
1083+
checked={filterByEscalating}
1084+
onChange={e => setFilterByEscalating(e.target.checked)}
1085+
aria-label={t('Show only escalating clusters')}
1086+
size="sm"
1087+
/>
1088+
<FilterLabel>{t('Escalating (>1.5x events)')}</FilterLabel>
1089+
</Flex>
1090+
</Flex>
1091+
</Flex>
9621092
</Flex>
9631093
</Disclosure.Content>
9641094
</Disclosure>
@@ -983,14 +1113,24 @@ function DynamicGrouping() {
9831113
{displayedClusters
9841114
.filter((_, index) => index % 2 === 0)
9851115
.map(cluster => (
986-
<ClusterCard key={cluster.cluster_id} cluster={cluster} />
1116+
<ClusterCard
1117+
key={cluster.cluster_id}
1118+
cluster={cluster}
1119+
filterByRegressed={filterByRegressed}
1120+
filterByEscalating={filterByEscalating}
1121+
/>
9871122
))}
9881123
</CardsColumn>
9891124
<CardsColumn>
9901125
{displayedClusters
9911126
.filter((_, index) => index % 2 === 1)
9921127
.map(cluster => (
993-
<ClusterCard key={cluster.cluster_id} cluster={cluster} />
1128+
<ClusterCard
1129+
key={cluster.cluster_id}
1130+
cluster={cluster}
1131+
filterByRegressed={filterByRegressed}
1132+
filterByEscalating={filterByEscalating}
1133+
/>
9941134
))}
9951135
</CardsColumn>
9961136
</CardsGrid>
@@ -1109,6 +1249,45 @@ const MoreProjectsCount = styled('span')`
11091249
margin-left: ${space(0.25)};
11101250
`;
11111251

1252+
// Status tags row for new/regressed/escalating indicators
1253+
const ClusterStatusTags = styled('div')`
1254+
display: flex;
1255+
flex-wrap: wrap;
1256+
gap: ${space(1)};
1257+
margin-top: ${space(1)};
1258+
`;
1259+
1260+
const StatusTag = styled('div')<{color: 'purple' | 'yellow' | 'red'}>`
1261+
display: inline-flex;
1262+
align-items: center;
1263+
gap: ${space(0.5)};
1264+
padding: ${space(0.5)} ${space(1)};
1265+
border-radius: ${p => p.theme.borderRadius};
1266+
font-size: ${p => p.theme.fontSize.xs};
1267+
1268+
${p => {
1269+
switch (p.color) {
1270+
case 'purple':
1271+
return `
1272+
background: ${p.theme.purple100};
1273+
color: ${p.theme.purple400};
1274+
`;
1275+
case 'yellow':
1276+
return `
1277+
background: ${p.theme.yellow100};
1278+
color: ${p.theme.yellow400};
1279+
`;
1280+
case 'red':
1281+
return `
1282+
background: ${p.theme.red100};
1283+
color: ${p.theme.red400};
1284+
`;
1285+
default:
1286+
return '';
1287+
}
1288+
}}
1289+
`;
1290+
11121291
// Tab section for Summary / Preview Issues
11131292
const TabSection = styled('div')``;
11141293

0 commit comments

Comments
 (0)