From a10c2a1bda71afe1268853af8521f3a17643b500 Mon Sep 17 00:00:00 2001 From: eoghan Date: Tue, 26 May 2020 18:09:30 +0000 Subject: [PATCH 01/13] The `processMutations` function needed to be bound to the `mutationBuffer` object, as otherwise `this` referred to the `MutationObserver` object itself --- src/record/observer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/record/observer.ts b/src/record/observer.ts index 792cd7ff7f..27ff04d6d8 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -53,7 +53,9 @@ function initMutationObserver( maskInputOptions, recordCanvas, ); - const observer = new MutationObserver(mutationBuffer.processMutations); + const observer = new MutationObserver( + mutationBuffer.processMutations.bind(mutationBuffer) + ); observer.observe(document, { attributes: true, attributeOldValue: true, From 321502ace93f8efe95735cba5e955a12f8ce0035 Mon Sep 17 00:00:00 2001 From: eoghan Date: Wed, 27 May 2020 16:25:58 +0000 Subject: [PATCH 02/13] Enable external pausing of mutation buffer emissions - no automatic pausing based on e.g. pageVisibility yet, assuming such a thing is desirable https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API - user code has to call new API method `freezePage` e.g. when page is hidden or after a timeout - automatically unpauses when the next user initiated event occurs (am assuming everything that isn't a mutation event counts as 'user initiated' either way think this is the correct thing to do until I see a counterexample of an event that shouldn't cause the mutations to be unbufferred) --- src/record/index.ts | 19 ++++++++++++++++++- src/record/mutation.ts | 8 ++++++-- src/record/observer.ts | 6 ++++-- typings/record/index.d.ts | 1 + typings/record/mutation.d.ts | 3 ++- typings/record/observer.d.ts | 4 +++- 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/record/index.ts b/src/record/index.ts index b5c508427a..fda4f32797 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -1,5 +1,5 @@ import { snapshot, MaskInputOptions } from 'rrweb-snapshot'; -import initObservers from './observer'; +import { initObservers, mutationBuffer } from './observer'; import { mirror, on, @@ -81,6 +81,19 @@ function record( let lastFullSnapshotEvent: eventWithTime; let incrementalSnapshotCount = 0; wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => { + if ( + mutationBuffer.paused && + !( + e.type == EventType.IncrementalSnapshot && + e.data.source == IncrementalSource.Mutation + ) + ) { + // we've got a user initiated event so first we need to apply + // all DOM changes that have been buffering during paused state + mutationBuffer.emit(); + mutationBuffer.paused = false; + } + emit(((packFn ? packFn(e) : e) as unknown) as T, isCheckout); if (e.type === EventType.FullSnapshot) { lastFullSnapshotEvent = e; @@ -325,4 +338,8 @@ record.addCustomEvent = (tag: string, payload: T) => { ); }; +record.freezePage = () => { + mutationBuffer.paused = true; +}; + export default record; diff --git a/src/record/mutation.ts b/src/record/mutation.ts index ef781b9be7..5aa12260b2 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -109,6 +109,8 @@ function isINode(n: Node | INode): n is INode { * controls behaviour of a MutationObserver */ export default class MutationBuffer { + public paused: boolean = false; + private texts: textCursor[] = []; private attributes: attributeCursor[] = []; private removes: removedNodeMutation[] = []; @@ -143,7 +145,7 @@ export default class MutationBuffer { private maskInputOptions: MaskInputOptions; private recordCanvas: boolean; - constructor( + public init( cb: mutationCallBack, blockClass: blockClass, inlineStylesheet: boolean, @@ -253,7 +255,9 @@ export default class MutationBuffer { pushAdd(node.value); } - this.emit(); + if (!this.paused) { + this.emit(); + } }; public emit = () => { diff --git a/src/record/observer.ts b/src/record/observer.ts index 27ff04d6d8..a557ce9216 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -38,6 +38,8 @@ import { } from '../types'; import MutationBuffer from './mutation'; +export const mutationBuffer = new MutationBuffer(); + function initMutationObserver( cb: mutationCallBack, blockClass: blockClass, @@ -46,7 +48,7 @@ function initMutationObserver( recordCanvas: boolean, ): MutationObserver { // see mutation.ts for details - const mutationBuffer = new MutationBuffer( + mutationBuffer.init( cb, blockClass, inlineStylesheet, @@ -562,7 +564,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) { }; } -export default function initObservers( +export function initObservers( o: observerParam, hooks: hooksParam = {}, ): listenerHandler { diff --git a/typings/record/index.d.ts b/typings/record/index.d.ts index 73e1f2c373..9c5e20436f 100644 --- a/typings/record/index.d.ts +++ b/typings/record/index.d.ts @@ -2,5 +2,6 @@ import { eventWithTime, recordOptions, listenerHandler } from '../types'; declare function record(options?: recordOptions): listenerHandler | undefined; declare namespace record { var addCustomEvent: (tag: string, payload: T) => void; + var freezePage: () => void; } export default record; diff --git a/typings/record/mutation.d.ts b/typings/record/mutation.d.ts index 9d92fd205e..f8e5a9c0b5 100644 --- a/typings/record/mutation.d.ts +++ b/typings/record/mutation.d.ts @@ -1,6 +1,7 @@ import { MaskInputOptions } from 'rrweb-snapshot'; import { mutationRecord, blockClass, mutationCallBack } from '../types'; export default class MutationBuffer { + paused: boolean; private texts; private attributes; private removes; @@ -14,7 +15,7 @@ export default class MutationBuffer { private inlineStylesheet; private maskInputOptions; private recordCanvas; - constructor(cb: mutationCallBack, blockClass: blockClass, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, recordCanvas: boolean); + init(cb: mutationCallBack, blockClass: blockClass, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, recordCanvas: boolean): void; processMutations: (mutations: mutationRecord[]) => void; emit: () => void; private processMutation; diff --git a/typings/record/observer.d.ts b/typings/record/observer.d.ts index 75cac487be..9958737880 100644 --- a/typings/record/observer.d.ts +++ b/typings/record/observer.d.ts @@ -1,3 +1,5 @@ import { observerParam, listenerHandler, hooksParam } from '../types'; +import MutationBuffer from './mutation'; +export declare const mutationBuffer: MutationBuffer; export declare const INPUT_TAGS: string[]; -export default function initObservers(o: observerParam, hooks?: hooksParam): listenerHandler; +export declare function initObservers(o: observerParam, hooks?: hooksParam): listenerHandler; From 18a6a5a18ef197b0e30a3a9fd7c27bb9ffb79eda Mon Sep 17 00:00:00 2001 From: eoghan Date: Tue, 9 Jun 2020 15:13:31 +0000 Subject: [PATCH 03/13] Avoid a build up of duplicate `adds` by delaying pushing to adds until emission time --- src/record/mutation.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/record/mutation.ts b/src/record/mutation.ts index 5aa12260b2..7366e3d8a6 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -114,7 +114,6 @@ export default class MutationBuffer { private texts: textCursor[] = []; private attributes: attributeCursor[] = []; private removes: removedNodeMutation[] = []; - private adds: addedNodeMutation[] = []; private movedMap: Record = {}; @@ -161,6 +160,14 @@ export default class MutationBuffer { public processMutations = (mutations: mutationRecord[]) => { mutations.forEach(this.processMutation); + if (!this.paused) { + this.emit(); + } + }; + + public emit = () => { + + const adds: addedNodeMutation[] = []; /** * Sometimes child node may be pushed before its newly added @@ -184,7 +191,7 @@ export default class MutationBuffer { if (parentId === -1 || nextId === -1) { return addList.addNode(n); } - this.adds.push({ + adds.push({ parentId, nextId, node: serializeNodeWithId( @@ -255,12 +262,6 @@ export default class MutationBuffer { pushAdd(node.value); } - if (!this.paused) { - this.emit(); - } - }; - - public emit = () => { const payload = { texts: this.texts .map((text) => ({ @@ -277,7 +278,7 @@ export default class MutationBuffer { // attribute mutation's id was not in the mirror map means the target node has been removed .filter((attribute) => mirror.has(attribute.id)), removes: this.removes, - adds: this.adds, + adds: adds, }; // payload may be empty if the mutations happened in some blocked elements if ( @@ -294,7 +295,6 @@ export default class MutationBuffer { this.texts = []; this.attributes = []; this.removes = []; - this.adds = []; this.addedSet = new Set(); this.movedSet = new Set(); this.droppedSet = new Set(); From fcc290d496b5b9189901b98c3da7a59350b9fd4d Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 12 Jun 2020 18:07:26 +0100 Subject: [PATCH 04/13] Need to export freezePage in order to use it from rrweb.min.js --- src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 7883678d9a..e57b8e42ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,5 +11,6 @@ export { } from './types'; const { addCustomEvent } = record; +const { freezePage } = record; -export { record, addCustomEvent, Replayer, mirror, utils }; +export { record, addCustomEvent, freezePage, Replayer, mirror, utils }; From e8fcfcc3dd0fbe834935c08ea21c4dda64e4e002 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 12 Jun 2020 18:29:05 +0100 Subject: [PATCH 05/13] Add a test to check if mutations can be turned off with the `freezePage` method --- test/__snapshots__/integration.test.ts.snap | 167 ++++++++++++++++++++ test/integration.test.ts | 26 +++ 2 files changed, 193 insertions(+) diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index 18875bd175..3f89221028 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -1561,6 +1561,173 @@ exports[`form 1`] = ` ]" `; +exports[`frozen 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"mutation observer\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 11 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 15 + } + ], + \\"id\\": 14 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 16 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 17, + \\"attributes\\": { + \\"foo\\": \\"bar\\" + } + }, + { + \\"id\\": 4, + \\"attributes\\": { + \\"test\\": \\"true\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"li\\", + \\"attributes\\": { + \\"foo\\": \\"bar\\" + }, + \\"childNodes\\": [], + \\"id\\": 17 + } + } + ] + } + } +]" +`; + exports[`ignore 1`] = ` "[ { diff --git a/test/integration.test.ts b/test/integration.test.ts index f308109cfe..394b4207f6 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -139,6 +139,32 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots, __filename, 'select2'); }); + it('can freeze mutations', async () => { + const page: puppeteer.Page = await this.browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'mutation-observer.html')); + + await page.evaluate(() => { + const li = document.createElement('li'); + const ul = document.querySelector('ul') as HTMLUListElement; + ul.appendChild(li); + li.setAttribute('foo', 'bar'); + document.body.setAttribute('test', 'true'); + }); + await page.evaluate('rrweb.freezePage()'); + await page.evaluate(() => { + document.body.setAttribute('test', 'bad'); + const ul = document.querySelector('ul') as HTMLUListElement; + const li = document.createElement('li'); + li.setAttribute('bad-attr', 'bad'); + li.innerText = 'bad text'; + ul.appendChild(li); + document.body.removeChild(ul); + }); + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots, __filename, 'frozen'); + }); + it('should not record input events on ignored elements', async () => { const page: puppeteer.Page = await this.browser.newPage(); await page.goto('about:blank'); From 33f3104bd71bcd97f71770ab13c70d97cf6450e2 Mon Sep 17 00:00:00 2001 From: eoghan Date: Tue, 29 Sep 2020 17:25:46 +0000 Subject: [PATCH 06/13] I noticed out of order ids (in terms of a DOM walk) in a FullSnapshot. A DOM mutation was executed against the mirror asynchronously before it could be fully processed. This would lead to a situation in replay where a mutation is executed against a DOM tree that already has the mutation applied. This changeset fixes that by freezing any mutations until the snapshot is completed. --- src/record/index.ts | 4 ++++ src/record/mutation.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/record/index.ts b/src/record/index.ts index fda4f32797..b9946cf901 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -123,6 +123,8 @@ function record( }), isCheckout, ); + + mutationBuffer.paused = true; // don't allow any mirror modifications during snapshotting const [node, idNodeMap] = snapshot( document, blockClass, @@ -160,6 +162,8 @@ function record( }, }), ); + mutationBuffer.emit(); // emit anything queued up now + mutationBuffer.paused = false; } try { diff --git a/src/record/mutation.ts b/src/record/mutation.ts index 7366e3d8a6..a4c20ccf8a 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -114,6 +114,7 @@ export default class MutationBuffer { private texts: textCursor[] = []; private attributes: attributeCursor[] = []; private removes: removedNodeMutation[] = []; + private mapRemoves: Node[] = []; private movedMap: Record = {}; @@ -166,6 +167,8 @@ export default class MutationBuffer { }; public emit = () => { + // delay any modification of the mirror until this function + // so that the mirror for takeFullSnapshot doesn't get mutated while it's event is being processed const adds: addedNodeMutation[] = []; @@ -262,6 +265,10 @@ export default class MutationBuffer { pushAdd(node.value); } + while (this.mapRemoves.length) { + mirror.removeNodeFromMap(this.mapRemoves.shift() as INode); + } + const payload = { texts: this.texts .map((text) => ({ @@ -377,7 +384,7 @@ export default class MutationBuffer { id: nodeId, }); } - mirror.removeNodeFromMap(n as INode); + this.mapRemoves.push(n); }); break; } From 2600fe737274a122ee171d489ea652803cce539c Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 2 Oct 2020 11:31:36 +0100 Subject: [PATCH 07/13] Remove attribute modifications from a mutation event that were incorrect in that they were repeating the attributes of those nodes present in the 'adds' array of the same mutation --- test/__snapshots__/integration.test.ts.snap | 32 --------------------- 1 file changed, 32 deletions(-) diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index 3f89221028..c052d3dfe5 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -4856,38 +4856,12 @@ exports[`select2 1`] = ` \\"class\\": \\"select2-container select2-dropdown-open select2-container-active\\" } }, - { - \\"id\\": 36, - \\"attributes\\": { - \\"id\\": \\"select2-drop\\", - \\"class\\": \\"select2-drop select2-display-none select2-with-searchbox select2-drop-active\\" - } - }, - { - \\"id\\": 70, - \\"attributes\\": { - \\"style\\": \\"\\" - } - }, - { - \\"id\\": 42, - \\"attributes\\": { - \\"class\\": \\"select2-input select2-focused\\", - \\"aria-activedescendant\\": \\"select2-result-label-2\\" - } - }, { \\"id\\": 35, \\"attributes\\": { \\"disabled\\": \\"\\" } }, - { - \\"id\\": 72, - \\"attributes\\": { - \\"class\\": \\"select2-results-dept-0 select2-result select2-result-selectable select2-highlighted\\" - } - } ], \\"removes\\": [ { @@ -5307,12 +5281,6 @@ exports[`select2 1`] = ` \\"source\\": 0, \\"texts\\": [], \\"attributes\\": [ - { - \\"id\\": 70, - \\"attributes\\": { - \\"style\\": \\"display: none;\\" - } - }, { \\"id\\": 36, \\"attributes\\": { From d2ccbdb0217ad65403bb253a68821258e2e2882b Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 2 Oct 2020 12:01:23 +0100 Subject: [PATCH 08/13] I've manually verified that this empty text node is actually removed when the dropdown is opened: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit document.getElementById('select2-results-1').childNodes NodeList(2) [li.select2-results-dept-0.select2-result.select2-result-selectable.select2-highlighted, li.select2-results-dept-0.select2-result.select2-result-selectable] and also that it is not reinstated after the second `await page.click('.select2-container');` --- test/__snapshots__/integration.test.ts.snap | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index c052d3dfe5..e6b49c0f0d 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -4871,6 +4871,10 @@ exports[`select2 1`] = ` { \\"parentId\\": 25, \\"id\\": 36 + }, + { + \\"parentId\\": 45, + \\"id\\": 46 } ], \\"adds\\": [ From a1a5a45296f932aeb191967d7e606da33b178f20 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 2 Oct 2020 14:46:57 +0100 Subject: [PATCH 09/13] Rearrange when removal happens in order to satisfy tests. I'm also reverting a recent test change (2600fe7) so that tests pass after this rearrangement; I believe that test change to still be the correct way of doing it, but maybe it is not strictly important that there are extra mutations on attributes of just added nodes --- src/record/mutation.ts | 8 +++--- test/__snapshots__/integration.test.ts.snap | 32 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/record/mutation.ts b/src/record/mutation.ts index a4c20ccf8a..d3303f82c9 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -210,6 +210,10 @@ export default class MutationBuffer { }); }; + while (this.mapRemoves.length) { + mirror.removeNodeFromMap(this.mapRemoves.shift() as INode); + } + for (const n of this.movedSet) { pushAdd(n); } @@ -265,10 +269,6 @@ export default class MutationBuffer { pushAdd(node.value); } - while (this.mapRemoves.length) { - mirror.removeNodeFromMap(this.mapRemoves.shift() as INode); - } - const payload = { texts: this.texts .map((text) => ({ diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index e6b49c0f0d..dfc80b8b51 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -4856,12 +4856,38 @@ exports[`select2 1`] = ` \\"class\\": \\"select2-container select2-dropdown-open select2-container-active\\" } }, + { + \\"id\\": 36, + \\"attributes\\": { + \\"id\\": \\"select2-drop\\", + \\"class\\": \\"select2-drop select2-display-none select2-with-searchbox select2-drop-active\\" + } + }, + { + \\"id\\": 70, + \\"attributes\\": { + \\"style\\": \\"\\" + } + }, + { + \\"id\\": 42, + \\"attributes\\": { + \\"class\\": \\"select2-input select2-focused\\", + \\"aria-activedescendant\\": \\"select2-result-label-2\\" + } + }, { \\"id\\": 35, \\"attributes\\": { \\"disabled\\": \\"\\" } }, + { + \\"id\\": 72, + \\"attributes\\": { + \\"class\\": \\"select2-results-dept-0 select2-result select2-result-selectable select2-highlighted\\" + } + } ], \\"removes\\": [ { @@ -5285,6 +5311,12 @@ exports[`select2 1`] = ` \\"source\\": 0, \\"texts\\": [], \\"attributes\\": [ + { + \\"id\\": 70, + \\"attributes\\": { + \\"style\\": \\"display: none;\\" + } + }, { \\"id\\": 36, \\"attributes\\": { From 1794b5fd46a3fc78cb1027be3d63dce6e342f23e Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 4 Dec 2019 16:22:53 +0000 Subject: [PATCH 10/13] As mutations are now paused during FullSnapshots, we shouldn't be counting this as a 'user emission'. We automatically emit mutations after unpause anyway ('emit anything queued up now') --- src/record/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/record/index.ts b/src/record/index.ts index b9946cf901..166a1c1d28 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -83,6 +83,7 @@ function record( wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => { if ( mutationBuffer.paused && + e.type !== EventType.FullSnapshot && !( e.type == EventType.IncrementalSnapshot && e.data.source == IncrementalSource.Mutation From a263331a0776de691848486760dbf117a41fa004 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 2 Oct 2020 15:58:51 +0100 Subject: [PATCH 11/13] Ensure that we clear arrays before emitting, as the mutation could have the side effect of triggering a FullSnapshot (checkoutEveryNth), which would otherwise re-trigger emission of same mutation (through the new pause/fullsnapshot/mutationemit/unpause process) --- src/record/mutation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/record/mutation.ts b/src/record/mutation.ts index d3303f82c9..bec1fe2472 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -296,7 +296,6 @@ export default class MutationBuffer { ) { return; } - this.emissionCallback(payload); // reset this.texts = []; @@ -306,6 +305,8 @@ export default class MutationBuffer { this.movedSet = new Set(); this.droppedSet = new Set(); this.movedMap = {}; + + this.emissionCallback(payload); }; private processMutation = (m: mutationRecord) => { From c93fbfd2a70ef055d4ea2da1c462b891028dd85e Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Fri, 2 Oct 2020 16:05:28 +0100 Subject: [PATCH 12/13] Don't let the programattic pausing during TakeFullSnapshot accidentally unpause a manual call to the API method `freezePage` --- src/record/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/record/index.ts b/src/record/index.ts index 166a1c1d28..19dcaeef61 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -125,6 +125,7 @@ function record( isCheckout, ); + let wasPaused = mutationBuffer.paused; mutationBuffer.paused = true; // don't allow any mirror modifications during snapshotting const [node, idNodeMap] = snapshot( document, @@ -163,8 +164,10 @@ function record( }, }), ); - mutationBuffer.emit(); // emit anything queued up now - mutationBuffer.paused = false; + if (!wasPaused) { + mutationBuffer.emit(); // emit anything queued up now + mutationBuffer.paused = false; + } } try { From 1006d8efeccd98e4bafdaa600f3498b3aa12e929 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Sun, 4 Oct 2020 13:37:42 +0100 Subject: [PATCH 13/13] Rename paused -> frozen for consistency and change to use getter/setter access methods --- src/record/index.ts | 14 +++++++------- src/record/mutation.ts | 16 ++++++++++++++-- typings/index.d.ts | 3 ++- typings/record/mutation.d.ts | 5 ++++- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/record/index.ts b/src/record/index.ts index 19dcaeef61..75393e33d5 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -82,7 +82,7 @@ function record( let incrementalSnapshotCount = 0; wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => { if ( - mutationBuffer.paused && + mutationBuffer.isFrozen() && e.type !== EventType.FullSnapshot && !( e.type == EventType.IncrementalSnapshot && @@ -92,7 +92,7 @@ function record( // we've got a user initiated event so first we need to apply // all DOM changes that have been buffering during paused state mutationBuffer.emit(); - mutationBuffer.paused = false; + mutationBuffer.unfreeze(); } emit(((packFn ? packFn(e) : e) as unknown) as T, isCheckout); @@ -125,8 +125,8 @@ function record( isCheckout, ); - let wasPaused = mutationBuffer.paused; - mutationBuffer.paused = true; // don't allow any mirror modifications during snapshotting + let wasFrozen = mutationBuffer.isFrozen(); + mutationBuffer.freeze(); // don't allow any mirror modifications during snapshotting const [node, idNodeMap] = snapshot( document, blockClass, @@ -164,9 +164,9 @@ function record( }, }), ); - if (!wasPaused) { + if (!wasFrozen) { mutationBuffer.emit(); // emit anything queued up now - mutationBuffer.paused = false; + mutationBuffer.unfreeze(); } } @@ -347,7 +347,7 @@ record.addCustomEvent = (tag: string, payload: T) => { }; record.freezePage = () => { - mutationBuffer.paused = true; + mutationBuffer.freeze(); }; export default record; diff --git a/src/record/mutation.ts b/src/record/mutation.ts index bec1fe2472..ec68f89ba7 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -109,7 +109,7 @@ function isINode(n: Node | INode): n is INode { * controls behaviour of a MutationObserver */ export default class MutationBuffer { - public paused: boolean = false; + private frozen: boolean = false; private texts: textCursor[] = []; private attributes: attributeCursor[] = []; @@ -159,9 +159,21 @@ export default class MutationBuffer { this.emissionCallback = cb; } + public freeze() { + this.frozen = true; + } + + public unfreeze() { + this.frozen = false; + } + + public isFrozen() { + return this.frozen; + } + public processMutations = (mutations: mutationRecord[]) => { mutations.forEach(this.processMutation); - if (!this.paused) { + if (!this.frozen) { this.emit(); } }; diff --git a/typings/index.d.ts b/typings/index.d.ts index 094914918a..b6bf190e78 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -4,4 +4,5 @@ import { mirror } from './utils'; import * as utils from './utils'; export { EventType, IncrementalSource, MouseInteractions, ReplayerEvents, } from './types'; declare const addCustomEvent: (tag: string, payload: T) => void; -export { record, addCustomEvent, Replayer, mirror, utils }; +declare const freezePage: () => void; +export { record, addCustomEvent, freezePage, Replayer, mirror, utils }; diff --git a/typings/record/mutation.d.ts b/typings/record/mutation.d.ts index f8e5a9c0b5..f57d7b8e54 100644 --- a/typings/record/mutation.d.ts +++ b/typings/record/mutation.d.ts @@ -1,7 +1,7 @@ import { MaskInputOptions } from 'rrweb-snapshot'; import { mutationRecord, blockClass, mutationCallBack } from '../types'; export default class MutationBuffer { - paused: boolean; + private frozen; private texts; private attributes; private removes; @@ -16,6 +16,9 @@ export default class MutationBuffer { private maskInputOptions; private recordCanvas; init(cb: mutationCallBack, blockClass: blockClass, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, recordCanvas: boolean): void; + freeze(): void; + unfreeze(): void; + isFrozen(): boolean; processMutations: (mutations: mutationRecord[]) => void; emit: () => void; private processMutation;