Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@ 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;
}
if (
(serializedNode.type === NodeType.Document ||
Expand Down Expand Up @@ -903,7 +904,6 @@ export function serializeNodeWithId(
}

if (isElement(n) && n.shadowRoot) {
serializedNode.isShadowHost = true;
for (const childN of Array.from(n.shadowRoot.childNodes)) {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
Expand Down
3 changes: 3 additions & 0 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ function record<T = eventWithTime>(
},
onIframeLoad: (iframe, childSn) => {
iframeManager.attachIframe(iframe, childSn);
shadowDomManager.observeAttachShadow(
(iframe as Node) as HTMLIFrameElement,
);
},
keepIframeSrcFn,
});
Expand Down
12 changes: 11 additions & 1 deletion packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ export default class MutationBuffer {
}

public reset() {
this.shadowDomManager.reset();
this.canvasManager.reset();
}

Expand Down Expand Up @@ -262,10 +263,16 @@ export default class MutationBuffer {
const shadowHost: Element | null = n.getRootNode
? (n.getRootNode() as ShadowRoot)?.host
: null;
// If n is in a nested shadow dom.
let rootShadowHost = shadowHost;
while ((rootShadowHost?.getRootNode?.() as ShadowRoot | undefined)?.host)
rootShadowHost =
(rootShadowHost?.getRootNode?.() as ShadowRoot | undefined)?.host ||
null;
// ensure shadowHost is a Node, or doc.contains will throw an error
const notInDoc =
!this.doc.contains(n) &&
(!(shadowHost instanceof Node) || !this.doc.contains(shadowHost));
(rootShadowHost === null || !this.doc.contains(rootShadowHost));
if (!n.parentNode || notInDoc) {
return;
}
Expand Down Expand Up @@ -301,6 +308,9 @@ export default class MutationBuffer {
},
onIframeLoad: (iframe, childSn) => {
this.iframeManager.attachIframe(iframe, childSn);
this.shadowDomManager.observeAttachShadow(
(iframe as Node) as HTMLIFrameElement,
);
},
});
if (sn) {
Expand Down
47 changes: 47 additions & 0 deletions packages/rrweb/src/record/shadow-dom-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SamplingStrategy,
} from '../types';
import { initMutationObserver, initScrollObserver } from './observer';
import { patch } from '../utils';

type BypassOptions = Omit<
MutationBufferParam,
Expand All @@ -19,6 +20,7 @@ export class ShadowDomManager {
private scrollCb: scrollCallback;
private bypassOptions: BypassOptions;
private mirror: Mirror;
private restorePatches: (() => void)[] = [];

constructor(options: {
mutationCb: mutationCallBack;
Expand All @@ -30,6 +32,19 @@ export class ShadowDomManager {
this.scrollCb = options.scrollCb;
this.bypassOptions = options.bypassOptions;
this.mirror = options.mirror;

// Patch 'attachShadow' to observe newly added shadow doms.
const manager = this;
this.restorePatches.push(
patch(HTMLElement.prototype, 'attachShadow', function (original) {
return function () {
const shadowRoot = original.apply(this, arguments);
if (this.shadowRoot)
manager.addShadowRoot(this.shadowRoot, this.ownerDocument);
return shadowRoot;
};
}),
);
}

public addShadowRoot(shadowRoot: ShadowRoot, doc: Document) {
Expand All @@ -52,4 +67,36 @@ export class ShadowDomManager {
mirror: this.mirror,
});
}

/**
* Monkey patch 'attachShadow' of an IFrameElement to observe newly added shadow doms.
*/
public observeAttachShadow(iframeElement: HTMLIFrameElement) {
if (iframeElement.contentWindow) {
const manager = this;
this.restorePatches.push(
patch(
(iframeElement.contentWindow as Window & {
HTMLElement: { prototype: HTMLElement };
}).HTMLElement.prototype,
'attachShadow',
function (original) {
return function () {
const shadowRoot = original.apply(this, arguments);
if (this.shadowRoot)
manager.addShadowRoot(
this.shadowRoot,
iframeElement.contentDocument as Document,
);
return shadowRoot;
};
},
),
);
}
}

public reset() {
this.restorePatches.forEach((restorePatch) => restorePatch());
}
}
8 changes: 6 additions & 2 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1421,8 +1421,12 @@ export class Replayer {
parent = virtualParent;
}

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

let previous: Node | null = null;
Expand Down
Loading