Skip to content

Commit fd41cfa

Browse files
committed
feat: cluster status tags
1 parent 77b8c91 commit fd41cfa

File tree

1 file changed

+126
-0
lines changed

1 file changed

+126
-0
lines changed

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

Lines changed: 126 additions & 0 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';
@@ -188,8 +191,11 @@ function CompactIssuePreview({group}: {group: Group}) {
188191

189192
interface ClusterStats {
190193
firstSeen: string | null;
194+
hasRegressedIssues: boolean;
195+
isEscalating: boolean;
191196
isPending: boolean;
192197
lastSeen: string | null;
198+
newIssuesCount: number;
193199
totalEvents: number;
194200
totalUsers: number;
195201
}
@@ -220,6 +226,9 @@ function useClusterStats(groupIds: number[]): ClusterStats {
220226
totalUsers: 0,
221227
firstSeen: null,
222228
lastSeen: null,
229+
newIssuesCount: 0,
230+
hasRegressedIssues: false,
231+
isEscalating: false,
223232
isPending,
224233
};
225234
}
@@ -229,6 +238,19 @@ function useClusterStats(groupIds: number[]): ClusterStats {
229238
let earliestFirstSeen: Date | null = null;
230239
let latestLastSeen: Date | null = null;
231240

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

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

296+
// Determine if escalating: second half has >1.5x events compared to first half
297+
// Only consider escalating if there were events in the first half (avoid division by zero)
298+
const isEscalating = firstHalfEvents > 0 && secondHalfEvents > firstHalfEvents * 1.5;
299+
251300
return {
252301
totalEvents,
253302
totalUsers,
254303
firstSeen: earliestFirstSeen?.toISOString() ?? null,
255304
lastSeen: latestLastSeen?.toISOString() ?? null,
305+
newIssuesCount,
306+
hasRegressedIssues,
307+
isEscalating,
256308
isPending,
257309
};
258310
}, [groups, isPending]);
@@ -464,6 +516,41 @@ function ClusterCard({cluster}: {cluster: ClusterSummary}) {
464516
</StatItem>
465517
)}
466518
</ClusterStats>
519+
{!clusterStats.isPending &&
520+
(clusterStats.newIssuesCount > 0 ||
521+
clusterStats.hasRegressedIssues ||
522+
clusterStats.isEscalating) && (
523+
<ClusterStatusTags>
524+
{clusterStats.newIssuesCount > 0 && (
525+
<StatusTag color="purple">
526+
<IconStar size="xs" />
527+
<Text size="xs" bold>
528+
{tn(
529+
'%s new issue this week',
530+
'%s new issues this week',
531+
clusterStats.newIssuesCount
532+
)}
533+
</Text>
534+
</StatusTag>
535+
)}
536+
{clusterStats.hasRegressedIssues && (
537+
<StatusTag color="yellow">
538+
<IconRefresh size="xs" />
539+
<Text size="xs" bold>
540+
{t('Has regressed issues')}
541+
</Text>
542+
</StatusTag>
543+
)}
544+
{clusterStats.isEscalating && (
545+
<StatusTag color="red">
546+
<IconArrow direction="up" size="xs" />
547+
<Text size="xs" bold>
548+
{t('Escalating')}
549+
</Text>
550+
</StatusTag>
551+
)}
552+
</ClusterStatusTags>
553+
)}
467554
</CardHeader>
468555

469556
<TabSection>
@@ -1070,6 +1157,45 @@ const StatItem = styled('div')`
10701157
gap: ${space(0.5)};
10711158
`;
10721159

1160+
// Status tags row for new/regressed/escalating indicators
1161+
const ClusterStatusTags = styled('div')`
1162+
display: flex;
1163+
flex-wrap: wrap;
1164+
gap: ${space(1)};
1165+
margin-top: ${space(1)};
1166+
`;
1167+
1168+
const StatusTag = styled('div')<{color: 'purple' | 'yellow' | 'red'}>`
1169+
display: inline-flex;
1170+
align-items: center;
1171+
gap: ${space(0.5)};
1172+
padding: ${space(0.5)} ${space(1)};
1173+
border-radius: ${p => p.theme.borderRadius};
1174+
font-size: ${p => p.theme.fontSize.xs};
1175+
1176+
${p => {
1177+
switch (p.color) {
1178+
case 'purple':
1179+
return `
1180+
background: ${p.theme.purple100};
1181+
color: ${p.theme.purple400};
1182+
`;
1183+
case 'yellow':
1184+
return `
1185+
background: ${p.theme.yellow100};
1186+
color: ${p.theme.yellow400};
1187+
`;
1188+
case 'red':
1189+
return `
1190+
background: ${p.theme.red100};
1191+
color: ${p.theme.red400};
1192+
`;
1193+
default:
1194+
return '';
1195+
}
1196+
}}
1197+
`;
1198+
10731199
// Tab section for Summary / Preview Issues
10741200
const TabSection = styled('div')``;
10751201

0 commit comments

Comments
 (0)