@@ -27,6 +27,7 @@ import Redirect from 'sentry/components/redirect';
2727import TimeSince from 'sentry/components/timeSince' ;
2828import { ALL_ACCESS_PROJECTS } from 'sentry/constants/pageFilters' ;
2929import {
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
189192interface 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
10741200const TabSection = styled ( 'div' ) `` ;
10751201
0 commit comments