diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 1281e172cd..a61f650a65 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -18,6 +18,7 @@ import { isElement, isShadowRoot, maskInputValue, + isNativeShadowDom, } from './utils'; let _id = 1; @@ -248,7 +249,10 @@ export function transformAttribute( value: string, ): string { // relative path in attribute - if (name === 'src' || (name === 'href' && value && !(tagName === 'use' && value[0] === '#'))) { + if ( + name === 'src' || + (name === 'href' && value && !(tagName === 'use' && value[0] === '#')) + ) { // href starts with a # is an id pointer for svg return absoluteToDoc(doc, value); } else if (name === 'xlink:href' && value && value[0] !== '#') { @@ -1025,7 +1029,9 @@ export function serializeNodeWithId( recordChild = recordChild && !serializedNode.needBlock; // this property was not needed in replay side delete serializedNode.needBlock; - if ((n as HTMLElement).shadowRoot) serializedNode.isShadowHost = true; + const shadowRoot = (n as HTMLElement).shadowRoot; + if (shadowRoot && isNativeShadowDom(shadowRoot)) + serializedNode.isShadowHost = true; } if ( (serializedNode.type === NodeType.Document || @@ -1075,14 +1081,19 @@ export function serializeNodeWithId( for (const childN of Array.from(n.shadowRoot.childNodes)) { const serializedChildNode = serializeNodeWithId(childN, bypassOptions); if (serializedChildNode) { - serializedChildNode.isShadow = true; + isNativeShadowDom(n.shadowRoot) && + (serializedChildNode.isShadow = true); serializedNode.childNodes.push(serializedChildNode); } } } } - if (n.parentNode && isShadowRoot(n.parentNode)) { + if ( + n.parentNode && + isShadowRoot(n.parentNode) && + isNativeShadowDom(n.parentNode) + ) { serializedNode.isShadow = true; } diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 652b9fb436..1c151a1e25 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -16,6 +16,14 @@ export function isShadowRoot(n: Node): n is ShadowRoot { return Boolean(host?.shadowRoot === n); } +/** + * To fix the issue https://github.com/rrweb-io/rrweb/issues/933. + * Some websites use polyfilled shadow dom and this function is used to detect this situation. + */ +export function isNativeShadowDom(shadowRoot: ShadowRoot) { + return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; +} + export class Mirror implements IMirror { private idNodeMap: idNodeMap = new Map(); private nodeMetaMap: nodeMetaMap = new WeakMap(); diff --git a/packages/rrweb-snapshot/typings/utils.d.ts b/packages/rrweb-snapshot/typings/utils.d.ts index 23ae46f7ea..0a793b1df4 100644 --- a/packages/rrweb-snapshot/typings/utils.d.ts +++ b/packages/rrweb-snapshot/typings/utils.d.ts @@ -1,6 +1,7 @@ import { MaskInputFn, MaskInputOptions, IMirror, serializedNodeWithId } from './types'; export declare function isElement(n: Node): n is Element; export declare function isShadowRoot(n: Node): n is ShadowRoot; +export declare function isNativeShadowDom(shadowRoot: ShadowRoot): boolean; export declare class Mirror implements IMirror { private idNodeMap; private nodeMetaMap; diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index a7b4dbe0fc..425445cd11 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -6,6 +6,7 @@ import { needMaskingText, maskInputValue, Mirror, + isNativeShadowDom, } from 'rrweb-snapshot'; import type { mutationRecord, @@ -581,7 +582,10 @@ export default class MutationBuffer { this.removes.push({ parentId, id: nodeId, - isShadow: isShadowRoot(m.target) ? true : undefined, + isShadow: + isShadowRoot(m.target) && isNativeShadowDom(m.target) + ? true + : undefined, }); } this.mapRemoves.push(n); diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 06a1d3953d..90a1d321d6 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -7,6 +7,7 @@ import type { import { initMutationObserver, initScrollObserver } from './observer'; import { patch } from '../utils'; import type { Mirror } from 'rrweb-snapshot'; +import { isNativeShadowDom } from 'rrweb-snapshot'; type BypassOptions = Omit< MutationBufferParam, @@ -53,6 +54,7 @@ export class ShadowDomManager { } public addShadowRoot(shadowRoot: ShadowRoot, doc: Document) { + if (!isNativeShadowDom(shadowRoot)) return; initMutationObserver( { ...this.bypassOptions, diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 5af8b6dcf1..53b8206015 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -10764,6 +10764,537 @@ exports[`record integration tests should record shadow DOM 1`] = ` ]" `; +exports[`record integration tests should record shadow doms polyfilled by shadydom 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\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"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 \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/@webcomponents/shadydom@1.9.0/shadydom.min.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 11 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 17 + } + ], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target3\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 25 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 28 + } + ], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 29 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 30 + } + } + ] + } + } +]" +`; + +exports[`record integration tests should record shadow doms polyfilled by synthetic-shadow 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\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"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 \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"src\\": \\"https://cdn.jsdelivr.net/npm/@lwc/synthetic-shadow@2.20.3/dist/synthetic-shadow.js\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"id\\": 11 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 17, + \\"isShadow\\": true + } + ], + \\"id\\": 16, + \\"isShadowHost\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 20 + } + ], + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target3\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 25 + } + ], + \\"id\\": 24 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 28 + } + ], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 29 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 22, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 30 + } + }, + { + \\"parentId\\": 14, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"target4\\" + }, + \\"childNodes\\": [], + \\"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 + } + } + ] + } + } +]" +`; + exports[`record integration tests should record webgl canvas mutations 1`] = ` "[ { diff --git a/packages/rrweb/test/html/polyfilled-shadowdom-mutation.html b/packages/rrweb/test/html/polyfilled-shadowdom-mutation.html new file mode 100644 index 0000000000..0f7ec6e002 --- /dev/null +++ b/packages/rrweb/test/html/polyfilled-shadowdom-mutation.html @@ -0,0 +1,24 @@ + + + + + + +
+
+
+ + + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 8caa8cf35c..3655efe931 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -590,6 +590,78 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + // https://github.com/webcomponents/polyfills/tree/master/packages/shadydom + it('should record shadow doms polyfilled by shadydom', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + // insert shadydom script + replaceLast( + getHtml.call(this, 'polyfilled-shadowdom-mutation.html'), + '', + ` + + + + `, + ), + ); + await page.evaluate(() => { + const target3 = document.querySelector('#target3'); + target3?.attachShadow({ + mode: 'open', + }); + target3?.shadowRoot?.appendChild(document.createElement('span')); + }); + await waitForRAF(page); // wait till browser sent snapshots + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + // https://github.com/salesforce/lwc/tree/master/packages/%40lwc/synthetic-shadow + it('should record shadow doms polyfilled by synthetic-shadow', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + // insert lwc's synthetic-shadow script + replaceLast( + getHtml.call(this, 'polyfilled-shadowdom-mutation.html'), + '', + ` + + + + `, + ), + ); + await page.evaluate(() => { + const target3 = document.querySelector('#target3'); + // create a shadow dom with synthetic shadow + // https://github.com/salesforce/lwc/blob/v2.20.3/packages/@lwc/synthetic-shadow/src/faux-shadow/element.ts#L81-L87 + target3?.attachShadow({ + mode: 'open', + '$$lwc-synthetic-mode': true, + } as ShadowRootInit); + target3?.shadowRoot?.appendChild(document.createElement('span')); + const target4 = document.createElement('div'); + target4.id = 'target4'; + // create a native shadow dom + document.body.appendChild(target4); + target4.attachShadow({ + mode: 'open', + }); + target4.shadowRoot?.appendChild(document.createElement('ul')); + }); + await waitForRAF(page); // wait till browser sent snapshots + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + it('should mask texts', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank');