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
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