@@ -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' ;
@@ -190,8 +193,11 @@ function CompactIssuePreview({group}: {group: Group}) {
190193
191194interface 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 ] ) ;
@@ -473,6 +525,41 @@ function ClusterCard({cluster}: {cluster: ClusterSummary}) {
473525 </ StatItem >
474526 ) }
475527 </ ClusterStats >
528+ { ! clusterStats . isPending &&
529+ ( clusterStats . newIssuesCount > 0 ||
530+ clusterStats . hasRegressedIssues ||
531+ clusterStats . isEscalating ) && (
532+ < ClusterStatusTags >
533+ { clusterStats . newIssuesCount > 0 && (
534+ < StatusTag color = "purple" >
535+ < IconStar size = "xs" />
536+ < Text size = "xs" bold >
537+ { tn (
538+ '%s new issue this week' ,
539+ '%s new issues this week' ,
540+ clusterStats . newIssuesCount
541+ ) }
542+ </ Text >
543+ </ StatusTag >
544+ ) }
545+ { clusterStats . hasRegressedIssues && (
546+ < StatusTag color = "yellow" >
547+ < IconRefresh size = "xs" />
548+ < Text size = "xs" bold >
549+ { t ( 'Has regressed issues' ) }
550+ </ Text >
551+ </ StatusTag >
552+ ) }
553+ { clusterStats . isEscalating && (
554+ < StatusTag color = "red" >
555+ < IconArrow direction = "up" size = "xs" />
556+ < Text size = "xs" bold >
557+ { t ( 'Escalating' ) }
558+ </ Text >
559+ </ StatusTag >
560+ ) }
561+ </ ClusterStatusTags >
562+ ) }
476563 </ CardHeader >
477564
478565 < TabSection >
@@ -1109,6 +1196,45 @@ const MoreProjectsCount = styled('span')`
11091196 margin-left: ${ space ( 0.25 ) } ;
11101197` ;
11111198
1199+ // Status tags row for new/regressed/escalating indicators
1200+ const ClusterStatusTags = styled ( 'div' ) `
1201+ display: flex;
1202+ flex-wrap: wrap;
1203+ gap: ${ space ( 1 ) } ;
1204+ margin-top: ${ space ( 1 ) } ;
1205+ ` ;
1206+
1207+ const StatusTag = styled ( 'div' ) < { color : 'purple' | 'yellow' | 'red' } > `
1208+ display: inline-flex;
1209+ align-items: center;
1210+ gap: ${ space ( 0.5 ) } ;
1211+ padding: ${ space ( 0.5 ) } ${ space ( 1 ) } ;
1212+ border-radius: ${ p => p . theme . borderRadius } ;
1213+ font-size: ${ p => p . theme . fontSize . xs } ;
1214+
1215+ ${ p => {
1216+ switch ( p . color ) {
1217+ case 'purple' :
1218+ return `
1219+ background: ${ p . theme . purple100 } ;
1220+ color: ${ p . theme . purple400 } ;
1221+ ` ;
1222+ case 'yellow' :
1223+ return `
1224+ background: ${ p . theme . yellow100 } ;
1225+ color: ${ p . theme . yellow400 } ;
1226+ ` ;
1227+ case 'red' :
1228+ return `
1229+ background: ${ p . theme . red100 } ;
1230+ color: ${ p . theme . red400 } ;
1231+ ` ;
1232+ default :
1233+ return '' ;
1234+ }
1235+ } }
1236+ ` ;
1237+
11121238// Tab section for Summary / Preview Issues
11131239const TabSection = styled ( 'div' ) `` ;
11141240
0 commit comments