Skip to content

Commit f76319b

Browse files
authored
fix(replay): Remove duplicate nav events from the Replay>Breadcrumbs list (#69057)
Replay frames include SpanFrame and CrumbFrames, which each have the similar concept of 'navigation', but each with some different properties: ![SCR-20240416-mtyp](https://github.com/getsentry/sentry/assets/187460/b94f3f14-73d1-4c80-a35a-314f6eee8558) The trouble is that these two things have the same or similar timestamps, so they render right next to each other, and mostly look the same (except that spans can have more unique titles...): **Before:** ![before](https://github.com/getsentry/sentry/assets/187460/0820aa35-e766-4902-a19a-488e464b6f14) **After:** ![after](https://github.com/getsentry/sentry/assets/187460/8a948e62-af31-4891-a0a1-94e1d22cfda6) This change de-duplicates these breadcrumb types, so in most cases you'll only see one "Navigation" row.
1 parent a909e82 commit f76319b

File tree

2 files changed

+47
-16
lines changed

2 files changed

+47
-16
lines changed

static/app/utils/replays/replayReader.spec.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,7 @@ describe('ReplayReader', () => {
200200
expected: [
201201
expect.objectContaining({category: 'replay.init'}),
202202
expect.objectContaining({category: 'ui.slowClickDetected'}),
203-
expect.objectContaining({category: 'navigation'}),
204-
expect.objectContaining({op: 'navigation.navigate'}),
203+
expect.objectContaining({op: 'navigation.navigate'}), // prefer the nav span over the breadcrumb
205204
expect.objectContaining({category: 'ui.click'}),
206205
expect.objectContaining({category: 'ui.click'}),
207206
],

static/app/utils/replays/replayReader.tsx

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,36 @@ function removeDuplicateClicks(frames: BreadcrumbFrame[]) {
103103
return uniqueClickFrames.concat(otherFrames).concat(slowClickFrames);
104104
}
105105

106+
// If a `navigation` crumb and `navigation.*` span happen within this timeframe,
107+
// we'll consider them duplicates.
108+
const DUPLICATE_NAV_THRESHOLD_MS = 2;
109+
110+
/**
111+
* Return a list of BreadcrumbFrames, where any navigation crumb is removed if
112+
* there is a matching navigation.* span to replace it.
113+
*
114+
* SpanFrame is preferred because they render with more specific titles.
115+
*/
116+
function removeDuplicateNavCrumbs(
117+
crumbFrames: BreadcrumbFrame[],
118+
spanFrames: SpanFrame[]
119+
) {
120+
const navCrumbs = crumbFrames.filter(crumb => crumb.category === 'navigation');
121+
const otherBreadcrumbFrames = crumbFrames.filter(
122+
crumb => crumb.category !== 'navigation'
123+
);
124+
125+
const navSpans = spanFrames.filter(span => span.op.startsWith('navigation.'));
126+
127+
const uniqueNavCrumbs = navCrumbs.filter(
128+
crumb =>
129+
!navSpans.some(
130+
span => Math.abs(crumb.offsetMs - span.offsetMs) <= DUPLICATE_NAV_THRESHOLD_MS
131+
)
132+
);
133+
return otherBreadcrumbFrames.concat(uniqueNavCrumbs);
134+
}
135+
106136
export default class ReplayReader {
107137
static factory({attachments, errors, replayRecord, clipWindow}: ReplayReaderParams) {
108138
if (!attachments || !replayRecord || !errors) {
@@ -446,20 +476,22 @@ export default class ReplayReader {
446476
)
447477
);
448478

449-
getPerfFrames = memoize(() =>
450-
[
451-
...removeDuplicateClicks(
452-
this._sortedBreadcrumbFrames.filter(
453-
frame =>
454-
['navigation', 'ui.click'].includes(frame.category) ||
455-
(frame.category === 'ui.slowClickDetected' &&
456-
(isDeadClick(frame as SlowClickFrame) ||
457-
isDeadRageClick(frame as SlowClickFrame)))
458-
)
459-
),
460-
...this._sortedSpanFrames.filter(frame => frame.op.startsWith('navigation.')),
461-
].sort(sortFrames)
462-
);
479+
getPerfFrames = memoize(() => {
480+
const crumbs = removeDuplicateClicks(
481+
this._sortedBreadcrumbFrames.filter(
482+
frame =>
483+
['navigation', 'ui.click'].includes(frame.category) ||
484+
(frame.category === 'ui.slowClickDetected' &&
485+
(isDeadClick(frame as SlowClickFrame) ||
486+
isDeadRageClick(frame as SlowClickFrame)))
487+
)
488+
);
489+
const spans = this._sortedSpanFrames.filter(frame =>
490+
frame.op.startsWith('navigation.')
491+
);
492+
const uniqueCrumbs = removeDuplicateNavCrumbs(crumbs, spans);
493+
return [...uniqueCrumbs, ...spans].sort(sortFrames);
494+
});
463495

464496
getLPCFrames = memoize(() => this._sortedSpanFrames.filter(isLCPFrame));
465497

0 commit comments

Comments
 (0)