Skip to content

Commit

Permalink
impl shadow DOM manager
Browse files Browse the repository at this point in the history
part of #38
1. observe DOM mutations in shadow DOM
2. rebuild DOM mutations in shadow DOM
  • Loading branch information
Yuyz0112 committed Mar 28, 2021
1 parent e3f9a4d commit df7537b
Show file tree
Hide file tree
Showing 12 changed files with 680 additions and 48 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@
"@xstate/fsm": "^1.4.0",
"fflate": "^0.4.4",
"mitt": "^1.1.3",
"rrweb-snapshot": "^1.0.7"
"rrweb-snapshot": "^1.1.1"
}
}
54 changes: 34 additions & 20 deletions src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getWindowHeight,
polyfill,
isIframeINode,
hasShadowRoot,
} from '../utils';
import {
EventType,
Expand All @@ -16,8 +17,10 @@ import {
IncrementalSource,
listenerHandler,
LogRecordOptions,
mutationCallbackParam,
} from '../types';
import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager';

function wrapEvent(e: event): eventWithTime {
return {
Expand Down Expand Up @@ -179,17 +182,33 @@ function record<T = eventWithTime>(
}
};

const wrappedMutationEmit = (m: mutationCallbackParam) => {
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
...m,
},
}),
);
};

const iframeManager = new IframeManager({
mutationCb: (m) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
...m,
},
}),
),
mutationCb: wrappedMutationEmit,
});

const shadowDomManager = new ShadowDomManager({
mutationCb: wrappedMutationEmit,
bypassOptions: {
blockClass,
blockSelector,
inlineStylesheet,
maskInputOptions,
recordCanvas,
slimDOMOptions,
iframeManager,
},
});

takeFullSnapshot = (isCheckout = false) => {
Expand Down Expand Up @@ -217,6 +236,9 @@ function record<T = eventWithTime>(
if (isIframeINode(n)) {
iframeManager.addIframe(n);
}
if (hasShadowRoot(n)) {
shadowDomManager.addShadowRoot(n.shadowRoot, document);
}
},
onIframeLoad: (iframe, childSn) => {
iframeManager.attachIframe(iframe, childSn);
Expand Down Expand Up @@ -271,16 +293,7 @@ function record<T = eventWithTime>(
const observe = (doc: Document) => {
return initObservers(
{
mutationCb: (m) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
...m,
},
}),
),
mutationCb: wrappedMutationEmit,
mousemoveCb: (positions, source) =>
wrappedEmit(
wrapEvent({
Expand Down Expand Up @@ -394,6 +407,7 @@ function record<T = eventWithTime>(
blockSelector,
slimDOMOptions,
iframeManager,
shadowDomManager,
},
hooks,
);
Expand Down
41 changes: 29 additions & 12 deletions src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
MaskInputOptions,
SlimDOMOptions,
IGNORED_NODE,
NodeType,
isShadowRoot,
} from 'rrweb-snapshot';
import {
mutationRecord,
Expand All @@ -16,8 +16,16 @@ import {
removedNodeMutation,
addedNodeMutation,
} from '../types';
import { mirror, isBlocked, isAncestorRemoved, isIgnored } from '../utils';
import {
mirror,
isBlocked,
isAncestorRemoved,
isIgnored,
isIframeINode,
hasShadowRoot,
} from '../utils';
import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager';

type DoubleLinkedListNode = {
previous: DoubleLinkedListNode | null;
Expand Down Expand Up @@ -158,6 +166,7 @@ export default class MutationBuffer {
private doc: Document;

private iframeManager: IframeManager;
private shadowDomManager: ShadowDomManager;

public init(
cb: mutationCallBack,
Expand All @@ -169,6 +178,7 @@ export default class MutationBuffer {
slimDOMOptions: SlimDOMOptions,
doc: Document,
iframeManager: IframeManager,
shadowDomManager: ShadowDomManager,
) {
this.blockClass = blockClass;
this.blockSelector = blockSelector;
Expand All @@ -179,6 +189,7 @@ export default class MutationBuffer {
this.emissionCallback = cb;
this.doc = doc;
this.iframeManager = iframeManager;
this.shadowDomManager = shadowDomManager;
}

public freeze() {
Expand Down Expand Up @@ -236,10 +247,14 @@ export default class MutationBuffer {
return nextId;
};
const pushAdd = (n: Node) => {
if (!n.parentNode || !this.doc.contains(n)) {
const shadowHost: Element | null = (n.getRootNode() as ShadowRoot)?.host;
const notInDoc = !this.doc.contains(n) && !this.doc.contains(shadowHost);
if (!n.parentNode || notInDoc) {
return;
}
const parentId = mirror.getId((n.parentNode as Node) as INode);
const parentId = isShadowRoot(n.parentNode)
? mirror.getId((shadowHost as unknown) as INode)
: mirror.getId((n.parentNode as Node) as INode);
const nextId = getNextId(n);
if (parentId === -1 || nextId === -1) {
return addList.addNode(n);
Expand All @@ -255,13 +270,11 @@ export default class MutationBuffer {
slimDOMOptions: this.slimDOMOptions,
recordCanvas: this.recordCanvas,
onSerialize: (currentN) => {
if (
currentN.__sn.type === NodeType.Element &&
currentN.__sn.tagName === 'iframe'
) {
this.iframeManager.addIframe(
(currentN as unknown) as HTMLIFrameElement,
);
if (isIframeINode(currentN)) {
this.iframeManager.addIframe(currentN);
}
if (hasShadowRoot(n)) {
this.shadowDomManager.addShadowRoot(n.shadowRoot, document);
}
},
onIframeLoad: (iframe, childSn) => {
Expand Down Expand Up @@ -418,6 +431,7 @@ export default class MutationBuffer {
// overwrite attribute if the mutations was triggered in same time
item.attributes[m.attributeName!] = transformAttribute(
this.doc,
(m.target as HTMLElement).tagName,
m.attributeName!,
value!,
);
Expand All @@ -427,7 +441,9 @@ export default class MutationBuffer {
m.addedNodes.forEach((n) => this.genAdds(n, m.target));
m.removedNodes.forEach((n) => {
const nodeId = mirror.getId(n as INode);
const parentId = mirror.getId(m.target as INode);
const parentId = isShadowRoot(m.target)
? mirror.getId((m.target.host as unknown) as INode)
: mirror.getId(m.target as INode);
if (
isBlocked(n, this.blockClass) ||
isBlocked(m.target, this.blockClass) ||
Expand Down Expand Up @@ -463,6 +479,7 @@ export default class MutationBuffer {
this.removes.push({
parentId,
id: nodeId,
isShadow: isShadowRoot(m.target) ? true : undefined,
});
}
this.mapRemoves.push(n);
Expand Down
10 changes: 8 additions & 2 deletions src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
import MutationBuffer from './mutation';
import { stringify } from './stringify';
import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager';

type WindowWithStoredMutationObserver = Window & {
__rrMutationObserver?: MutationObserver;
Expand All @@ -56,7 +57,7 @@ type WindowWithAngularZone = Window & {

export const mutationBuffers: MutationBuffer[] = [];

function initMutationObserver(
export function initMutationObserver(
cb: mutationCallBack,
doc: Document,
blockClass: blockClass,
Expand All @@ -66,6 +67,8 @@ function initMutationObserver(
recordCanvas: boolean,
slimDOMOptions: SlimDOMOptions,
iframeManager: IframeManager,
shadowDomManager: ShadowDomManager,
rootEl: Node,
): MutationObserver {
const mutationBuffer = new MutationBuffer();
mutationBuffers.push(mutationBuffer);
Expand All @@ -80,6 +83,7 @@ function initMutationObserver(
slimDOMOptions,
doc,
iframeManager,
shadowDomManager,
);
let mutationObserverCtor =
window.MutationObserver ||
Expand Down Expand Up @@ -109,7 +113,7 @@ function initMutationObserver(
const observer = new mutationObserverCtor(
mutationBuffer.processMutations.bind(mutationBuffer),
);
observer.observe(doc, {
observer.observe(rootEl, {
attributes: true,
attributeOldValue: true,
characterData: true,
Expand Down Expand Up @@ -763,6 +767,8 @@ export function initObservers(
o.recordCanvas,
o.slimDOMOptions,
o.iframeManager,
o.shadowDomManager,
o.doc,
);
const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.sampling, o.doc);
const mouseInteractionHandler = initMouseInteractionObserver(
Expand Down
43 changes: 43 additions & 0 deletions src/record/shadow-dom-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { mutationCallBack, blockClass } from '../types';
import { MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot';
import { IframeManager } from './iframe-manager';
import { initMutationObserver } from './observer';

type BypassOptions = {
blockClass: blockClass;
blockSelector: string | null;
inlineStylesheet: boolean;
maskInputOptions: MaskInputOptions;
recordCanvas: boolean;
slimDOMOptions: SlimDOMOptions;
iframeManager: IframeManager;
};

export class ShadowDomManager {
private mutationCb: mutationCallBack;
private bypassOptions: BypassOptions;

constructor(options: {
mutationCb: mutationCallBack;
bypassOptions: BypassOptions;
}) {
this.mutationCb = options.mutationCb;
this.bypassOptions = options.bypassOptions;
}

public addShadowRoot(shadowRoot: ShadowRoot, doc: Document) {
initMutationObserver(
this.mutationCb,
doc,

This comment has been minimized.

Copy link
@eoghanmurray

eoghanmurray Sep 27, 2021

Contributor

Why are we passing in doc here (the parent document)?
My naive assumption is that we need to add an observer which observes the shadow document (i.e. pass shoadowRoot instead of doc here).

This comment has been minimized.

Copy link
@Yuyz0112

Yuyz0112 Sep 29, 2021

Author Member

The last parameter of initMutationObserver is root element, which we passed shadowRoot in.

I think I've met some issues when using shadowRoot as doc, but not quite sure here. Maybe this is a bug.

this.bypassOptions.blockClass,
this.bypassOptions.blockSelector,
this.bypassOptions.inlineStylesheet,
this.bypassOptions.maskInputOptions,
this.bypassOptions.recordCanvas,
this.bypassOptions.slimDOMOptions,
this.bypassOptions.iframeManager,
this,
shadowRoot,
);
}
}
15 changes: 12 additions & 3 deletions src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
AppendedIframe,
isIframeINode,
getBaseDimension,
hasShadowRoot,
} from '../utils';
import getInjectStyleRules from './styles/inject-style';
import './styles/style.css';
Expand Down Expand Up @@ -1048,14 +1049,18 @@ export class Replayer {
if (!target) {
return this.warnNodeNotFound(d, mutation.id);
}
const parent = mirror.getNode(mutation.parentId);
let parent: INode | null | ShadowRoot = mirror.getNode(mutation.parentId);
if (!parent) {
return this.warnNodeNotFound(d, mutation.parentId);
}
if (mutation.isShadow && hasShadowRoot(parent)) {
parent = parent.shadowRoot;
}
// target may be removed with its parents before
mirror.removeNodeFromMap(target);
if (parent) {
const realParent = this.fragmentParentMap.get(parent);
const realParent =
'__sn' in parent ? this.fragmentParentMap.get(parent) : undefined;
if (realParent && realParent.contains(target)) {
realParent.removeChild(target);
} else if (this.fragmentParentMap.has(target)) {
Expand Down Expand Up @@ -1100,7 +1105,7 @@ export class Replayer {
if (!this.iframe.contentDocument) {
return console.warn('Looks like your replayer has been destroyed.');
}
let parent = mirror.getNode(mutation.parentId);
let parent: INode | null | ShadowRoot = mirror.getNode(mutation.parentId);
if (!parent) {
if (mutation.node.type === NodeType.Document) {
// is newly added document, maybe the document node of an iframe
Expand Down Expand Up @@ -1133,6 +1138,10 @@ export class Replayer {
parent = virtualParent;
}

if (mutation.node.isShadow && hasShadowRoot(parent)) {
parent = parent.shadowRoot;
}

let previous: Node | null = null;
let next: Node | null = null;
if (mutation.previousId) {
Expand Down
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { PackFn, UnpackFn } from './packer/base';
import { FontFaceDescriptors } from 'css-font-loading-module';
import { IframeManager } from './record/iframe-manager';
import { ShadowDomManager } from './record/shadow-dom-manager';

export enum EventType {
DomContentLoaded,
Expand Down Expand Up @@ -231,6 +232,7 @@ export type observerParam = {
slimDOMOptions: SlimDOMOptions;
doc: Document;
iframeManager: IframeManager;
shadowDomManager: ShadowDomManager;
};

export type hooksParam = {
Expand Down Expand Up @@ -282,6 +284,7 @@ export type attributeMutation = {
export type removedNodeMutation = {
parentId: number;
id: number;
isShadow?: boolean;
};

export type addedNodeMutation = {
Expand All @@ -292,7 +295,7 @@ export type addedNodeMutation = {
node: serializedNodeWithId;
};

type mutationCallbackParam = {
export type mutationCallbackParam = {
texts: textMutation[];
attributes: attributeMutation[];
removes: removedNodeMutation[];
Expand Down
Loading

0 comments on commit df7537b

Please sign in to comment.