diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js index 5c30f352959c..3aa2548f1522 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js +++ b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js @@ -4,9 +4,7 @@ window.Sentry = Sentry; window.Replay = new Sentry.Replay({ flushMinDelay: 500, flushMaxDelay: 500, - _experiments: { - mutationLimit: 250, - }, + mutationLimit: 250, }); Sentry.init({ diff --git a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts index 5d5dbb9d3f93..84f0113263d7 100644 --- a/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts +++ b/packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts @@ -1,10 +1,15 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers'; +import { + getReplayRecordingContent, + getReplaySnapshot, + shouldSkipReplayTest, + waitForReplayRequest, +} from '../../../../utils/replayHelpers'; sentryTest( - 'handles large mutations with _experiments.mutationLimit configured', + 'handles large mutations by stopping replay when `mutationLimit` configured', async ({ getLocalTestPath, page, forceFlushReplay, browserName }) => { if (shouldSkipReplayTest() || ['webkit', 'firefox'].includes(browserName)) { sentryTest.skip(); @@ -34,36 +39,29 @@ sentryTest( await forceFlushReplay(); const res1 = await reqPromise1; - const reqPromise2 = waitForReplayRequest(page); + // replay should be stopped due to mutation limit + let replay = await getReplaySnapshot(page); + expect(replay.session).toBe(undefined); + expect(replay._isEnabled).toBe(false); void page.click('#button-modify'); await forceFlushReplay(); - const res2 = await reqPromise2; - const reqPromise3 = waitForReplayRequest(page); - - void page.click('#button-remove'); + await page.click('#button-remove'); await forceFlushReplay(); - const res3 = await reqPromise3; const replayData0 = getReplayRecordingContent(res0); - const replayData1 = getReplayRecordingContent(res1); - const replayData2 = getReplayRecordingContent(res2); - const replayData3 = getReplayRecordingContent(res3); - expect(replayData0.fullSnapshots.length).toBe(1); expect(replayData0.incrementalSnapshots.length).toBe(0); - // This includes both a full snapshot as well as some incremental snapshots - expect(replayData1.fullSnapshots.length).toBe(1); + // Breadcrumbs (click and mutation); + const replayData1 = getReplayRecordingContent(res1); + expect(replayData1.fullSnapshots.length).toBe(0); expect(replayData1.incrementalSnapshots.length).toBeGreaterThan(0); + expect(replayData1.breadcrumbs.map(({ category }) => category).sort()).toEqual(['replay.mutations', 'ui.click']); - // This does not trigger mutations, for whatever reason - so no full snapshot either! - expect(replayData2.fullSnapshots.length).toBe(0); - expect(replayData2.incrementalSnapshots.length).toBeGreaterThan(0); - - // This includes both a full snapshot as well as some incremental snapshots - expect(replayData3.fullSnapshots.length).toBe(1); - expect(replayData3.incrementalSnapshots.length).toBeGreaterThan(0); + replay = await getReplaySnapshot(page); + expect(replay.session).toBe(undefined); + expect(replay._isEnabled).toBe(false); }, ); diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 114d015f2702..0a8813c14d38 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -60,6 +60,9 @@ export class Replay implements Integration { maskAllInputs = true, blockAllMedia = true, + mutationBreadcrumbLimit = 750, + mutationLimit = 10_000, + networkDetailAllowUrls = [], networkCaptureBodies = true, networkRequestHeaders = [], @@ -127,6 +130,8 @@ export class Replay implements Integration { blockAllMedia, maskAllInputs, maskAllText, + mutationBreadcrumbLimit, + mutationLimit, networkDetailAllowUrls, networkCaptureBodies, networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders), diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 53f263b5fa3f..5642b66b8b5a 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -1056,8 +1056,8 @@ export class ReplayContainer implements ReplayContainerInterface { private _onMutationHandler = (mutations: unknown[]): boolean => { const count = mutations.length; - const mutationLimit = this._options._experiments.mutationLimit || 0; - const mutationBreadcrumbLimit = this._options._experiments.mutationBreadcrumbLimit || 1000; + const mutationLimit = this._options.mutationLimit; + const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit; const overMutationLimit = mutationLimit && count > mutationLimit; // Create a breadcrumb if a lot of mutations happen at the same time @@ -1067,15 +1067,15 @@ export class ReplayContainer implements ReplayContainerInterface { category: 'replay.mutations', data: { count, + limit: overMutationLimit, }, }); this._createCustomBreadcrumb(breadcrumb); } + // Stop replay if over the mutation limit if (overMutationLimit) { - // We want to skip doing an incremental snapshot if there are too many mutations - // Instead, we do a full snapshot - this._triggerFullSnapshot(false); + void this.stop('mutationLimit'); return false; } diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index 3d1d8616bb79..393eed5802a8 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -272,6 +272,20 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { */ maskAllText: boolean; + /** + * A high number of DOM mutations (in a single event loop) can cause + * performance regressions in end-users' browsers. This setting will create + * a breadcrumb in the recording when the limit has been reached. + */ + mutationBreadcrumbLimit: number; + + /** + * A high number of DOM mutations (in a single event loop) can cause + * performance regressions in end-users' browsers. This setting will cause + * recording to stop when the limit has been reached. + */ + mutationLimit: number; + /** * Callback before adding a custom recording event * @@ -295,8 +309,6 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { _experiments: Partial<{ captureExceptions: boolean; traceInternals: boolean; - mutationLimit: number; - mutationBreadcrumbLimit: number; slowClicks: { threshold: number; timeout: number; diff --git a/packages/replay/test/utils/setupReplayContainer.ts b/packages/replay/test/utils/setupReplayContainer.ts index 83ced117c464..c7c302cce72b 100644 --- a/packages/replay/test/utils/setupReplayContainer.ts +++ b/packages/replay/test/utils/setupReplayContainer.ts @@ -15,6 +15,8 @@ const DEFAULT_OPTIONS = { networkCaptureBodies: true, networkRequestHeaders: [], networkResponseHeaders: [], + mutationLimit: 1500, + mutationBreadcrumbLimit: 500, _experiments: {}, }; @@ -54,6 +56,8 @@ export const DEFAULT_OPTIONS_EVENT_PAYLOAD = { maskAllText: false, maskAllInputs: false, useCompression: DEFAULT_OPTIONS.useCompression, + mutationLimit: DEFAULT_OPTIONS.mutationLimit, + mutationBreadcrumbLimit: DEFAULT_OPTIONS.mutationBreadcrumbLimit, networkDetailHasUrls: DEFAULT_OPTIONS.networkDetailAllowUrls.length > 0, networkCaptureBodies: DEFAULT_OPTIONS.networkCaptureBodies, networkRequestHeaders: DEFAULT_OPTIONS.networkRequestHeaders.length > 0,