From afb2441a69943033336e5e0e79cbecd7f97ca57b Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Wed, 9 Nov 2022 18:11:09 +0100 Subject: [PATCH 01/10] Add test cases for bugs --- .../__snapshots__/integration.test.ts.snap | 427 ++++++++++++++++++ packages/rrweb/test/html/blank.html | 3 + packages/rrweb/test/integration.test.ts | 92 +++- 3 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 packages/rrweb/test/html/blank.html diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 61c6d0b465..52f779683e 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -11495,6 +11495,186 @@ exports[`record integration tests should record input userTriggered values if us ]" `; +exports[`record integration tests should record moved shadow DOM 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\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 8 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 4, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 9, + \\"isShadowHost\\": true + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 4, + \\"id\\": 9 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 4, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 11 + } + }, + { + \\"parentId\\": 11, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 9, + \\"isShadowHost\\": true + } + }, + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } + } + ] + } + } +]" +`; + exports[`record integration tests should record mutations in iframes accross pages 1`] = ` "[ { @@ -12601,6 +12781,253 @@ exports[`record integration tests should record shadow DOM 1`] = ` ]" `; +exports[`record integration tests should record shadow DOM 2 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\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 8 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 4, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 9, + \\"isShadowHost\\": true + } + }, + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } + } + ] + } + } +]" +`; + +exports[`record integration tests should record shadow DOM 3 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\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 8 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 4, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 9, + \\"isShadowHost\\": true + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } + } + ] + } + } +]" +`; + exports[`record integration tests should record shadow doms polyfilled by shadydom 1`] = ` "[ { diff --git a/packages/rrweb/test/html/blank.html b/packages/rrweb/test/html/blank.html new file mode 100644 index 0000000000..80e6028fd4 --- /dev/null +++ b/packages/rrweb/test/html/blank.html @@ -0,0 +1,3 @@ + + + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 7df01ca26d..13521d0610 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -603,7 +603,97 @@ describe('record integration tests', function (this: ISuite) { }); await page.waitForTimeout(50); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + + it('should record shadow DOM 2', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + // page.on('console', (msg) => console.log(msg.text())); + await page.setContent(getHtml.call(this, 'blank.html')); + + await page.evaluate(() => { + return new Promise((resolve) => { + /* + el = document.createElement('div'); + el.attachShadow({ mode: 'open' }); + el.shadowRoot.append(document.createElement('input')); + setTimeout(()=>{ + document.body.append(el); + }, 500); + */ + + const el = document.createElement('div') as HTMLDivElement; + el.attachShadow({ mode: 'open' }); + (el.shadowRoot as ShadowRoot).appendChild( + document.createElement('input'), + ); + setTimeout(() => { + document.body.append(el); + resolve(null); + }, 1000); + }); + }); + await waitForRAF(page); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + + it('should record shadow DOM 3', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + // page.on('console', (msg) => console.log(msg.text())); + await page.setContent(getHtml.call(this, 'blank.html')); + + await page.evaluate(() => { + const el = document.createElement('div') as HTMLDivElement; + el.attachShadow({ mode: 'open' }); + (el.shadowRoot as ShadowRoot).appendChild( + document.createElement('input'), + ); + document.body.append(el); + }); + await waitForRAF(page); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + + it('should record moved shadow DOM', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + // page.on('console', (msg) => console.log(msg.text())); + await page.setContent(getHtml.call(this, 'blank.html')); + + await page.evaluate(() => { + return new Promise((resolve) => { + const el = document.createElement('div') as HTMLDivElement; + el.attachShadow({ mode: 'open' }); + (el.shadowRoot as ShadowRoot).appendChild( + document.createElement('input'), + ); + document.body.append(el); + setTimeout(() => { + const newEl = document.createElement('div') as HTMLDivElement; + document.body.append(newEl); + newEl.append(el); + resolve(null); + }, 50); + }); + }); + await waitForRAF(page); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); From c468d580a71417c3c50476886dc7a6a22df6431e Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 10 Nov 2022 16:14:54 +0100 Subject: [PATCH 02/10] Fix shadow dom recording When moving an element containing shadow dom When adding an element to shadow dom before its attached to the dom --- packages/rrweb-snapshot/test/rebuild.test.ts | 31 ++++ packages/rrweb/src/record/mutation.ts | 4 +- .../rrweb/src/record/shadow-dom-manager.ts | 8 +- packages/rrweb/src/replay/index.ts | 5 +- packages/rrweb/src/utils.ts | 22 +++ .../__snapshots__/integration.test.ts.snap | 148 ++++++----------- packages/rrweb/test/integration.test.ts | 149 ++++++++++++------ 7 files changed, 216 insertions(+), 151 deletions(-) diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index 70e86a7142..dfbb2dcabf 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -47,6 +47,37 @@ describe('rebuild', function () { }); }); + describe('shadowDom', function () { + it('should rebuild shadowRoot with siblings', function () { + const node = buildNodeWithSN( + { + id: 1, + tagName: 'div', + type: NodeType.Element, + attributes: {}, + childNodes: [ + { + id: 2, + tagName: 'div', + type: NodeType.Element, + attributes: {}, + childNodes: [], + isShadow: true, + }, + ], + isShadowHost: true, + }, + { + doc: document, + mirror, + hackCss: false, + cache, + }, + ) as HTMLDivElement; + expect(node.shadowRoot?.childNodes.length).toBe(1); + }); + }); + describe('add hover class to hover selector related rules', function () { it('will do nothing to css text without :hover', () => { const cssText = 'body { color: white }'; diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 461811303d..c8dc5ee6e6 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -300,7 +300,7 @@ export default class MutationBuffer { blockSelector: this.blockSelector, maskTextClass: this.maskTextClass, maskTextSelector: this.maskTextSelector, - skipChild: true, + skipChild: !hasShadowRoot(n), newlyAddedElement: true, inlineStylesheet: this.inlineStylesheet, maskInputOptions: this.maskInputOptions, @@ -320,7 +320,7 @@ export default class MutationBuffer { ); } if (hasShadowRoot(n)) { - this.shadowDomManager.addShadowRoot(n.shadowRoot, document); + this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc); } }, onIframeLoad: (iframe, childSn) => { diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 807f6220c9..0719c5a76b 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -9,7 +9,7 @@ import { initScrollObserver, initAdoptedStyleSheetObserver, } from './observer'; -import { patch } from '../utils'; +import { shadowHostInDom, patch } from '../utils'; import type { Mirror } from 'rrweb-snapshot'; import { isNativeShadowDom } from 'rrweb-snapshot'; @@ -49,7 +49,11 @@ export class ShadowDomManager { function (original: (init: ShadowRootInit) => ShadowRoot) { return function (this: HTMLElement, option: ShadowRootInit) { const shadowRoot = original.call(this, option); - if (this.shadowRoot) + + // For the shadow dom elements in the document, monitor their dom mutations. + // For shadow dom elements that aren't in the document yet, + // we start monitoring them once their shadow dom host is appended to the document. + if (this.shadowRoot && shadowHostInDom(this)) manager.addShadowRoot(this.shadowRoot, this.ownerDocument); return shadowRoot; }; diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 61696fbc56..a66d53a28b 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1482,7 +1482,10 @@ export class Replayer { const target = buildNodeWithSN(mutation.node, { doc: targetDoc as Document, // can be Document or RRDocument mirror: mirror as Mirror, // can be this.mirror or virtualDom.mirror - skipChild: true, + /** + * shadowHosts contain a snapshot of shadow dom nodes which must also be added. + */ + skipChild: !mutation.node.isShadowHost, hackCss: true, cache: this.cache, /** diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 06affd3061..2238dcdedb 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -492,3 +492,25 @@ export class StyleSheetMirror { this.id = 1; } } + +export function getRootShadowHost(n: Node): Node | null { + const shadowHost = (n.getRootNode() as ShadowRoot).host; + // If n is in a nested shadow dom. + let rootShadowHost = shadowHost; + + while ( + rootShadowHost?.getRootNode?.()?.nodeType === Node.DOCUMENT_FRAGMENT_NODE && + (rootShadowHost.getRootNode() as ShadowRoot).host + ) + rootShadowHost = (rootShadowHost.getRootNode() as ShadowRoot).host; + + return rootShadowHost; +} + +export function shadowHostInDom(n: Node): boolean { + const doc = n.ownerDocument; + if (!doc) return false; + const shadowHost = getRootShadowHost(n); + return Boolean(shadowHost && doc.contains(shadowHost)); +} + diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 52f779683e..e08453d6b1 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -11590,7 +11590,16 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, - \\"childNodes\\": [], + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } + ], \\"id\\": 9, \\"isShadowHost\\": true } @@ -11598,29 +11607,6 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` ] } }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [], - \\"adds\\": [ - { - \\"parentId\\": 9, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } - } - ] - } - }, { \\"type\\": 3, \\"data\\": { @@ -11652,22 +11638,19 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, - \\"childNodes\\": [], + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } + ], \\"id\\": 9, \\"isShadowHost\\": true } - }, - { - \\"parentId\\": 9, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } } ] } @@ -12876,22 +12859,19 @@ exports[`record integration tests should record shadow DOM 2 1`] = ` \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, - \\"childNodes\\": [], + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } + ], \\"id\\": 9, \\"isShadowHost\\": true } - }, - { - \\"parentId\\": 9, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } } ] } @@ -12994,36 +12974,22 @@ exports[`record integration tests should record shadow DOM 3 1`] = ` \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, - \\"childNodes\\": [], + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } + ], \\"id\\": 9, \\"isShadowHost\\": true } } ] } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [], - \\"adds\\": [ - { - \\"parentId\\": 9, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } - } - ] - } } ]" `; @@ -13525,36 +13491,22 @@ exports[`record integration tests should record shadow doms polyfilled by synthe \\"attributes\\": { \\"id\\": \\"target4\\" }, - \\"childNodes\\": [], + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 32, + \\"isShadow\\": true + } + ], \\"id\\": 31, \\"isShadowHost\\": true } } ] } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [], - \\"adds\\": [ - { - \\"parentId\\": 31, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"ul\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 32, - \\"isShadow\\": true - } - } - ] - } } ]" `; diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 13521d0610..30bfe6f7f2 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -73,7 +73,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('textarea', 'textarea test'); await page.select('select', '1'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -91,7 +93,9 @@ describe('record integration tests', function (this: ISuite) { p.appendChild(document.createElement('span')); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -111,7 +115,9 @@ describe('record integration tests', function (this: ISuite) { p.innerText = 'mutated'; }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -129,7 +135,9 @@ describe('record integration tests', function (this: ISuite) { document.body.setAttribute('test', 'true'); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -146,7 +154,9 @@ describe('record integration tests', function (this: ISuite) { await page.evaluate( 'document.getElementById("select2-drop").setAttribute("style", document.getElementById("select2-drop").style.cssText + "color:black !important")', ); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -180,7 +190,9 @@ describe('record integration tests', function (this: ISuite) { await waitForRAF(page); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -191,7 +203,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('.rr-ignore', 'secret'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -209,7 +223,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('textarea', 'textarea test'); await page.select('select', '1'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -233,7 +249,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('input[type="password"]', 'password'); await page.select('select', '1'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -250,7 +268,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('input[type="password"]', 'secr3t'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -268,7 +288,9 @@ describe('record integration tests', function (this: ISuite) { await page.type('textarea', 'textarea test'); await page.select('select', '1'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -281,7 +303,9 @@ describe('record integration tests', function (this: ISuite) { await page.evaluate(`document.getElementById('text').innerText = '1'`); await page.click('#text'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -301,7 +325,9 @@ describe('record integration tests', function (this: ISuite) { nextElement.parentNode!.insertBefore(el, nextElement); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -311,12 +337,14 @@ describe('record integration tests', function (this: ISuite) { await page.setContent(getHtml.call(this, 'blocked-unblocked.html')); const elements1 = await page.$x('/html/body/div[1]/button'); - await elements1[0].click(); + await (elements1[0] as puppeteer.ElementHandle).click(); const elements2 = await page.$x('/html/body/div[2]/button'); - await elements2[0].click(); + await (elements2[0] as puppeteer.ElementHandle).click(); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -334,7 +362,9 @@ describe('record integration tests', function (this: ISuite) { p.removeChild(span); div.appendChild(span); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -349,7 +379,9 @@ describe('record integration tests', function (this: ISuite) { document.body.appendChild(div); div.appendChild(span); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -358,7 +390,9 @@ describe('record integration tests', function (this: ISuite) { await page.goto('about:blank'); await page.setContent(getHtml.call(this, 'react-styled-components.html')); await page.click('.toggle'); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -371,7 +405,9 @@ describe('record integration tests', function (this: ISuite) { }), ); await waitForRAF(page); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; for (const event of snapshots) { if (event.type === EventType.FullSnapshot) { visitSnapshot(event.data.node, (n) => { @@ -393,7 +429,9 @@ describe('record integration tests', function (this: ISuite) { }), ); await page.waitForTimeout(50); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -406,7 +444,9 @@ describe('record integration tests', function (this: ISuite) { }), ); await waitForRAF(page); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -425,7 +465,9 @@ describe('record integration tests', function (this: ISuite) { } }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -487,7 +529,9 @@ describe('record integration tests', function (this: ISuite) { console.log('from iframe'); }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -504,7 +548,9 @@ describe('record integration tests', function (this: ISuite) { await page.waitForTimeout(50); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -519,7 +565,9 @@ describe('record integration tests', function (this: ISuite) { await page.waitForSelector('img'); // wait for image to get added await waitForRAF(page); // wait for image to be captured - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -534,7 +582,9 @@ describe('record integration tests', function (this: ISuite) { await page.waitForTimeout(50); // wait for image to get added await waitForRAF(page); // wait for image to be captured - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -555,7 +605,9 @@ describe('record integration tests', function (this: ISuite) { await page.waitForTimeout(50); // wait for image to get added await waitForRAF(page); // wait for image to be captured - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -612,20 +664,9 @@ describe('record integration tests', function (this: ISuite) { it('should record shadow DOM 2', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); - // page.on('console', (msg) => console.log(msg.text())); await page.setContent(getHtml.call(this, 'blank.html')); - await page.evaluate(() => { return new Promise((resolve) => { - /* - el = document.createElement('div'); - el.attachShadow({ mode: 'open' }); - el.shadowRoot.append(document.createElement('input')); - setTimeout(()=>{ - document.body.append(el); - }, 500); - */ - const el = document.createElement('div') as HTMLDivElement; el.attachShadow({ mode: 'open' }); (el.shadowRoot as ShadowRoot).appendChild( @@ -648,7 +689,6 @@ describe('record integration tests', function (this: ISuite) { it('should record shadow DOM 3', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); - // page.on('console', (msg) => console.log(msg.text())); await page.setContent(getHtml.call(this, 'blank.html')); await page.evaluate(() => { @@ -670,7 +710,6 @@ describe('record integration tests', function (this: ISuite) { it('should record moved shadow DOM', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); - // page.on('console', (msg) => console.log(msg.text())); await page.setContent(getHtml.call(this, 'blank.html')); await page.evaluate(() => { @@ -736,7 +775,9 @@ describe('record integration tests', function (this: ISuite) { }); await waitForRAF(page); // wait till browser sent snapshots - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -764,7 +805,9 @@ describe('record integration tests', function (this: ISuite) { }); await waitForRAF(page); // wait for snapshot to be updated - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -796,7 +839,9 @@ describe('record integration tests', function (this: ISuite) { }); await waitForRAF(page); // wait till browser sent snapshots - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -836,7 +881,9 @@ describe('record integration tests', function (this: ISuite) { }); await waitForRAF(page); // wait till browser sent snapshots - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -849,7 +896,9 @@ describe('record integration tests', function (this: ISuite) { }), ); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -863,7 +912,9 @@ describe('record integration tests', function (this: ISuite) { }), ); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); @@ -884,7 +935,9 @@ describe('record integration tests', function (this: ISuite) { p.innerText = 'mutated'; }); - const snapshots = await page.evaluate('window.snapshots'); + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; assertSnapshot(snapshots); }); }); From 1427e291e7711de3b92d7422015172989247be0f Mon Sep 17 00:00:00 2001 From: Juice10 Date: Thu, 10 Nov 2022 15:16:21 +0000 Subject: [PATCH 03/10] Apply formatting changes --- packages/rrweb/src/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 2238dcdedb..b46b7fba7e 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -513,4 +513,3 @@ export function shadowHostInDom(n: Node): boolean { const shadowHost = getRootShadowHost(n); return Boolean(shadowHost && doc.contains(shadowHost)); } - From ab2b80f24a604db608800ff63b749ae3820e2cfd Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 10 Nov 2022 16:18:48 +0100 Subject: [PATCH 04/10] Refactor in dom checking code --- packages/rrweb/src/record/mutation.ts | 16 +++------------- packages/rrweb/src/utils.ts | 5 +++++ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index c8dc5ee6e6..4c3402f49f 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -26,6 +26,7 @@ import { hasShadowRoot, isSerializedIframe, isSerializedStylesheet, + inDom, } from '../utils'; type DoubleLinkedListNode = { @@ -271,19 +272,8 @@ export default class MutationBuffer { (n.getRootNode() as ShadowRoot).host ) shadowHost = (n.getRootNode() as ShadowRoot).host; - // If n is in a nested shadow dom. - let rootShadowHost = shadowHost; - while ( - rootShadowHost?.getRootNode?.()?.nodeType === - Node.DOCUMENT_FRAGMENT_NODE && - (rootShadowHost.getRootNode() as ShadowRoot).host - ) - rootShadowHost = (rootShadowHost.getRootNode() as ShadowRoot).host; - // ensure contains is passed a Node, or it will throw an error - const notInDoc = - !this.doc.contains(n) && - (!rootShadowHost || !this.doc.contains(rootShadowHost)); - if (!n.parentNode || notInDoc) { + + if (!n.parentNode || !inDom(n)) { return; } const parentId = isShadowRoot(n.parentNode) diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 2238dcdedb..4bbc35651b 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -514,3 +514,8 @@ export function shadowHostInDom(n: Node): boolean { return Boolean(shadowHost && doc.contains(shadowHost)); } +export function inDom(n: Node): boolean { + const doc = n.ownerDocument; + if (!doc) return false; + return doc.contains(n) || shadowHostInDom(n); +} From e146c940eb0f17396734076b01c0877304130e8e Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 10 Nov 2022 20:56:17 +0100 Subject: [PATCH 05/10] Nodes don't get processed in more than one mutation buffer --- packages/rrweb/src/record/index.ts | 8 +- packages/rrweb/src/record/mutation.ts | 15 ++- packages/rrweb/src/record/observer.ts | 2 + .../src/record/processed-node-manager.ts | 26 ++++ .../rrweb/src/record/shadow-dom-manager.ts | 4 +- packages/rrweb/src/replay/index.ts | 2 +- packages/rrweb/src/types.ts | 3 + .../__snapshots__/integration.test.ts.snap | 115 ++++++++++-------- 8 files changed, 119 insertions(+), 56 deletions(-) create mode 100644 packages/rrweb/src/record/processed-node-manager.ts diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index ace4dbf297..b43702e478 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -4,7 +4,11 @@ import { SlimDOMOptions, createMirror, } from 'rrweb-snapshot'; -import { initObservers, mutationBuffers } from './observer'; +import { + initObservers, + mutationBuffers, + processedNodeManager, +} from './observer'; import { on, getWindowWidth, @@ -280,6 +284,7 @@ function record( stylesheetManager, canvasManager, keepIframeSrcFn, + processedNodeManager, }, mirror, }); @@ -505,6 +510,7 @@ function record( iframeManager, stylesheetManager, shadowDomManager, + processedNodeManager, canvasManager, ignoreCSSAttributes, plugins: diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 4c3402f49f..445af39565 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -176,6 +176,7 @@ export default class MutationBuffer { private stylesheetManager: observerParam['stylesheetManager']; private shadowDomManager: observerParam['shadowDomManager']; private canvasManager: observerParam['canvasManager']; + private processedNodeManager: observerParam['processedNodeManager']; public init(options: MutationBufferParam) { ([ @@ -199,6 +200,7 @@ export default class MutationBuffer { 'stylesheetManager', 'shadowDomManager', 'canvasManager', + 'processedNodeManager', ] as const).forEach((key) => { // just a type trick, the runtime result is correct this[key] = options[key] as never; @@ -290,7 +292,7 @@ export default class MutationBuffer { blockSelector: this.blockSelector, maskTextClass: this.maskTextClass, maskTextSelector: this.maskTextSelector, - skipChild: !hasShadowRoot(n), + skipChild: true, newlyAddedElement: true, inlineStylesheet: this.inlineStylesheet, maskInputOptions: this.maskInputOptions, @@ -637,6 +639,9 @@ export default class MutationBuffer { * Make sure you check if `n`'s parent is blocked before calling this function * */ private genAdds = (n: Node, target?: Node) => { + // this node was already recorded, ignore it + if (this.processedNodeManager.has(n)) return; + if (this.mirror.hasNode(n)) { if (isIgnored(n, this.mirror)) { return; @@ -656,8 +661,14 @@ export default class MutationBuffer { // if this node is blocked `serializeNode` will turn it into a placeholder element // but we have to remove it's children otherwise they will be added as placeholders too - if (!isBlocked(n, this.blockClass, this.blockSelector, false)) + if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { n.childNodes.forEach((childN) => this.genAdds(childN)); + if (hasShadowRoot(n)) { + n.shadowRoot.childNodes.forEach((childN) => this.genAdds(childN, n)); + } + } + + this.processedNodeManager.add(n); }; } diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 975f8fa386..0dc7518d68 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -39,6 +39,7 @@ import { selectionCallback, } from '@rrweb/types'; import MutationBuffer from './mutation'; +import ProcessedNodeManager from './processed-node-manager'; type WindowWithStoredMutationObserver = IWindow & { __rrMutationObserver?: MutationObserver; @@ -50,6 +51,7 @@ type WindowWithAngularZone = IWindow & { }; export const mutationBuffers: MutationBuffer[] = []; +export const processedNodeManager = new ProcessedNodeManager(); const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== 'undefined'; const isCSSMediaRuleSupported = typeof CSSMediaRule !== 'undefined'; diff --git a/packages/rrweb/src/record/processed-node-manager.ts b/packages/rrweb/src/record/processed-node-manager.ts new file mode 100644 index 0000000000..aecb175634 --- /dev/null +++ b/packages/rrweb/src/record/processed-node-manager.ts @@ -0,0 +1,26 @@ +export default class ProcessedNodeManager { + private nodeSet: WeakSet = new WeakSet(); + + constructor() { + this.periodicallyClear(); + } + + private periodicallyClear() { + requestAnimationFrame(() => { + this.clear(); + this.periodicallyClear(); + }); + } + + public has(node: Node) { + return this.nodeSet.has(node); + } + + public add(node: Node) { + this.nodeSet.add(node); + } + + private clear() { + this.nodeSet = new WeakSet(); + } +} diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 0719c5a76b..9bf8187501 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -9,7 +9,7 @@ import { initScrollObserver, initAdoptedStyleSheetObserver, } from './observer'; -import { shadowHostInDom, patch } from '../utils'; +import { patch, inDom } from '../utils'; import type { Mirror } from 'rrweb-snapshot'; import { isNativeShadowDom } from 'rrweb-snapshot'; @@ -53,7 +53,7 @@ export class ShadowDomManager { // For the shadow dom elements in the document, monitor their dom mutations. // For shadow dom elements that aren't in the document yet, // we start monitoring them once their shadow dom host is appended to the document. - if (this.shadowRoot && shadowHostInDom(this)) + if (this.shadowRoot && inDom(this)) manager.addShadowRoot(this.shadowRoot, this.ownerDocument); return shadowRoot; }; diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index a66d53a28b..16582f20f7 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1485,7 +1485,7 @@ export class Replayer { /** * shadowHosts contain a snapshot of shadow dom nodes which must also be added. */ - skipChild: !mutation.node.isShadowHost, + skipChild: true, hackCss: true, cache: this.cache, /** diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 7ada19c521..cdf0b2cb70 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -39,6 +39,7 @@ import type { styleSheetRuleCallback, viewportResizeCallback, } from '@rrweb/types'; +import type ProcessedNodeManager from './record/processed-node-manager'; export type recordOptions = { emit?: (e: T, isCheckout?: boolean) => void; @@ -106,6 +107,7 @@ export type observerParam = { stylesheetManager: StylesheetManager; shadowDomManager: ShadowDomManager; canvasManager: CanvasManager; + processedNodeManager: ProcessedNodeManager; ignoreCSSAttributes: Set; plugins: Array<{ observer: ( @@ -140,6 +142,7 @@ export type MutationBufferParam = Pick< | 'stylesheetManager' | 'shadowDomManager' | 'canvasManager' + | 'processedNodeManager' >; export type ReplayPlugin = { diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index e08453d6b1..f219eab29c 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -11590,19 +11590,22 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } - ], + \\"childNodes\\": [], \\"id\\": 9, \\"isShadowHost\\": true } + }, + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } } ] } @@ -11620,6 +11623,18 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` } ], \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } + }, { \\"parentId\\": 4, \\"nextId\\": null, @@ -11638,16 +11653,7 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } - ], + \\"childNodes\\": [], \\"id\\": 9, \\"isShadowHost\\": true } @@ -12859,19 +12865,22 @@ exports[`record integration tests should record shadow DOM 2 1`] = ` \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } - ], + \\"childNodes\\": [], \\"id\\": 9, \\"isShadowHost\\": true } + }, + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } } ] } @@ -12974,19 +12983,22 @@ exports[`record integration tests should record shadow DOM 3 1`] = ` \\"type\\": 2, \\"tagName\\": \\"div\\", \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } - ], + \\"childNodes\\": [], \\"id\\": 9, \\"isShadowHost\\": true } + }, + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadow\\": true + } } ] } @@ -13491,19 +13503,22 @@ exports[`record integration tests should record shadow doms polyfilled by synthe \\"attributes\\": { \\"id\\": \\"target4\\" }, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"ul\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 32, - \\"isShadow\\": true - } - ], + \\"childNodes\\": [], \\"id\\": 31, \\"isShadowHost\\": true } + }, + { + \\"parentId\\": 31, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 32, + \\"isShadow\\": true + } } ] } From 67f5ef9974fdb6ab5012a94db40e0d57b10c5c09 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Thu, 10 Nov 2022 23:53:13 +0100 Subject: [PATCH 06/10] Constrain node mutations to one mutation buffer per request animation frame --- packages/rrweb/src/record/mutation.ts | 11 +- .../src/record/processed-node-manager.ts | 16 +- .../__snapshots__/integration.test.ts.snap | 183 ++++++++++++++++++ packages/rrweb/test/integration.test.ts | 38 ++++ 4 files changed, 242 insertions(+), 6 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 445af39565..9b1714b10d 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -639,8 +639,8 @@ export default class MutationBuffer { * Make sure you check if `n`'s parent is blocked before calling this function * */ private genAdds = (n: Node, target?: Node) => { - // this node was already recorded, ignore it - if (this.processedNodeManager.has(n)) return; + // this node was already recorded in other buffer, ignore it + if (this.processedNodeManager.inOtherBuffer(n, this)) return; if (this.mirror.hasNode(n)) { if (isIgnored(n, this.mirror)) { @@ -664,11 +664,12 @@ export default class MutationBuffer { if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { n.childNodes.forEach((childN) => this.genAdds(childN)); if (hasShadowRoot(n)) { - n.shadowRoot.childNodes.forEach((childN) => this.genAdds(childN, n)); + n.shadowRoot.childNodes.forEach((childN) => { + this.processedNodeManager.add(childN, this); + this.genAdds(childN, n); + }); } } - - this.processedNodeManager.add(n); }; } diff --git a/packages/rrweb/src/record/processed-node-manager.ts b/packages/rrweb/src/record/processed-node-manager.ts index aecb175634..6533fdf128 100644 --- a/packages/rrweb/src/record/processed-node-manager.ts +++ b/packages/rrweb/src/record/processed-node-manager.ts @@ -1,5 +1,11 @@ +import type MutationBuffer from './mutation'; + +/** + * Keeps a log of nodes that could show up in multiple mutation buffer but shouldn't be handled twice. + */ export default class ProcessedNodeManager { private nodeSet: WeakSet = new WeakSet(); + private nodeMap: WeakMap> = new WeakMap(); constructor() { this.periodicallyClear(); @@ -16,8 +22,16 @@ export default class ProcessedNodeManager { return this.nodeSet.has(node); } - public add(node: Node) { + public inOtherBuffer(node: Node, thisBuffer: MutationBuffer) { + const buffers = this.nodeMap.get(node); + return ( + buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer) + ); + } + + public add(node: Node, buffer: MutationBuffer) { this.nodeSet.add(node); + this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer)); } private clear() { diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index f219eab29c..2471a6a1dc 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -11664,6 +11664,189 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` ]" `; +exports[`record integration tests should record moved shadow DOM 2 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\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 8 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 4, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"newEl\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + } + }, + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"el\\" + }, + \\"childNodes\\": [], + \\"id\\": 10, + \\"isShadowHost\\": true + } + }, + { + \\"parentId\\": 10, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 11, + \\"isShadow\\": true + } + }, + { + \\"parentId\\": 10, + \\"nextId\\": 11, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 12, + \\"isShadow\\": true + } + }, + { + \\"parentId\\": 12, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 13 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [ + { + \\"parentId\\": 12, + \\"id\\": 13 + } + ], + \\"adds\\": [ + { + \\"parentId\\": 11, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 13 + } + } + ] + } + } +]" +`; + exports[`record integration tests should record mutations in iframes accross pages 1`] = ` "[ { diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 30bfe6f7f2..5a5c406c5b 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -736,6 +736,44 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('should record moved shadow DOM 2', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'blank.html')); + + await page.evaluate(() => { + const el = document.createElement('div') as HTMLDivElement; + el.id = 'el'; + el.attachShadow({ mode: 'open' }); + (el.shadowRoot as ShadowRoot).appendChild( + document.createElement('input'), + ); + document.body.append(el); + (el.shadowRoot as ShadowRoot).appendChild(document.createElement('span')); + (el.shadowRoot as ShadowRoot).appendChild(document.createElement('p')); + const newEl = document.createElement('div') as HTMLDivElement; + newEl.id = 'newEl'; + document.body.append(newEl); + newEl.append(el); + const input = el.shadowRoot?.children[0] as HTMLInputElement; + const span = el.shadowRoot?.children[1] as HTMLSpanElement; + const p = el.shadowRoot?.children[2] as HTMLParagraphElement; + input.remove(); + span.append(input); + p.append(input); + span.append(input); + setTimeout(() => { + p.append(input); + }, 0); + }); + await waitForRAF(page); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + it('should record nested iframes and shadow doms', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); From c9769bfe1b065cc00e5bdd7b0f501277cfd189a0 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 11 Nov 2022 13:12:59 +0100 Subject: [PATCH 07/10] Make tests less flaky under heavy load --- packages/rrweb/test/record.test.ts | 173 ++++++++++++++++------------- 1 file changed, 93 insertions(+), 80 deletions(-) diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 515bdfc1f8..37cecd16f8 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -465,54 +465,63 @@ describe('record', function (this: ISuite) { it('captures mutations on adopted stylesheets', async () => { await ctx.page.evaluate(() => { - document.body.innerHTML = ` + return new Promise((resolve) => { + document.body.innerHTML = `
div in outermost document
`; - const sheet = new CSSStyleSheet(); - // Add stylesheet to a document. + const sheet = new CSSStyleSheet(); + // Add stylesheet to a document. - document.adoptedStyleSheets = [sheet]; + document.adoptedStyleSheets = [sheet]; - const iframe = document.querySelector('iframe'); - const sheet2 = new (iframe!.contentWindow! as Window & - typeof globalThis).CSSStyleSheet(); + const iframe = document.querySelector('iframe'); + const sheet2 = new (iframe!.contentWindow! as Window & + typeof globalThis).CSSStyleSheet(); - // Add stylesheet to an IFrame document. - iframe!.contentDocument!.adoptedStyleSheets = [sheet2]; - iframe!.contentDocument!.body.innerHTML = '

h1 in iframe

'; + // Add stylesheet to an IFrame document. + iframe!.contentDocument!.adoptedStyleSheets = [sheet2]; + iframe!.contentDocument!.body.innerHTML = '

h1 in iframe

'; - const { record } = ((window as unknown) as IWindow).rrweb; - record({ - emit: ((window as unknown) as IWindow).emit, - }); + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + emit: ((window as unknown) as IWindow).emit, + }); - setTimeout(() => { - sheet.replace!('div { color: yellow; }'); - sheet2.replace!('h1 { color: blue; }'); - }, 0); + setTimeout(() => { + sheet.replace!('div { color: yellow; }'); + sheet2.replace!('h1 { color: blue; }'); + }, 0); - setTimeout(() => { - sheet.replaceSync!('div { display: inline ; }'); - sheet2.replaceSync!('h1 { font-size: large; }'); - }, 5); + setTimeout(() => { + sheet.replaceSync!('div { display: inline ; }'); + sheet2.replaceSync!('h1 { font-size: large; }'); + }, 5); - setTimeout(() => { - (sheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green'); - (sheet.cssRules[0] as CSSStyleRule).style.removeProperty('display'); - (sheet2.cssRules[0] as CSSStyleRule).style.setProperty( - 'font-size', - 'medium', - 'important', - ); - sheet2.insertRule('h2 { color: red; }'); - }, 10); + setTimeout(() => { + (sheet.cssRules[0] as CSSStyleRule).style.setProperty( + 'color', + 'green', + ); + (sheet.cssRules[0] as CSSStyleRule).style.removeProperty('display'); + (sheet2.cssRules[0] as CSSStyleRule).style.setProperty( + 'font-size', + 'medium', + 'important', + ); + sheet2.insertRule('h2 { color: red; }'); + }, 10); - setTimeout(() => { - sheet.insertRule('body { border: 2px solid blue; }', 1); - sheet2.deleteRule(0); - }, 15); + setTimeout(() => { + sheet.insertRule('body { border: 2px solid blue; }', 1); + sheet2.deleteRule(0); + }, 15); + + setTimeout(() => { + resolve(null); + }, 20); + }); }); await waitForRAF(ctx.page); assertSnapshot(ctx.events); @@ -695,65 +704,69 @@ describe('record', function (this: ISuite) { it('captures adopted stylesheets in shadow doms and iframe', async () => { await ctx.page.evaluate(() => { - document.body.innerHTML = ` + return new Promise((resolve) => { + document.body.innerHTML = `
div in outermost document
`; - const sheet = new CSSStyleSheet(); - sheet.replaceSync!( - 'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}', - ); - // Add stylesheet to a document. - - document.adoptedStyleSheets = [sheet]; - - // Add stylesheet to a shadow host. - const host = document.querySelector('#shadow-host1'); - const shadow = host!.attachShadow({ mode: 'open' }); - shadow.innerHTML = - '
div in shadow dom 1
span in shadow dom 1'; - const sheet2 = new CSSStyleSheet(); + const sheet = new CSSStyleSheet(); + sheet.replaceSync!( + 'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}', + ); + // Add stylesheet to a document. - sheet2.replaceSync!('span { color: red; }'); + document.adoptedStyleSheets = [sheet]; - shadow.adoptedStyleSheets = [sheet, sheet2]; + // Add stylesheet to a shadow host. + const host = document.querySelector('#shadow-host1'); + const shadow = host!.attachShadow({ mode: 'open' }); + shadow.innerHTML = + '
div in shadow dom 1
span in shadow dom 1'; + const sheet2 = new CSSStyleSheet(); - // Add stylesheet to an IFrame document. - const iframe = document.querySelector('iframe'); - const sheet3 = new (iframe!.contentWindow! as IWindow & - typeof globalThis).CSSStyleSheet(); - sheet3.replaceSync!('h1 { color: blue; }'); + sheet2.replaceSync!('span { color: red; }'); - iframe!.contentDocument!.adoptedStyleSheets = [sheet3]; + shadow.adoptedStyleSheets = [sheet, sheet2]; - const ele = iframe!.contentDocument!.createElement('h1'); - ele.innerText = 'h1 in iframe'; - iframe!.contentDocument!.body.appendChild(ele); + // Add stylesheet to an IFrame document. + const iframe = document.querySelector('iframe'); + const sheet3 = new (iframe!.contentWindow! as IWindow & + typeof globalThis).CSSStyleSheet(); + sheet3.replaceSync!('h1 { color: blue; }'); - ((window as unknown) as IWindow).rrweb.record({ - emit: ((window.top as unknown) as IWindow).emit, - }); + iframe!.contentDocument!.adoptedStyleSheets = [sheet3]; - // Make incremental changes to shadow dom. - setTimeout(() => { - const host = document.querySelector('#shadow-host2'); - const shadow = host!.attachShadow({ mode: 'open' }); - shadow.innerHTML = - '
div in shadow dom 2
span in shadow dom 2'; - const sheet4 = new CSSStyleSheet(); - sheet4.replaceSync!('span { color: green; }'); - shadow.adoptedStyleSheets = [sheet, sheet4]; + const ele = iframe!.contentDocument!.createElement('h1'); + ele.innerText = 'h1 in iframe'; + iframe!.contentDocument!.body.appendChild(ele); - document.adoptedStyleSheets = [sheet4, sheet, sheet2]; + ((window as unknown) as IWindow).rrweb.record({ + emit: ((window.top as unknown) as IWindow).emit, + }); - const sheet5 = new (iframe!.contentWindow! as IWindow & - typeof globalThis).CSSStyleSheet(); - sheet5.replaceSync!('h2 { color: purple; }'); - iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3]; - }, 10); + // Make incremental changes to shadow dom. + setTimeout(() => { + const host = document.querySelector('#shadow-host2'); + const shadow = host!.attachShadow({ mode: 'open' }); + shadow.innerHTML = + '
div in shadow dom 2
span in shadow dom 2'; + const sheet4 = new CSSStyleSheet(); + sheet4.replaceSync!('span { color: green; }'); + shadow.adoptedStyleSheets = [sheet, sheet4]; + + document.adoptedStyleSheets = [sheet4, sheet, sheet2]; + + const sheet5 = new (iframe!.contentWindow! as IWindow & + typeof globalThis).CSSStyleSheet(); + sheet5.replaceSync!('h2 { color: purple; }'); + iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3]; + + resolve(null); + }, 10); + }); }); await waitForRAF(ctx.page); // wait till events get sent From 58c8e02746a4482f7f3c86d157d41dbde19710b7 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Fri, 11 Nov 2022 13:26:07 +0100 Subject: [PATCH 08/10] Apply suggestions from code review --- packages/rrweb/src/replay/index.ts | 3 --- packages/rrweb/test/integration.test.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 16582f20f7..61696fbc56 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1482,9 +1482,6 @@ export class Replayer { const target = buildNodeWithSN(mutation.node, { doc: targetDoc as Document, // can be Document or RRDocument mirror: mirror as Mirror, // can be this.mirror or virtualDom.mirror - /** - * shadowHosts contain a snapshot of shadow dom nodes which must also be added. - */ skipChild: true, hackCss: true, cache: this.cache, diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 5a5c406c5b..ce1769c3d7 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -675,7 +675,7 @@ describe('record integration tests', function (this: ISuite) { setTimeout(() => { document.body.append(el); resolve(null); - }, 1000); + }, 10); }); }); await waitForRAF(page); From 5014576384d990b37b44a642844e9e96826ecf4a Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Mon, 9 Jan 2023 11:01:21 +0100 Subject: [PATCH 09/10] Update packages/rrweb-snapshot/test/rebuild.test.ts --- packages/rrweb-snapshot/test/rebuild.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb-snapshot/test/rebuild.test.ts b/packages/rrweb-snapshot/test/rebuild.test.ts index dfbb2dcabf..357cd2fb3c 100644 --- a/packages/rrweb-snapshot/test/rebuild.test.ts +++ b/packages/rrweb-snapshot/test/rebuild.test.ts @@ -48,7 +48,7 @@ describe('rebuild', function () { }); describe('shadowDom', function () { - it('should rebuild shadowRoot with siblings', function () { + it('rebuild shadowRoot without siblings', function () { const node = buildNodeWithSN( { id: 1, From 03ed8da84e07fe9f863c1cbd196f290df7f4f603 Mon Sep 17 00:00:00 2001 From: Justin Halsall Date: Tue, 10 Jan 2023 11:31:54 +0100 Subject: [PATCH 10/10] Remove unused nodeSet --- packages/rrweb/src/record/processed-node-manager.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/rrweb/src/record/processed-node-manager.ts b/packages/rrweb/src/record/processed-node-manager.ts index 6533fdf128..60b939bb12 100644 --- a/packages/rrweb/src/record/processed-node-manager.ts +++ b/packages/rrweb/src/record/processed-node-manager.ts @@ -4,7 +4,6 @@ import type MutationBuffer from './mutation'; * Keeps a log of nodes that could show up in multiple mutation buffer but shouldn't be handled twice. */ export default class ProcessedNodeManager { - private nodeSet: WeakSet = new WeakSet(); private nodeMap: WeakMap> = new WeakMap(); constructor() { @@ -18,10 +17,6 @@ export default class ProcessedNodeManager { }); } - public has(node: Node) { - return this.nodeSet.has(node); - } - public inOtherBuffer(node: Node, thisBuffer: MutationBuffer) { const buffers = this.nodeMap.get(node); return ( @@ -30,11 +25,10 @@ export default class ProcessedNodeManager { } public add(node: Node, buffer: MutationBuffer) { - this.nodeSet.add(node); this.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer)); } private clear() { - this.nodeSet = new WeakSet(); + this.nodeMap = new WeakMap(); } }