Skip to content

Commit 1a2ec01

Browse files
authored
feat(supergroups): Show filtered vs total events in supergroup chart (#112215)
When search filters are active, individual issue rows show matching events vs total events as separate series in the chart. Supergroup rows were only showing total stats. This aggregates filtered stats separately so the supergroup row renders the same dual-bar chart with matching/total event and user counts. Move stuff out of app/utils <img width="259" height="167" alt="image" src="https://github.com/user-attachments/assets/8a9939e6-e91d-4c5c-a1ec-3ddcee3c2747" />
1 parent 0b212a8 commit 1a2ec01

File tree

9 files changed

+267
-67
lines changed

9 files changed

+267
-67
lines changed

static/app/components/stream/supergroupRow.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {useState} from 'react';
22
import styled from '@emotion/styled';
33

44
import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
5+
import {Stack} from '@sentry/scraps/layout';
56
import {Text} from '@sentry/scraps/text';
67

78
import type {IndexedMembersByProject} from 'sentry/actionCreators/members';
@@ -13,8 +14,8 @@ import {Placeholder} from 'sentry/components/placeholder';
1314
import {TimeSince} from 'sentry/components/timeSince';
1415
import {IconStack} from 'sentry/icons';
1516
import {t} from 'sentry/locale';
16-
import type {AggregatedSupergroupStats} from 'sentry/utils/supergroup/aggregateSupergroupStats';
1717
import {COLUMN_BREAKPOINTS} from 'sentry/views/issueList/actions/utils';
18+
import type {AggregatedSupergroupStats} from 'sentry/views/issueList/supergroups/aggregateSupergroupStats';
1819
import {SupergroupDetailDrawer} from 'sentry/views/issueList/supergroups/supergroupDrawer';
1920
import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
2021

@@ -106,23 +107,47 @@ export function SupergroupRow({
106107

107108
<ChartColumn>
108109
{aggregatedStats?.mergedStats && aggregatedStats.mergedStats.length > 0 ? (
109-
<GroupStatusChart hideZeros stats={aggregatedStats.mergedStats} showMarkLine />
110+
<GroupStatusChart
111+
hideZeros
112+
stats={aggregatedStats.mergedFilteredStats ?? aggregatedStats.mergedStats}
113+
secondaryStats={
114+
aggregatedStats.mergedFilteredStats
115+
? aggregatedStats.mergedStats
116+
: undefined
117+
}
118+
showSecondaryPoints={aggregatedStats.mergedFilteredStats !== null}
119+
showMarkLine
120+
/>
110121
) : (
111122
<Placeholder height="36px" />
112123
)}
113124
</ChartColumn>
114125

115126
<EventsColumn>
116127
{aggregatedStats ? (
117-
<PrimaryCount value={aggregatedStats.eventCount} />
128+
<Stack position="relative">
129+
<PrimaryCount
130+
value={aggregatedStats.filteredEventCount ?? aggregatedStats.eventCount}
131+
/>
132+
{aggregatedStats.filteredEventCount !== null && (
133+
<SecondaryCount value={aggregatedStats.eventCount} />
134+
)}
135+
</Stack>
118136
) : (
119137
<Placeholder height="18px" width="40px" />
120138
)}
121139
</EventsColumn>
122140

123141
<UsersColumn>
124142
{aggregatedStats ? (
125-
<PrimaryCount value={aggregatedStats.userCount} />
143+
<Stack position="relative">
144+
<PrimaryCount
145+
value={aggregatedStats.filteredUserCount ?? aggregatedStats.userCount}
146+
/>
147+
{aggregatedStats.filteredUserCount !== null && (
148+
<SecondaryCount value={aggregatedStats.userCount} />
149+
)}
150+
</Stack>
126151
) : (
127152
<Placeholder height="18px" width="40px" />
128153
)}
@@ -258,6 +283,14 @@ const PrimaryCount = styled(Count)`
258283
font-variant-numeric: tabular-nums;
259284
`;
260285

286+
const SecondaryCount = styled(Count)`
287+
font-size: ${p => p.theme.font.size.sm};
288+
display: flex;
289+
justify-content: flex-end;
290+
color: ${p => p.theme.tokens.content.secondary};
291+
font-variant-numeric: tabular-nums;
292+
`;
293+
261294
// Empty spacers to match StreamGroup column widths and keep alignment
262295
const PrioritySpacer = styled('div')`
263296
width: 64px;

static/app/utils/supergroup/aggregateSupergroupStats.ts

Lines changed: 0 additions & 58 deletions
This file was deleted.

static/app/views/issueList/groupListBody.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import {LoadingStreamGroup, StreamGroup} from 'sentry/components/stream/group';
99
import {SupergroupRow} from 'sentry/components/stream/supergroupRow';
1010
import {GroupStore} from 'sentry/stores/groupStore';
1111
import type {Group} from 'sentry/types/group';
12-
import {aggregateSupergroupStats} from 'sentry/utils/supergroup/aggregateSupergroupStats';
13-
import type {SupergroupLookup} from 'sentry/utils/supergroup/useSuperGroups';
1412
import {useApi} from 'sentry/utils/useApi';
1513
import {useMedia} from 'sentry/utils/useMedia';
1614
import {useOrganization} from 'sentry/utils/useOrganization';
1715
import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState';
16+
import {aggregateSupergroupStats} from 'sentry/views/issueList/supergroups/aggregateSupergroupStats';
1817
import type {SupergroupDetail} from 'sentry/views/issueList/supergroups/types';
18+
import type {SupergroupLookup} from 'sentry/views/issueList/supergroups/useSuperGroups';
1919
import type {IssueUpdateData} from 'sentry/views/issueList/types';
2020

2121
import {NoGroupsHandler} from './noGroupsHandler';

static/app/views/issueList/issueListTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import {t} from 'sentry/locale';
1010
import type {PageFilters} from 'sentry/types/core';
1111
import {DemoTourElement, DemoTourStep} from 'sentry/utils/demoMode/demoTours';
1212
import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
13-
import type {SupergroupLookup} from 'sentry/utils/supergroup/useSuperGroups';
1413
import {useLocation} from 'sentry/utils/useLocation';
1514
import {IssueListActions} from 'sentry/views/issueList/actions';
1615
import {GroupListBody} from 'sentry/views/issueList/groupListBody';
1716
import {IssueSelectionProvider} from 'sentry/views/issueList/issueSelectionContext';
1817
import {NewViewEmptyState} from 'sentry/views/issueList/newViewEmptyState';
18+
import type {SupergroupLookup} from 'sentry/views/issueList/supergroups/useSuperGroups';
1919
import type {IssueUpdateData} from 'sentry/views/issueList/types';
2020

2121
interface IssueListTableProps {

static/app/views/issueList/overview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import type {RequestError} from 'sentry/utils/requestError/requestError';
3838
import {useDisableRouteAnalytics} from 'sentry/utils/routeAnalytics/useDisableRouteAnalytics';
3939
import {useRouteAnalyticsEventNames} from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
4040
import {useRouteAnalyticsParams} from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
41-
import {useSuperGroups} from 'sentry/utils/supergroup/useSuperGroups';
4241
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
4342
import {useApi} from 'sentry/utils/useApi';
4443
import {useLocation} from 'sentry/utils/useLocation';
@@ -49,6 +48,7 @@ import {usePrevious} from 'sentry/utils/usePrevious';
4948
import {IssueListTable} from 'sentry/views/issueList/issueListTable';
5049
import {IssuesDataConsentBanner} from 'sentry/views/issueList/issuesDataConsentBanner';
5150
import {IssueViewsHeader} from 'sentry/views/issueList/issueViewsHeader';
51+
import {useSuperGroups} from 'sentry/views/issueList/supergroups/useSuperGroups';
5252
import type {IssueUpdateData} from 'sentry/views/issueList/types';
5353
import {parseIssuePrioritySearch} from 'sentry/views/issueList/utils/parseIssuePrioritySearch';
5454
import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature';
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {GroupFixture} from 'sentry-fixture/group';
2+
3+
import {aggregateSupergroupStats} from './aggregateSupergroupStats';
4+
5+
describe('aggregateSupergroupStats', () => {
6+
it('returns null for empty groups', () => {
7+
expect(aggregateSupergroupStats([], '24h')).toBeNull();
8+
});
9+
10+
it('sums event and user counts', () => {
11+
const groups = [
12+
GroupFixture({count: '10', userCount: 3}),
13+
GroupFixture({count: '20', userCount: 7}),
14+
];
15+
const result = aggregateSupergroupStats(groups, '24h');
16+
expect(result?.eventCount).toBe(30);
17+
expect(result?.userCount).toBe(10);
18+
});
19+
20+
it('takes min firstSeen and max lastSeen', () => {
21+
const groups = [
22+
GroupFixture({firstSeen: '2024-01-05T00:00:00Z', lastSeen: '2024-01-10T00:00:00Z'}),
23+
GroupFixture({firstSeen: '2024-01-01T00:00:00Z', lastSeen: '2024-01-15T00:00:00Z'}),
24+
];
25+
const result = aggregateSupergroupStats(groups, '24h');
26+
expect(result?.firstSeen).toBe('2024-01-01T00:00:00Z');
27+
expect(result?.lastSeen).toBe('2024-01-15T00:00:00Z');
28+
});
29+
30+
it('point-wise sums stats timeseries', () => {
31+
const groups = [
32+
GroupFixture({
33+
stats: {
34+
'24h': [
35+
[1000, 1],
36+
[2000, 2],
37+
],
38+
},
39+
}),
40+
GroupFixture({
41+
stats: {
42+
'24h': [
43+
[1000, 3],
44+
[2000, 4],
45+
],
46+
},
47+
}),
48+
];
49+
const result = aggregateSupergroupStats(groups, '24h');
50+
expect(result?.mergedStats).toEqual([
51+
[1000, 4],
52+
[2000, 6],
53+
]);
54+
});
55+
56+
it('returns null filtered fields when no groups have filters', () => {
57+
const groups = [GroupFixture({filtered: null})];
58+
const result = aggregateSupergroupStats(groups, '24h');
59+
expect(result?.filteredEventCount).toBeNull();
60+
expect(result?.filteredUserCount).toBeNull();
61+
expect(result?.mergedFilteredStats).toBeNull();
62+
});
63+
64+
it('aggregates filtered stats separately', () => {
65+
const groups = [
66+
GroupFixture({
67+
count: '100',
68+
userCount: 50,
69+
stats: {
70+
'24h': [
71+
[1000, 10],
72+
[2000, 20],
73+
],
74+
},
75+
filtered: {
76+
count: '30',
77+
userCount: 15,
78+
firstSeen: '2024-01-01T00:00:00Z',
79+
lastSeen: '2024-01-10T00:00:00Z',
80+
stats: {
81+
'24h': [
82+
[1000, 3],
83+
[2000, 5],
84+
],
85+
},
86+
},
87+
}),
88+
GroupFixture({
89+
count: '200',
90+
userCount: 80,
91+
stats: {
92+
'24h': [
93+
[1000, 40],
94+
[2000, 60],
95+
],
96+
},
97+
filtered: {
98+
count: '70',
99+
userCount: 25,
100+
firstSeen: '2024-01-02T00:00:00Z',
101+
lastSeen: '2024-01-12T00:00:00Z',
102+
stats: {
103+
'24h': [
104+
[1000, 7],
105+
[2000, 15],
106+
],
107+
},
108+
},
109+
}),
110+
];
111+
112+
const result = aggregateSupergroupStats(groups, '24h');
113+
114+
// Total stats
115+
expect(result?.eventCount).toBe(300);
116+
expect(result?.userCount).toBe(130);
117+
expect(result?.mergedStats).toEqual([
118+
[1000, 50],
119+
[2000, 80],
120+
]);
121+
122+
// Filtered stats
123+
expect(result?.filteredEventCount).toBe(100);
124+
expect(result?.filteredUserCount).toBe(40);
125+
expect(result?.mergedFilteredStats).toEqual([
126+
[1000, 10],
127+
[2000, 20],
128+
]);
129+
});
130+
});

0 commit comments

Comments
 (0)