Skip to content

Commit 912ed5c

Browse files
committed
feat(replay): Add ignoreMutations option
This option allows to configure a selector list of elements to not capture mutation for.
1 parent d7c2aa7 commit 912ed5c

File tree

7 files changed

+155
-2
lines changed

7 files changed

+155
-2
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window.Replay = Sentry.replayIntegration({
5+
flushMinDelay: 200,
6+
flushMaxDelay: 200,
7+
minReplayDuration: 0,
8+
useCompression: false,
9+
ignoreMutations: ['.moving'],
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
14+
sampleRate: 0,
15+
replaysSessionSampleRate: 1.0,
16+
replaysOnErrorSampleRate: 0.0,
17+
18+
integrations: [window.Replay],
19+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
function moveElement(el, remaining) {
2+
if (!remaining) {
3+
el.classList.remove('moving');
4+
5+
setTimeout(() => {
6+
el.style.transform = `translate(${remaining}0px, 0)`;
7+
el.classList.add('moved');
8+
});
9+
return;
10+
}
11+
12+
el.style.transform = `translate(${remaining}0px, 0)`;
13+
14+
setTimeout(() => {
15+
moveElement(el, remaining - 1);
16+
}, 10);
17+
}
18+
19+
const el = document.querySelector('#mutation-target');
20+
const btn = document.querySelector('#button-move');
21+
22+
btn.addEventListener('click', event => {
23+
el.classList.add('moving');
24+
event.preventDefault();
25+
moveElement(el, 20);
26+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<div id="mutation-target" style="position: relative">This is moved around!</div>
8+
9+
<button id="button-move" type="button">Move</button>
10+
</body>
11+
</html>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect } from '@playwright/test';
2+
import type { mutationData } from '@sentry-internal/rrweb-types';
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import type { RecordingSnapshot } from '../../../utils/replayHelpers';
5+
import { collectReplayRequests, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers';
6+
7+
sentryTest('allows to ignore mutations via `ignoreMutations` option', async ({ getLocalTestUrl, page }) => {
8+
if (shouldSkipReplayTest()) {
9+
sentryTest.skip();
10+
}
11+
12+
const url = await getLocalTestUrl({ testDir: __dirname });
13+
14+
const reqPromise0 = waitForReplayRequest(page, 0);
15+
16+
await page.goto(url);
17+
await reqPromise0;
18+
19+
const requestsPromise = collectReplayRequests(page, recordingEvents => {
20+
const events = recordingEvents as (RecordingSnapshot & { data: mutationData })[];
21+
return events.some(event => event.data.attributes?.some(attr => attr.attributes['class'] === 'moved'));
22+
});
23+
24+
page.locator('#button-move').click();
25+
26+
const requests = await requestsPromise;
27+
28+
// All transform mutatinos are ignored and not captured
29+
const transformMutations = requests.replayRecordingSnapshots.filter(
30+
item =>
31+
(item.data as mutationData)?.attributes?.some(
32+
attr => attr.attributes['style'] && attr.attributes['class'] !== 'moved',
33+
),
34+
);
35+
36+
// Should capture the final class mutation
37+
const classMutations = requests.replayRecordingSnapshots.filter(
38+
item => (item.data as mutationData)?.attributes?.some(attr => attr.attributes['class']),
39+
);
40+
41+
expect(transformMutations).toEqual([]);
42+
expect(classMutations).toEqual([
43+
{
44+
data: {
45+
adds: [],
46+
attributes: [
47+
{
48+
attributes: {
49+
class: 'moved',
50+
style: {
51+
transform: 'translate(0px, 0px)',
52+
},
53+
},
54+
id: expect.any(Number),
55+
},
56+
],
57+
removes: [],
58+
source: expect.any(Number),
59+
texts: [],
60+
},
61+
timestamp: 0,
62+
type: 3,
63+
},
64+
]);
65+
});

packages/replay-internal/src/integration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export class Replay implements Integration {
8484

8585
mutationBreadcrumbLimit = 750,
8686
mutationLimit = 10_000,
87+
ignoreMutations = [],
8788

8889
slowClickTimeout = 7_000,
8990
slowClickIgnoreSelectors = [],
@@ -167,6 +168,7 @@ export class Replay implements Integration {
167168
maskAllText,
168169
mutationBreadcrumbLimit,
169170
mutationLimit,
171+
ignoreMutations,
170172
slowClickTimeout,
171173
slowClickIgnoreSelectors,
172174
networkDetailAllowUrls,

packages/replay-internal/src/replay.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-lines */ // TODO: We might want to split this file up
22
import type { ReplayRecordingMode, Span } from '@sentry/core';
33
import { getActiveSpan, getClient, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core';
4-
import { EventType, record } from '@sentry-internal/rrweb';
4+
import { EventType, record, utils as rrwebUtils } from '@sentry-internal/rrweb';
55
import {
66
BUFFER_CHECKOUT_TIME,
77
SESSION_IDLE_EXPIRE_DURATION,
@@ -1304,7 +1304,20 @@ export class ReplayContainer implements ReplayContainerInterface {
13041304
}
13051305

13061306
/** Handler for rrweb.record.onMutation */
1307-
private _onMutationHandler(mutations: unknown[]): boolean {
1307+
private _onMutationHandler(mutations: MutationRecord[]): boolean {
1308+
const { ignoreMutations } = this._options;
1309+
if (ignoreMutations.length) {
1310+
if (
1311+
mutations.some(mutation => {
1312+
const el = rrwebUtils.closestElementOfNode(mutation.target);
1313+
const selector = ignoreMutations.join(',');
1314+
return el?.matches(selector);
1315+
})
1316+
) {
1317+
return false;
1318+
}
1319+
}
1320+
13081321
const count = mutations.length;
13091322

13101323
const mutationLimit = this._options.mutationLimit;
@@ -1336,3 +1349,12 @@ export class ReplayContainer implements ReplayContainerInterface {
13361349
return true;
13371350
}
13381351
}
1352+
1353+
interface MutationRecord {
1354+
type: string;
1355+
target: Node;
1356+
oldValue: string | null;
1357+
addedNodes: NodeList;
1358+
removedNodes: NodeList;
1359+
attributeName: string | null;
1360+
}

packages/replay-internal/src/types/replay.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
161161
*/
162162
mutationLimit: number;
163163

164+
/**
165+
* Completetly ignore mutations matching the given selectors.
166+
* This can be used if a specific type of mutation is causing (e.g. performance) problems.
167+
* NOTE: This can be dangerous to use, as mutations are applied as incremental patches.
168+
* Make sure to verify that the captured replays still work when using this option.
169+
*/
170+
ignoreMutations: string[];
171+
164172
/**
165173
* The max. time in ms to wait for a slow click to finish.
166174
* After this amount of time we stop waiting for actions after a click happened.

0 commit comments

Comments
 (0)