Skip to content

Commit b461588

Browse files
committed
feat(replay): Share performance instrumentation with tracing
1 parent df08e8f commit b461588

File tree

30 files changed

+351
-330
lines changed

30 files changed

+351
-330
lines changed

packages/browser-integration-tests/suites/replay/eventBufferError/test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { envelopeRequestParser } from '../../../utils/helpers';
55
import {
66
getDecompressedRecordingEvents,
77
getReplaySnapshot,
8+
isCustomSnapshot,
89
isReplayEvent,
910
REPLAY_DEFAULT_FLUSH_MAX_DELAY,
1011
shouldSkipReplayTest,
@@ -41,8 +42,8 @@ sentryTest(
4142
// We only want to count replays here
4243
if (event && isReplayEvent(event)) {
4344
const events = getDecompressedRecordingEvents(route.request());
44-
// this makes sure we ignore e.g. mouse move events which can otherwise lead to flakes
45-
if (events.length > 0) {
45+
// Make sure to not count mouse moves or performance spans
46+
if (events.filter(event => !isCustomSnapshot(event) || event.data.tag !== 'performanceSpan').length > 0) {
4647
called++;
4748
}
4849
}

packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/template.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
<meta charset="utf-8" />
55
</head>
66
<body>
7-
<button id="button-add">Add items</button>
8-
<button id="button-modify">Modify items</button>
9-
<button id="button-remove">Remove items</button>
7+
<button id="noop" type="button">Noop</button>
8+
<button id="button-add" type="button">Add items</button>
9+
<button id="button-modify" type="button">Modify items</button>
10+
<button id="button-remove" type="button">Remove items</button>
1011
<ul class="list"></ul>
1112

1213
<script>

packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ sentryTest(
2121
const url = await getLocalTestPath({ testDir: __dirname });
2222

2323
const [res0] = await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]);
24+
// Ensure LCP is captured
25+
await Promise.all([waitForReplayRequest(page), page.click('#noop')]);
2426
await forceFlushReplay();
2527

2628
const [res1] = await Promise.all([waitForReplayRequest(page), page.click('#button-add')]);

packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/template.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
<meta charset="utf-8" />
55
</head>
66
<body>
7-
<button id="button-add">Add items</button>
8-
<button id="button-modify">Modify items</button>
9-
<button id="button-remove">Remove items</button>
7+
<button id="noop" type="button">Noop</button>
8+
<button id="button-add" type="button">Add items</button>
9+
<button id="button-modify" type="button">Modify items</button>
10+
<button id="button-remove" type="button">Remove items</button>
1011
<ul class="list"></ul>
1112

1213
<script>

packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,14 @@ sentryTest(
2323
});
2424
});
2525

26-
const reqPromise0 = waitForReplayRequest(page, 0);
27-
2826
const url = await getLocalTestPath({ testDir: __dirname });
2927

30-
const [res0] = await Promise.all([reqPromise0, page.goto(url)]);
28+
const [res0] = await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]);
29+
// Ensure LCP is captured
30+
await Promise.all([waitForReplayRequest(page), page.click('#noop')]);
3131
await forceFlushReplay();
3232

33-
const reqPromise1 = waitForReplayRequest(page);
34-
35-
const [res1] = await Promise.all([reqPromise1, page.click('#button-add')]);
33+
const [res1] = await Promise.all([waitForReplayRequest(page), page.click('#button-add')]);
3634
await forceFlushReplay();
3735

3836
// replay should be stopped due to mutation limit

packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-1-snap-incremental-chromium

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": 1,
77
"id": 9,
88
"x": 41.810001373291016,
9-
"y": 18.479999542236328
9+
"y": 18.75
1010
},
1111
"timestamp": [timestamp]
1212
},
@@ -26,7 +26,7 @@
2626
"type": 0,
2727
"id": 9,
2828
"x": 41.810001373291016,
29-
"y": 18.479999542236328
29+
"y": 18.75
3030
},
3131
"timestamp": [timestamp]
3232
},

packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-3-snap-incremental-chromium

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": 1,
77
"id": 9,
88
"x": 41.810001373291016,
9-
"y": 18.479999542236328
9+
"y": 18.75
1010
},
1111
"timestamp": [timestamp]
1212
},
@@ -26,7 +26,7 @@
2626
"type": 0,
2727
"id": 9,
2828
"x": 41.810001373291016,
29-
"y": 18.479999542236328
29+
"y": 18.75
3030
},
3131
"timestamp": [timestamp]
3232
},

packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-5-snap-incremental-chromium

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": 1,
77
"id": 12,
88
"x": 41.810001373291016,
9-
"y": 90.37000274658203
9+
"y": 90.62000274658203
1010
},
1111
"timestamp": [timestamp]
1212
},
@@ -26,7 +26,7 @@
2626
"type": 0,
2727
"id": 12,
2828
"x": 41.810001373291016,
29-
"y": 90.37000274658203
29+
"y": 90.62000274658203
3030
},
3131
"timestamp": [timestamp]
3232
},

packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-6-snap-incremental-chromium

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": 1,
77
"id": 15,
88
"x": 157.13999938964844,
9-
"y": 90.37000274658203
9+
"y": 90.62000274658203
1010
},
1111
"timestamp": [timestamp]
1212
},
@@ -35,7 +35,7 @@
3535
"type": 0,
3636
"id": 15,
3737
"x": 157.13999938964844,
38-
"y": 90.37000274658203
38+
"y": 90.62000274658203
3939
},
4040
"timestamp": [timestamp]
4141
},

packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-7-snap-incremental-chromium

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": 1,
77
"id": 12,
88
"x": 41.810001373291016,
9-
"y": 90.37000274658203
9+
"y": 90.62000274658203
1010
},
1111
"timestamp": [timestamp]
1212
},
@@ -35,7 +35,7 @@
3535
"type": 0,
3636
"id": 12,
3737
"x": 41.810001373291016,
38-
"y": 90.37000274658203
38+
"y": 90.62000274658203
3939
},
4040
"timestamp": [timestamp]
4141
},

packages/browser-integration-tests/suites/replay/multiple-pages/test.ts-snapshots/seg-9-snap-incremental-chromium

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"type": 1,
77
"id": 9,
88
"x": 41.810001373291016,
9-
"y": 18.479999542236328
9+
"y": 18.75
1010
},
1111
"timestamp": [timestamp]
1212
},
@@ -26,7 +26,7 @@
2626
"type": 0,
2727
"id": 9,
2828
"x": 41.810001373291016,
29-
"y": 18.479999542236328
29+
"y": 18.75
3030
},
3131
"timestamp": [timestamp]
3232
},

packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/template.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
<body>
77
<div id="content"></div>
88
<img src="https://example.com/path/to/image.png" />
9+
<button type="button">Test button</button>
910
</body>
1011
</html>

packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ sentryTest('should capture a LCP vital with element details.', async ({ browserN
1515
);
1616

1717
const url = await getLocalTestPath({ testDir: __dirname });
18-
await page.goto(url);
19-
20-
const eventData = await getFirstSentryEnvelopeRequest<Event>(page);
18+
const [eventData] = await Promise.all([
19+
getFirstSentryEnvelopeRequest<Event>(page),
20+
page.goto(url),
21+
page.click('button'),
22+
]);
2123

2224
expect(eventData.measurements).toBeDefined();
2325
expect(eventData.measurements?.lcp?.value).toBeDefined();

packages/browser-integration-tests/utils/replayHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ function isFullSnapshot(event: RecordingEvent): event is FullRecordingSnapshot {
153153
return event.type === EventType.FullSnapshot;
154154
}
155155

156-
function isCustomSnapshot(event: RecordingEvent): event is RecordingEvent & { data: CustomRecordingEvent } {
156+
export function isCustomSnapshot(event: RecordingEvent): event is RecordingEvent & { data: CustomRecordingEvent } {
157157
return event.type === EventType.Custom;
158158
}
159159

packages/replay/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
},
6060
"dependencies": {
6161
"@sentry/core": "7.74.1",
62+
"@sentry-internal/tracing": "7.74.1",
6263
"@sentry/types": "7.74.1",
6364
"@sentry/utils": "7.74.1"
6465
},
Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,37 @@
1-
import type { AllPerformanceEntry, ReplayContainer } from '../types';
2-
import { dedupePerformanceEntries } from '../util/dedupePerformanceEntries';
1+
import { addPerformanceInstrumentationHandler } from '@sentry-internal/tracing';
2+
3+
import type { ReplayContainer } from '../types';
34

45
/**
56
* Sets up a PerformanceObserver to listen to all performance entry types.
7+
* Returns a callback to stop observing.
68
*/
7-
export function setupPerformanceObserver(replay: ReplayContainer): PerformanceObserver {
8-
const performanceObserverHandler = (list: PerformanceObserverEntryList): void => {
9-
// For whatever reason the observer was returning duplicate navigation
10-
// entries (the other entry types were not duplicated).
11-
const newPerformanceEntries = dedupePerformanceEntries(
12-
replay.performanceEvents,
13-
list.getEntries() as AllPerformanceEntry[],
14-
);
15-
replay.performanceEvents = newPerformanceEntries;
16-
};
9+
export function setupPerformanceObserver(replay: ReplayContainer): () => void {
10+
function addPerformanceEntry(entry: PerformanceEntry): void {
11+
// It is possible for entries to come up multiple times
12+
if (!replay.performanceEvents.includes(entry)) {
13+
replay.performanceEvents.push(entry);
14+
}
15+
}
1716

18-
const performanceObserver = new PerformanceObserver(performanceObserverHandler);
17+
function onEntries({ entries }: { entries: PerformanceEntry[] }): void {
18+
entries.forEach(addPerformanceEntry);
19+
}
1920

20-
[
21-
'element',
22-
'event',
23-
'first-input',
24-
'largest-contentful-paint',
25-
'layout-shift',
26-
'longtask',
27-
'navigation',
28-
'paint',
29-
'resource',
30-
].forEach(type => {
31-
try {
32-
performanceObserver.observe({
33-
type,
34-
buffered: true,
35-
});
36-
} catch {
37-
// This can throw if an entry type is not supported in the browser.
38-
// Ignore these errors.
39-
}
21+
const clearCallbacks: (() => void)[] = [];
22+
23+
(['navigation', 'paint', 'resource'] as const).forEach(type => {
24+
clearCallbacks.push(addPerformanceInstrumentationHandler(type, onEntries));
4025
});
4126

42-
return performanceObserver;
27+
clearCallbacks.push(
28+
addPerformanceInstrumentationHandler('lcp', ({ metric }) => {
29+
replay.lcpMetric = metric;
30+
}),
31+
);
32+
33+
// A callback to cleanup all handlers
34+
return () => {
35+
clearCallbacks.forEach(clearCallback => clearCallback());
36+
};
4337
}

packages/replay/src/replay.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { addEvent, addEventSync } from './util/addEvent';
4242
import { addGlobalListeners } from './util/addGlobalListeners';
4343
import { addMemoryEntry } from './util/addMemoryEntry';
4444
import { createBreadcrumb } from './util/createBreadcrumb';
45-
import { createPerformanceEntries } from './util/createPerformanceEntries';
45+
import { createPerformanceEntries, getLargestContentfulPaint } from './util/createPerformanceEntries';
4646
import { createPerformanceSpans } from './util/createPerformanceSpans';
4747
import { debounce } from './util/debounce';
4848
import { getHandleRecordingEmit } from './util/handleRecordingEmit';
@@ -64,6 +64,8 @@ export class ReplayContainer implements ReplayContainerInterface {
6464
*/
6565
public performanceEvents: AllPerformanceEntry[];
6666

67+
public lcpMetric: { value: number; entries: PerformanceEntry[] } | undefined;
68+
6769
public session: Session | undefined;
6870

6971
public clickDetector: ClickDetector | undefined;
@@ -101,7 +103,7 @@ export class ReplayContainer implements ReplayContainerInterface {
101103

102104
private readonly _options: ReplayPluginOptions;
103105

104-
private _performanceObserver: PerformanceObserver | undefined;
106+
private _performanceCleanupCallback?: () => void;
105107

106108
private _debouncedFlush: ReturnType<typeof debounce>;
107109
private _flushLock: Promise<unknown> | undefined;
@@ -817,12 +819,7 @@ export class ReplayContainer implements ReplayContainerInterface {
817819
this._handleException(err);
818820
}
819821

820-
// PerformanceObserver //
821-
if (!('PerformanceObserver' in WINDOW)) {
822-
return;
823-
}
824-
825-
this._performanceObserver = setupPerformanceObserver(this);
822+
this._performanceCleanupCallback = setupPerformanceObserver(this);
826823
}
827824

828825
/**
@@ -840,9 +837,8 @@ export class ReplayContainer implements ReplayContainerInterface {
840837
this.clickDetector.removeListeners();
841838
}
842839

843-
if (this._performanceObserver) {
844-
this._performanceObserver.disconnect();
845-
this._performanceObserver = undefined;
840+
if (this._performanceCleanupCallback) {
841+
this._performanceCleanupCallback();
846842
}
847843
} catch (err) {
848844
this._handleException(err);
@@ -999,7 +995,14 @@ export class ReplayContainer implements ReplayContainerInterface {
999995
const entries = [...this.performanceEvents];
1000996
this.performanceEvents = [];
1001997

1002-
return Promise.all(createPerformanceSpans(this, createPerformanceEntries(entries)));
998+
const performanceEntries = createPerformanceEntries(entries);
999+
1000+
if (this.lcpMetric) {
1001+
performanceEntries.push(getLargestContentfulPaint(this.lcpMetric));
1002+
this.lcpMetric = undefined;
1003+
}
1004+
1005+
return Promise.all(createPerformanceSpans(this, performanceEntries));
10031006
}
10041007

10051008
/**

packages/replay/src/types/replay.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ export interface ReplayContainer {
442442
eventBuffer: EventBuffer | null;
443443
clickDetector: ReplayClickDetector | undefined;
444444
performanceEvents: AllPerformanceEntry[];
445+
lcpMetric?: { value: number; entries: PerformanceEntry[] };
445446
session: Session | undefined;
446447
recordingMode: ReplayRecordingMode;
447448
timeouts: Timeouts;

0 commit comments

Comments
 (0)