|
9 | 9 | SLOW_CLICK_SCROLL_TIMEOUT, |
10 | 10 | SLOW_CLICK_THRESHOLD, |
11 | 11 | WINDOW, |
| 12 | + MUTATION_DEBOUNCE_TIME, |
12 | 13 | } from './constants'; |
13 | 14 | import { ClickDetector } from './coreHandlers/handleClick'; |
14 | 15 | import { handleKeyboardEvent } from './coreHandlers/handleKeyboardEvent'; |
@@ -169,6 +170,17 @@ export class ReplayContainer implements ReplayContainerInterface { |
169 | 170 | /** Ensure page remains active when a key is pressed. */ |
170 | 171 | private _handleKeyboardEvent: (event: KeyboardEvent) => void; |
171 | 172 |
|
| 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 | + |
172 | 184 | public constructor({ |
173 | 185 | options, |
174 | 186 | recordingOptions, |
@@ -272,6 +284,8 @@ export class ReplayContainer implements ReplayContainerInterface { |
272 | 284 | this._handleKeyboardEvent = (event: KeyboardEvent) => { |
273 | 285 | handleKeyboardEvent(this, event); |
274 | 286 | }; |
| 287 | + |
| 288 | + this._lastMutationMap = new WeakMap(); |
275 | 289 | } |
276 | 290 |
|
277 | 291 | /** Get the event context. */ |
@@ -1303,10 +1317,60 @@ export class ReplayContainer implements ReplayContainerInterface { |
1303 | 1317 | } |
1304 | 1318 | } |
1305 | 1319 |
|
| 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 | + |
1306 | 1333 | /** Handler for rrweb.record.onMutation */ |
1307 | 1334 | private _onMutationHandler(mutations: unknown[]): boolean { |
1308 | 1335 | const count = mutations.length; |
1309 | 1336 |
|
| 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 | + |
1310 | 1374 | const mutationLimit = this._options.mutationLimit; |
1311 | 1375 | const mutationBreadcrumbLimit = this._options.mutationBreadcrumbLimit; |
1312 | 1376 | const overMutationLimit = mutationLimit && count > mutationLimit; |
|
0 commit comments