Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: shadow dom bugs #1049

Merged
merged 13 commits into from
Jan 10, 2023
31 changes: 31 additions & 0 deletions packages/rrweb-snapshot/test/rebuild.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,37 @@ describe('rebuild', function () {
});
});

describe('shadowDom', function () {
it('rebuild shadowRoot without 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 }';
Expand Down
8 changes: 7 additions & 1 deletion packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {
SlimDOMOptions,
createMirror,
} from 'rrweb-snapshot';
import { initObservers, mutationBuffers } from './observer';
import {
initObservers,
mutationBuffers,
processedNodeManager,
} from './observer';
import {
on,
getWindowWidth,
Expand Down Expand Up @@ -316,6 +320,7 @@ function record<T = eventWithTime>(
stylesheetManager,
canvasManager,
keepIframeSrcFn,
processedNodeManager,
},
mirror,
});
Expand Down Expand Up @@ -526,6 +531,7 @@ function record<T = eventWithTime>(
iframeManager,
stylesheetManager,
shadowDomManager,
processedNodeManager,
canvasManager,
ignoreCSSAttributes,
plugins:
Expand Down
30 changes: 16 additions & 14 deletions packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
hasShadowRoot,
isSerializedIframe,
isSerializedStylesheet,
inDom,
} from '../utils';

type DoubleLinkedListNode = {
Expand Down Expand Up @@ -175,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) {
([
Expand All @@ -198,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;
Expand Down Expand Up @@ -271,19 +274,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)
Expand Down Expand Up @@ -647,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 in other buffer, ignore it
if (this.processedNodeManager.inOtherBuffer(n, this)) return;

if (this.mirror.hasNode(n)) {
if (isIgnored(n, this.mirror)) {
return;
Expand All @@ -666,8 +661,15 @@ 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.processedNodeManager.add(childN, this);
this.genAdds(childN, n);
});
}
}
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/rrweb/src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
selectionCallback,
} from '@rrweb/types';
import MutationBuffer from './mutation';
import ProcessedNodeManager from './processed-node-manager';

type WindowWithStoredMutationObserver = IWindow & {
__rrMutationObserver?: MutationObserver;
Expand All @@ -51,6 +52,7 @@ type WindowWithAngularZone = IWindow & {
};

export const mutationBuffers: MutationBuffer[] = [];
export const processedNodeManager = new ProcessedNodeManager();

const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== 'undefined';
const isCSSMediaRuleSupported = typeof CSSMediaRule !== 'undefined';
Expand Down
34 changes: 34 additions & 0 deletions packages/rrweb/src/record/processed-node-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 nodeMap: WeakMap<Node, Set<MutationBuffer>> = new WeakMap();

constructor() {
this.periodicallyClear();
}

private periodicallyClear() {
requestAnimationFrame(() => {
this.clear();
this.periodicallyClear();
});
}

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.nodeMap.set(node, (this.nodeMap.get(node) || new Set()).add(buffer));
}

private clear() {
this.nodeMap = new WeakMap();
}
}
8 changes: 6 additions & 2 deletions packages/rrweb/src/record/shadow-dom-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
initScrollObserver,
initAdoptedStyleSheetObserver,
} from './observer';
import { patch } from '../utils';
import { patch, inDom } from '../utils';
import type { Mirror } from 'rrweb-snapshot';
import { isNativeShadowDom } from 'rrweb-snapshot';

Expand Down Expand Up @@ -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 && inDom(this))
manager.addShadowRoot(this.shadowRoot, this.ownerDocument);
return shadowRoot;
};
Expand Down
3 changes: 3 additions & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
styleSheetRuleCallback,
viewportResizeCallback,
} from '@rrweb/types';
import type ProcessedNodeManager from './record/processed-node-manager';

export type recordOptions<T> = {
emit?: (e: T, isCheckout?: boolean) => void;
Expand Down Expand Up @@ -105,6 +106,7 @@ export type observerParam = {
stylesheetManager: StylesheetManager;
shadowDomManager: ShadowDomManager;
canvasManager: CanvasManager;
processedNodeManager: ProcessedNodeManager;
ignoreCSSAttributes: Set<string>;
plugins: Array<{
observer: (
Expand Down Expand Up @@ -139,6 +141,7 @@ export type MutationBufferParam = Pick<
| 'stylesheetManager'
| 'shadowDomManager'
| 'canvasManager'
| 'processedNodeManager'
>;

export type ReplayPlugin = {
Expand Down
27 changes: 27 additions & 0 deletions packages/rrweb/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,30 @@ export class StyleSheetMirror {
return this.id++;
}
}

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));
}

export function inDom(n: Node): boolean {
const doc = n.ownerDocument;
if (!doc) return false;
return doc.contains(n) || shadowHostInDom(n);
}
Loading