Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/replay-domexception-refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"rrweb": patch
"@rrweb/replay": patch
---

Make replay slightly more robust against bad mutations - replace a prior try/catch from #620 which didn't work against rrdom, and also cover appendChild failures
57 changes: 31 additions & 26 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import {
type AppendedIframe,
getBaseDimension,
hasShadowRoot,
canContainChildren,
isSerializedIframe,
getNestedRule,
getPositionsAndIndex,
Expand Down Expand Up @@ -1453,32 +1454,30 @@ export class Replayer {
}
// target may be removed with its parents before
mirror.removeNodeFromMap(target as Node & RRNode);
if (parent)
try {
parent.removeChild(target as Node & RRNode);
/**
* https://github.com/rrweb-io/rrweb/pull/887
* Remove any virtual style rules for stylesheets if a child text node is removed.
*/
if (
this.usingVirtualDom &&
target.nodeName === '#text' &&
parent.nodeName === 'STYLE' &&
(parent as RRStyleElement).rules?.length > 0
)
(parent as RRStyleElement).rules = [];
} catch (error) {
if (error instanceof DOMException) {
this.warn(
'parent could not remove child in mutation',
parent,
target,
d,
);
} else {
throw error;
}
}
if (!parent) return;
if (parent !== target.parentNode) {
this.warn(
canContainChildren(parent)
? 'parent mismatch, cannot be used to remove node in mutation'
: 'invalid parent, cannot be used to remove node in mutation',
parent,
target,
d,
);
return;
}
parent.removeChild(target as Node & RRNode);
/**
* https://github.com/rrweb-io/rrweb/pull/887
* Remove any virtual style rules for stylesheets if a child text node is removed.
*/
if (
this.usingVirtualDom &&
target.nodeName === '#text' &&
parent.nodeName === 'STYLE' &&
(parent as RRStyleElement).rules?.length > 0
)
(parent as RRStyleElement).rules = [];
});

const legacy_missingNodeMap: missingNodeMap = {
Expand Down Expand Up @@ -1517,6 +1516,12 @@ export class Replayer {
return this.newDocumentQueue.push(mutation);
}
return queue.push(mutation);
} else if (!canContainChildren(parent)) {
this.warn(
'parent is a leaf node and cannot be used to append new child in mutation',
parent,
mutation.node,
);
}

if (mutation.node.isShadow) {
Expand Down
14 changes: 14 additions & 0 deletions packages/rrweb/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,20 @@ export function hasShadowRoot<T extends Node | RRNode>(
return Boolean(dom.shadowRoot(n as unknown as Element));
}

/**
* Whether a node can contain children, i.e. is an Element, Document or
* DocumentFragment which properly support appendChild or removeChild.
* Duck typing against those methods won't work as they are in fact present
* on leaf nodes, but throw a HierarchyRequestError/DOMException when used
*/
export function canContainChildren(node: Node | RRNode): boolean {
return (
node.nodeType === Node.ELEMENT_NODE ||
node.nodeType === Node.DOCUMENT_NODE ||
node.nodeType === Node.DOCUMENT_FRAGMENT_NODE
);
}

/**
* Traverses a CSSRuleList to find a nested rule at the given position.
*
Expand Down
Loading