Skip to content

Commit d8f9290

Browse files
authored
add support for nested shadow dom (#834)
* fix: can't record shadow host and shadow dom in incremental mutations * enable to record newly added shadow dom * Revert "enable to record newly added shadow dom" This reverts commit cf7c0ad. * Revert "fix: can't record shadow host and shadow dom in incremental mutations" This reverts commit 8b25cc9. * fix: can't record shadow host and shadow dom in incremental mutations * add support for nested shadow root and add integration test * fix test error * enable to record shadow-dom in iframes * add an integration test case for nested iframes and shadow-doms * use the patch function
1 parent cf2388f commit d8f9290

File tree

7 files changed

+497
-4
lines changed

7 files changed

+497
-4
lines changed

packages/rrweb-snapshot/src/snapshot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,7 @@ export function serializeNodeWithId(
859859
recordChild = recordChild && !serializedNode.needBlock;
860860
// this property was not needed in replay side
861861
delete serializedNode.needBlock;
862+
if ((n as HTMLElement).shadowRoot) serializedNode.isShadowHost = true;
862863
}
863864
if (
864865
(serializedNode.type === NodeType.Document ||
@@ -903,7 +904,6 @@ export function serializeNodeWithId(
903904
}
904905

905906
if (isElement(n) && n.shadowRoot) {
906-
serializedNode.isShadowHost = true;
907907
for (const childN of Array.from(n.shadowRoot.childNodes)) {
908908
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
909909
if (serializedChildNode) {

packages/rrweb/src/record/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ function record<T = eventWithTime>(
273273
},
274274
onIframeLoad: (iframe, childSn) => {
275275
iframeManager.attachIframe(iframe, childSn);
276+
shadowDomManager.observeAttachShadow(
277+
(iframe as Node) as HTMLIFrameElement,
278+
);
276279
},
277280
keepIframeSrcFn,
278281
});

packages/rrweb/src/record/mutation.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ export default class MutationBuffer {
226226
}
227227

228228
public reset() {
229+
this.shadowDomManager.reset();
229230
this.canvasManager.reset();
230231
}
231232

@@ -262,10 +263,16 @@ export default class MutationBuffer {
262263
const shadowHost: Element | null = n.getRootNode
263264
? (n.getRootNode() as ShadowRoot)?.host
264265
: null;
266+
// If n is in a nested shadow dom.
267+
let rootShadowHost = shadowHost;
268+
while ((rootShadowHost?.getRootNode?.() as ShadowRoot | undefined)?.host)
269+
rootShadowHost =
270+
(rootShadowHost?.getRootNode?.() as ShadowRoot | undefined)?.host ||
271+
null;
265272
// ensure shadowHost is a Node, or doc.contains will throw an error
266273
const notInDoc =
267274
!this.doc.contains(n) &&
268-
(!(shadowHost instanceof Node) || !this.doc.contains(shadowHost));
275+
(rootShadowHost === null || !this.doc.contains(rootShadowHost));
269276
if (!n.parentNode || notInDoc) {
270277
return;
271278
}
@@ -301,6 +308,9 @@ export default class MutationBuffer {
301308
},
302309
onIframeLoad: (iframe, childSn) => {
303310
this.iframeManager.attachIframe(iframe, childSn);
311+
this.shadowDomManager.observeAttachShadow(
312+
(iframe as Node) as HTMLIFrameElement,
313+
);
304314
},
305315
});
306316
if (sn) {

packages/rrweb/src/record/shadow-dom-manager.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
SamplingStrategy,
77
} from '../types';
88
import { initMutationObserver, initScrollObserver } from './observer';
9+
import { patch } from '../utils';
910

1011
type BypassOptions = Omit<
1112
MutationBufferParam,
@@ -19,6 +20,7 @@ export class ShadowDomManager {
1920
private scrollCb: scrollCallback;
2021
private bypassOptions: BypassOptions;
2122
private mirror: Mirror;
23+
private restorePatches: (() => void)[] = [];
2224

2325
constructor(options: {
2426
mutationCb: mutationCallBack;
@@ -30,6 +32,19 @@ export class ShadowDomManager {
3032
this.scrollCb = options.scrollCb;
3133
this.bypassOptions = options.bypassOptions;
3234
this.mirror = options.mirror;
35+
36+
// Patch 'attachShadow' to observe newly added shadow doms.
37+
const manager = this;
38+
this.restorePatches.push(
39+
patch(HTMLElement.prototype, 'attachShadow', function (original) {
40+
return function () {
41+
const shadowRoot = original.apply(this, arguments);
42+
if (this.shadowRoot)
43+
manager.addShadowRoot(this.shadowRoot, this.ownerDocument);
44+
return shadowRoot;
45+
};
46+
}),
47+
);
3348
}
3449

3550
public addShadowRoot(shadowRoot: ShadowRoot, doc: Document) {
@@ -52,4 +67,36 @@ export class ShadowDomManager {
5267
mirror: this.mirror,
5368
});
5469
}
70+
71+
/**
72+
* Monkey patch 'attachShadow' of an IFrameElement to observe newly added shadow doms.
73+
*/
74+
public observeAttachShadow(iframeElement: HTMLIFrameElement) {
75+
if (iframeElement.contentWindow) {
76+
const manager = this;
77+
this.restorePatches.push(
78+
patch(
79+
(iframeElement.contentWindow as Window & {
80+
HTMLElement: { prototype: HTMLElement };
81+
}).HTMLElement.prototype,
82+
'attachShadow',
83+
function (original) {
84+
return function () {
85+
const shadowRoot = original.apply(this, arguments);
86+
if (this.shadowRoot)
87+
manager.addShadowRoot(
88+
this.shadowRoot,
89+
iframeElement.contentDocument as Document,
90+
);
91+
return shadowRoot;
92+
};
93+
},
94+
),
95+
);
96+
}
97+
}
98+
99+
public reset() {
100+
this.restorePatches.forEach((restorePatch) => restorePatch());
101+
}
55102
}

packages/rrweb/src/replay/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,8 +1421,12 @@ export class Replayer {
14211421
parent = virtualParent;
14221422
}
14231423

1424-
if (mutation.node.isShadow && hasShadowRoot(parent)) {
1425-
parent = parent.shadowRoot;
1424+
if (mutation.node.isShadow) {
1425+
// If the parent is attached a shadow dom after it's created, it won't have a shadow root.
1426+
if (!hasShadowRoot(parent)) {
1427+
((parent as Node) as HTMLElement).attachShadow({ mode: 'open' });
1428+
parent = ((parent as Node) as HTMLElement).shadowRoot!;
1429+
} else parent = parent.shadowRoot;
14261430
}
14271431

14281432
let previous: Node | null = null;

0 commit comments

Comments
 (0)