Skip to content

Commit 58cb8fa

Browse files
committed
add experimental dropRepetitiveMutations
1 parent 136370c commit 58cb8fa

File tree

3 files changed

+68
-0
lines changed

3 files changed

+68
-0
lines changed

packages/replay-internal/src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,6 @@ export const MAX_REPLAY_DURATION = 3_600_000; // 60 minutes in ms;
5353

5454
/** Default attributes to be ignored when `maskAllText` is enabled */
5555
export const DEFAULT_IGNORED_ATTRIBUTES = ['title', 'placeholder'];
56+
57+
// Time window in which to check for repeated DOM mutations
58+
export const MUTATION_DEBOUNCE_TIME = 100; // ms

packages/replay-internal/src/replay.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SLOW_CLICK_SCROLL_TIMEOUT,
1010
SLOW_CLICK_THRESHOLD,
1111
WINDOW,
12+
MUTATION_DEBOUNCE_TIME,
1213
} from './constants';
1314
import { ClickDetector } from './coreHandlers/handleClick';
1415
import { handleKeyboardEvent } from './coreHandlers/handleKeyboardEvent';
@@ -169,6 +170,17 @@ export class ReplayContainer implements ReplayContainerInterface {
169170
/** Ensure page remains active when a key is pressed. */
170171
private _handleKeyboardEvent: (event: KeyboardEvent) => void;
171172

173+
/**
174+
* Map to track the history for DOM node mutations
175+
*/
176+
private _lastMutationMap: WeakMap<
177+
Node,
178+
{
179+
timestamp: number;
180+
fingerprint: string;
181+
}
182+
>;
183+
172184
public constructor({
173185
options,
174186
recordingOptions,
@@ -272,6 +284,8 @@ export class ReplayContainer implements ReplayContainerInterface {
272284
this._handleKeyboardEvent = (event: KeyboardEvent) => {
273285
handleKeyboardEvent(this, event);
274286
};
287+
288+
this._lastMutationMap = new WeakMap();
275289
}
276290

277291
/** Get the event context. */
@@ -1303,10 +1317,60 @@ export class ReplayContainer implements ReplayContainerInterface {
13031317
}
13041318
}
13051319

1320+
/**
1321+
* Heuristically create an identifier for a mutation record.
1322+
* This is used for checking on repeated mutations on the same target.
1323+
*/
1324+
private _getMutationFingerprint(mutation: MutationRecord): string {
1325+
if (mutation.type === 'attributes') {
1326+
return `attr:${mutation.attributeName}`;
1327+
}
1328+
// For other mutation types, return empty string
1329+
// TODO: Should be extended to handle other mutation types
1330+
return '';
1331+
}
1332+
13061333
/** Handler for rrweb.record.onMutation */
13071334
private _onMutationHandler(mutations: unknown[]): boolean {
13081335
const count = mutations.length;
13091336

1337+
if (this._options._experiments.dropRepetitiveMutations) {
1338+
const now = Date.now();
1339+
1340+
// Filter out repeated mutations
1341+
const uniqueMutations = (mutations as MutationRecord[]).filter(mutation => {
1342+
const target = mutation.target;
1343+
const lastMutation = this._lastMutationMap.get(target);
1344+
1345+
// Create a fingerprint of this mutation
1346+
const fingerprint = this._getMutationFingerprint(mutation);
1347+
1348+
// Check if this is a repeated mutation within our debounce window
1349+
if (
1350+
fingerprint &&
1351+
lastMutation &&
1352+
lastMutation.fingerprint === fingerprint &&
1353+
now - lastMutation.timestamp < MUTATION_DEBOUNCE_TIME
1354+
) {
1355+
return false; // Skip this mutation
1356+
}
1357+
1358+
// Update mutation tracking for this target
1359+
this._lastMutationMap.set(target, {
1360+
timestamp: now,
1361+
fingerprint,
1362+
});
1363+
1364+
return true;
1365+
});
1366+
1367+
// All mutations are repetitions, do not process in rrweb
1368+
if (uniqueMutations.length === 0) {
1369+
// todo: maybe create a new breadcrumb here?
1370+
return false;
1371+
}
1372+
}
1373+
13101374
const mutationLimit = this._options.mutationLimit;
13111375
const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit;
13121376
const overMutationLimit = mutationLimit && count > mutationLimit;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
230230
traceInternals: boolean;
231231
continuousCheckout: number;
232232
autoFlushOnFeedback: boolean;
233+
dropRepetitiveMutations: boolean;
233234
}>;
234235
}
235236

0 commit comments

Comments
 (0)