Skip to content

Commit d48a008

Browse files
authored
ref(groupstats): decouple groupStats from GroupStore (#60259)
Decoupling stats from groups and GroupStore allows us to optimize the react tree rerender and avoid large blocking updates (as we no longer create a fresh group reference for each group). We also move the stats state to use react query, which means we can leverage caching on pagination and navigation loading. ~I will open a followup pr to remove stats from the group type and finalize the decoupling~ <- should never happen as some places load stats together with the group request
1 parent 68a1f39 commit d48a008

File tree

8 files changed

+219
-156
lines changed

8 files changed

+219
-156
lines changed

static/app/components/eventOrGroupExtraDetails.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {Group, Organization} from 'sentry/types';
1818
import {Event} from 'sentry/types/event';
1919
import {projectCanLinkToReplay} from 'sentry/utils/replays/projectSupportsReplay';
2020
import withOrganization from 'sentry/utils/withOrganization';
21+
import {useGroupStats} from 'sentry/views/issueList/groupStatsProvider';
2122

2223
type Props = {
2324
data: Event | Group;
@@ -43,13 +44,13 @@ function EventOrGroupExtraDetails({
4344
annotations,
4445
shortId,
4546
project,
46-
lifetime,
47-
isUnhandled,
4847
inbox,
4948
status,
5049
substatus,
5150
} = data as Group;
5251

52+
const {lifetime, isUnhandled} = useGroupStats(data as Group);
53+
5354
const issuesPath = `/organizations/${organization.slug}/issues/`;
5455

5556
const showReplayCount =

static/app/components/stream/group.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
4343
import usePageFilters from 'sentry/utils/usePageFilters';
4444
import withOrganization from 'sentry/utils/withOrganization';
4545
import {TimePeriodType} from 'sentry/views/alerts/rules/metric/details/constants';
46+
import {useGroupStats} from 'sentry/views/issueList/groupStatsProvider';
4647
import {
4748
DISCOVER_EXCLUSION_FIELDS,
4849
getTabs,
@@ -246,7 +247,8 @@ function BaseGroupRow({
246247
};
247248

248249
const renderReprocessingColumns = () => {
249-
const {statusDetails, count} = group as GroupReprocessing;
250+
const {count} = stats;
251+
const {statusDetails} = group as GroupReprocessing;
250252
const {info, pendingEvents} = statusDetails;
251253

252254
if (!info) {
@@ -284,18 +286,20 @@ function BaseGroupRow({
284286
);
285287
};
286288

289+
const stats = useGroupStats(group);
290+
287291
// Use data.filtered to decide on which value to use
288292
// In case of the query has filters but we avoid showing both sets of filtered/unfiltered stats
289293
// we use useFilteredStats param passed to Group for deciding
290-
const primaryCount = group.filtered ? group.filtered.count : group.count;
291-
const secondaryCount = group.filtered ? group.count : undefined;
292-
const primaryUserCount = group.filtered ? group.filtered.userCount : group.userCount;
293-
const secondaryUserCount = group.filtered ? group.userCount : undefined;
294+
const primaryCount = stats.filtered ? stats.filtered.count : stats.count;
295+
const secondaryCount = stats.filtered ? stats.count : undefined;
296+
const primaryUserCount = stats.filtered ? stats.filtered.userCount : stats.userCount;
297+
const secondaryUserCount = stats.filtered ? stats.userCount : undefined;
294298
// preview stats
295-
const lastTriggeredDate = group.lastTriggered;
299+
const lastTriggeredDate = stats.lastTriggered;
296300

297301
const showSecondaryPoints = Boolean(
298-
withChart && group && group.filtered && statsPeriod && useFilteredStats
302+
withChart && group && stats.filtered && statsPeriod && useFilteredStats
299303
);
300304

301305
const groupCategoryCountTitles: Record<IssueCategory, string> = {
@@ -315,24 +319,24 @@ function BaseGroupRow({
315319
title={
316320
<CountTooltipContent>
317321
<h4>{groupCategoryCountTitles[group.issueCategory]}</h4>
318-
{group.filtered && (
322+
{stats.filtered && (
319323
<Fragment>
320324
<div>{queryFilterDescription ?? t('Matching filters')}</div>
321325
<Link to={getDiscoverUrl(true)}>
322-
<Count value={group.filtered?.count} />
326+
<Count value={stats.filtered?.count} />
323327
</Link>
324328
</Fragment>
325329
)}
326330
<Fragment>
327331
<div>{t('Total in %s', summary)}</div>
328332
<Link to={getDiscoverUrl()}>
329-
<Count value={group.count} />
333+
<Count value={stats.count} />
330334
</Link>
331335
</Fragment>
332-
{group.lifetime && (
336+
{stats.lifetime && (
333337
<Fragment>
334338
<div>{t('Since issue began')}</div>
335-
<Count value={group.lifetime.count} />
339+
<Count value={stats.lifetime.count} />
336340
</Fragment>
337341
)}
338342
</CountTooltipContent>
@@ -355,24 +359,24 @@ function BaseGroupRow({
355359
title={
356360
<CountTooltipContent>
357361
<h4>{t('Affected Users')}</h4>
358-
{group.filtered && (
362+
{stats.filtered && (
359363
<Fragment>
360364
<div>{queryFilterDescription ?? t('Matching filters')}</div>
361365
<Link to={getDiscoverUrl(true)}>
362-
<Count value={group.filtered?.userCount} />
366+
<Count value={stats.filtered?.userCount} />
363367
</Link>
364368
</Fragment>
365369
)}
366370
<Fragment>
367371
<div>{t('Total in %s', summary)}</div>
368372
<Link to={getDiscoverUrl()}>
369-
<Count value={group.userCount} />
373+
<Count value={stats.userCount} />
370374
</Link>
371375
</Fragment>
372-
{group.lifetime && (
376+
{stats.lifetime && (
373377
<Fragment>
374378
<div>{t('Since issue began')}</div>
375-
<Count value={group.lifetime.userCount} />
379+
<Count value={stats.lifetime.userCount} />
376380
</Fragment>
377381
)}
378382
</CountTooltipContent>

static/app/components/stream/groupChart.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {Group, TimeseriesValue} from 'sentry/types';
88
import {Series} from 'sentry/types/echarts';
99
import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
1010
import theme from 'sentry/utils/theme';
11+
import {useGroupStats} from 'sentry/views/issueList/groupStatsProvider';
1112

1213
function asChartPoint(point: [number, number]): {name: number | string; value: number} {
1314
return {
@@ -33,14 +34,15 @@ function GroupChart({
3334
height = 24,
3435
showMarkLine = false,
3536
}: Props) {
37+
const groupStats = useGroupStats(data);
3638
const stats: ReadonlyArray<TimeseriesValue> = statsPeriod
37-
? data.filtered
38-
? data.filtered.stats?.[statsPeriod]
39-
: data.stats?.[statsPeriod]
39+
? groupStats.filtered
40+
? groupStats.filtered.stats?.[statsPeriod]
41+
: groupStats.stats?.[statsPeriod]
4042
: EMPTY_STATS;
4143

4244
const secondaryStats: TimeseriesValue[] | null =
43-
statsPeriod && data.filtered ? data.stats[statsPeriod] : null;
45+
statsPeriod && groupStats.filtered ? groupStats.stats[statsPeriod] : null;
4446

4547
const [series, colors, emphasisColors]: [
4648
Series[],

static/app/stores/groupStore.spec.tsx

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Project} from 'sentry-fixture/project';
22

33
import GroupStore from 'sentry/stores/groupStore';
4-
import {Group, GroupActivityType, GroupStats, TimeseriesValue} from 'sentry/types';
4+
import {Group, GroupActivityType} from 'sentry/types';
55

66
const MOCK_PROJECT = TestStubs.Project();
77

@@ -105,48 +105,6 @@ describe('GroupStore', function () {
105105
});
106106
});
107107

108-
describe('onPopulateStats()', function () {
109-
const stats: Record<string, TimeseriesValue[]> = {auto: [[1611576000, 10]]};
110-
111-
beforeEach(function () {
112-
jest.spyOn(GroupStore, 'trigger');
113-
GroupStore.items = [g('1'), g('2'), g('3')];
114-
});
115-
afterEach(() => {
116-
jest.restoreAllMocks();
117-
});
118-
119-
it('should merge stats into existing groups', function () {
120-
GroupStore.onPopulateStats(
121-
['1', '2', '3'],
122-
[
123-
{id: '1', stats} as GroupStats,
124-
{id: '2', stats} as GroupStats,
125-
{id: '3', stats} as GroupStats,
126-
]
127-
);
128-
129-
const group = GroupStore.getAllItems()[0] as Group;
130-
131-
expect(group.stats).toEqual(stats);
132-
expect(GroupStore.trigger).toHaveBeenCalledWith(new Set(['1', '2', '3']));
133-
});
134-
135-
it('should not change current item ids', function () {
136-
GroupStore.onPopulateStats(
137-
['2', '3'],
138-
[{id: '2', stats} as GroupStats, {id: '3', stats} as GroupStats]
139-
);
140-
141-
const group1 = GroupStore.getAllItems()[0] as Group;
142-
const group2 = GroupStore.getAllItems()[1] as Group;
143-
144-
expect(GroupStore.trigger).toHaveBeenCalledWith(new Set(['2', '3']));
145-
expect(group1.stats).not.toEqual(stats);
146-
expect(group2.stats).toEqual(stats);
147-
});
148-
});
149-
150108
describe('getAllItems()', function () {
151109
it('Merges pending changes into items', function () {
152110
GroupStore.items = [];

static/app/stores/groupStore.tsx

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {createStore} from 'reflux';
33
import {Indicator} from 'sentry/actionCreators/indicator';
44
import {t} from 'sentry/locale';
55
import IndicatorStore from 'sentry/stores/indicatorStore';
6-
import {Activity, BaseGroup, Group, GroupStats} from 'sentry/types';
6+
import {Activity, BaseGroup, Group} from 'sentry/types';
77
import RequestError from 'sentry/utils/requestError/requestError';
88
import toArray from 'sentry/utils/toArray';
99

@@ -70,8 +70,6 @@ interface GroupStoreDefinition extends CommonStoreDefinition<Item[]>, InternalDe
7070
onMergeError: (changeId: string, itemIds: ItemIds, response: any) => void;
7171
onMergeSuccess: (changeId: string, itemIds: ItemIds, response: any) => void;
7272

73-
onPopulateStats: (itemIds: ItemIds, response: GroupStats[]) => void;
74-
7573
onUpdate: (changeId: string, itemIds: ItemIds, data: any) => void;
7674
onUpdateError: (changeId: string, itemIds: ItemIds, silent: boolean) => void;
7775
onUpdateSuccess: (changeId: string, itemIds: ItemIds, response: Partial<Group>) => void;
@@ -459,24 +457,6 @@ const storeConfig: GroupStoreDefinition = {
459457
this.pendingChanges.delete(changeId);
460458
this.updateItems(ids);
461459
},
462-
463-
onPopulateStats(itemIds, response) {
464-
// Organize stats by id
465-
const groupStatsMap = response.reduce<Record<string, GroupStats>>(
466-
(map, stats) => ({...map, [stats.id]: stats}),
467-
{}
468-
);
469-
470-
this.items.forEach((item, idx) => {
471-
if (itemIds?.includes(item.id)) {
472-
this.items[idx] = {
473-
...item,
474-
...groupStatsMap[item.id],
475-
};
476-
}
477-
});
478-
this.updateItems(itemIds);
479-
},
480460
};
481461

482462
const GroupStore = createStore(storeConfig);

static/app/utils/parseApiError.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {ResponseMeta} from 'sentry/api';
2+
import RequestError from 'sentry/utils/requestError/requestError';
23

3-
export default function parseApiError(resp: ResponseMeta): string {
4+
export default function parseApiError(resp: ResponseMeta | RequestError): string {
45
const {detail} = (resp && resp.responseJSON) || ({} as object);
56

67
// return immediately if string

0 commit comments

Comments
 (0)