@@ -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 ] ) ;
@@ -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
11131292const TabSection = styled ( 'div' ) `` ;
11141293
0 commit comments