Skip to content

Commit df5c84a

Browse files
authored
feat(replay): Promote mutationBreadcrumbLimit and mutationLimit to regular feature (#8228)
Instead of taking a fullsnapshot when `mutationLimit` is reached, lets be aggressive and stop the replay to ensure end-users are not negatively affected performance wise. The default for showing a breadcrumb is at 750 mutations, and the default limit to stop recording is 10000 mutations. Closes #8002
1 parent 78454aa commit df5c84a

File tree

6 files changed

+48
-31
lines changed

6 files changed

+48
-31
lines changed

packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ window.Sentry = Sentry;
44
window.Replay = new Sentry.Replay({
55
flushMinDelay: 500,
66
flushMaxDelay: 500,
7-
_experiments: {
8-
mutationLimit: 250,
9-
},
7+
mutationLimit: 250,
108
});
119

1210
Sentry.init({

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

+19-21
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { expect } from '@playwright/test';
22

33
import { sentryTest } from '../../../../utils/fixtures';
4-
import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';
4+
import {
5+
getReplayRecordingContent,
6+
getReplaySnapshot,
7+
shouldSkipReplayTest,
8+
waitForReplayRequest,
9+
} from '../../../../utils/replayHelpers';
510

611
sentryTest(
7-
'handles large mutations with _experiments.mutationLimit configured',
12+
'handles large mutations by stopping replay when `mutationLimit` configured',
813
async ({ getLocalTestPath, page, forceFlushReplay, browserName }) => {
914
if (shouldSkipReplayTest() || ['webkit', 'firefox'].includes(browserName)) {
1015
sentryTest.skip();
@@ -34,36 +39,29 @@ sentryTest(
3439
await forceFlushReplay();
3540
const res1 = await reqPromise1;
3641

37-
const reqPromise2 = waitForReplayRequest(page);
42+
// replay should be stopped due to mutation limit
43+
let replay = await getReplaySnapshot(page);
44+
expect(replay.session).toBe(undefined);
45+
expect(replay._isEnabled).toBe(false);
3846

3947
void page.click('#button-modify');
4048
await forceFlushReplay();
41-
const res2 = await reqPromise2;
4249

43-
const reqPromise3 = waitForReplayRequest(page);
44-
45-
void page.click('#button-remove');
50+
await page.click('#button-remove');
4651
await forceFlushReplay();
47-
const res3 = await reqPromise3;
4852

4953
const replayData0 = getReplayRecordingContent(res0);
50-
const replayData1 = getReplayRecordingContent(res1);
51-
const replayData2 = getReplayRecordingContent(res2);
52-
const replayData3 = getReplayRecordingContent(res3);
53-
5454
expect(replayData0.fullSnapshots.length).toBe(1);
5555
expect(replayData0.incrementalSnapshots.length).toBe(0);
5656

57-
// This includes both a full snapshot as well as some incremental snapshots
58-
expect(replayData1.fullSnapshots.length).toBe(1);
57+
// Breadcrumbs (click and mutation);
58+
const replayData1 = getReplayRecordingContent(res1);
59+
expect(replayData1.fullSnapshots.length).toBe(0);
5960
expect(replayData1.incrementalSnapshots.length).toBeGreaterThan(0);
61+
expect(replayData1.breadcrumbs.map(({ category }) => category).sort()).toEqual(['replay.mutations', 'ui.click']);
6062

61-
// This does not trigger mutations, for whatever reason - so no full snapshot either!
62-
expect(replayData2.fullSnapshots.length).toBe(0);
63-
expect(replayData2.incrementalSnapshots.length).toBeGreaterThan(0);
64-
65-
// This includes both a full snapshot as well as some incremental snapshots
66-
expect(replayData3.fullSnapshots.length).toBe(1);
67-
expect(replayData3.incrementalSnapshots.length).toBeGreaterThan(0);
63+
replay = await getReplaySnapshot(page);
64+
expect(replay.session).toBe(undefined);
65+
expect(replay._isEnabled).toBe(false);
6866
},
6967
);

packages/replay/src/integration.ts

+5
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export class Replay implements Integration {
6060
maskAllInputs = true,
6161
blockAllMedia = true,
6262

63+
mutationBreadcrumbLimit = 750,
64+
mutationLimit = 10_000,
65+
6366
networkDetailAllowUrls = [],
6467
networkCaptureBodies = true,
6568
networkRequestHeaders = [],
@@ -127,6 +130,8 @@ export class Replay implements Integration {
127130
blockAllMedia,
128131
maskAllInputs,
129132
maskAllText,
133+
mutationBreadcrumbLimit,
134+
mutationLimit,
130135
networkDetailAllowUrls,
131136
networkCaptureBodies,
132137
networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders),

packages/replay/src/replay.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1056,8 +1056,8 @@ export class ReplayContainer implements ReplayContainerInterface {
10561056
private _onMutationHandler = (mutations: unknown[]): boolean => {
10571057
const count = mutations.length;
10581058

1059-
const mutationLimit = this._options._experiments.mutationLimit || 0;
1060-
const mutationBreadcrumbLimit = this._options._experiments.mutationBreadcrumbLimit || 1000;
1059+
const mutationLimit = this._options.mutationLimit;
1060+
const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit;
10611061
const overMutationLimit = mutationLimit && count > mutationLimit;
10621062

10631063
// Create a breadcrumb if a lot of mutations happen at the same time
@@ -1067,15 +1067,15 @@ export class ReplayContainer implements ReplayContainerInterface {
10671067
category: 'replay.mutations',
10681068
data: {
10691069
count,
1070+
limit: overMutationLimit,
10701071
},
10711072
});
10721073
this._createCustomBreadcrumb(breadcrumb);
10731074
}
10741075

1076+
// Stop replay if over the mutation limit
10751077
if (overMutationLimit) {
1076-
// We want to skip doing an incremental snapshot if there are too many mutations
1077-
// Instead, we do a full snapshot
1078-
this._triggerFullSnapshot(false);
1078+
void this.stop('mutationLimit');
10791079
return false;
10801080
}
10811081

packages/replay/src/types.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,20 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
272272
*/
273273
maskAllText: boolean;
274274

275+
/**
276+
* A high number of DOM mutations (in a single event loop) can cause
277+
* performance regressions in end-users' browsers. This setting will create
278+
* a breadcrumb in the recording when the limit has been reached.
279+
*/
280+
mutationBreadcrumbLimit: number;
281+
282+
/**
283+
* A high number of DOM mutations (in a single event loop) can cause
284+
* performance regressions in end-users' browsers. This setting will cause
285+
* recording to stop when the limit has been reached.
286+
*/
287+
mutationLimit: number;
288+
275289
/**
276290
* Callback before adding a custom recording event
277291
*
@@ -295,8 +309,6 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
295309
_experiments: Partial<{
296310
captureExceptions: boolean;
297311
traceInternals: boolean;
298-
mutationLimit: number;
299-
mutationBreadcrumbLimit: number;
300312
slowClicks: {
301313
threshold: number;
302314
timeout: number;

packages/replay/test/utils/setupReplayContainer.ts

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const DEFAULT_OPTIONS = {
1515
networkCaptureBodies: true,
1616
networkRequestHeaders: [],
1717
networkResponseHeaders: [],
18+
mutationLimit: 1500,
19+
mutationBreadcrumbLimit: 500,
1820
_experiments: {},
1921
};
2022

@@ -54,6 +56,8 @@ export const DEFAULT_OPTIONS_EVENT_PAYLOAD = {
5456
maskAllText: false,
5557
maskAllInputs: false,
5658
useCompression: DEFAULT_OPTIONS.useCompression,
59+
mutationLimit: DEFAULT_OPTIONS.mutationLimit,
60+
mutationBreadcrumbLimit: DEFAULT_OPTIONS.mutationBreadcrumbLimit,
5761
networkDetailHasUrls: DEFAULT_OPTIONS.networkDetailAllowUrls.length > 0,
5862
networkCaptureBodies: DEFAULT_OPTIONS.networkCaptureBodies,
5963
networkRequestHeaders: DEFAULT_OPTIONS.networkRequestHeaders.length > 0,

0 commit comments

Comments
 (0)