Keep blocked root elements as placeholders (#696)

* Keep blocked root elements as placeholders

`serializeNode` turns blocked elements into placeholder nodes so we need to make sure we don't remove these elements from the mutations when they get added.
We do however need to keep removing any children of these blocked elements from getting added or mutated.

* Update packages/rrweb/src/record/mutation.ts
This commit is contained in:
Justin Halsall
2026-04-01 12:00:00 +08:00
committed by GitHub
parent d50094272e
commit bab5526dd5
3 changed files with 236 additions and 13 deletions

View File

@@ -262,9 +262,6 @@ export default class MutationBuffer {
ns = ns && ns.nextSibling;
nextId = ns && this.mirror.getId((ns as unknown) as INode);
}
if (nextId === -1 && isBlocked(n.nextSibling, this.blockClass)) {
nextId = null;
}
return nextId;
};
const pushAdd = (n: Node) => {
@@ -534,11 +531,7 @@ export default class MutationBuffer {
const parentId = isShadowRoot(m.target)
? this.mirror.getId((m.target.host as unknown) as INode)
: this.mirror.getId(m.target as INode);
if (
isBlocked(n, this.blockClass) ||
isBlocked(m.target, this.blockClass) ||
isIgnored(n)
) {
if (isBlocked(m.target, this.blockClass) || isIgnored(n)) {
return;
}
// removed node has not been serialized yet, just remove it from the Set
@@ -582,9 +575,7 @@ export default class MutationBuffer {
};
private genAdds = (n: Node | INode, target?: Node | INode) => {
if (isBlocked(n, this.blockClass)) {
return;
}
// parent was blocked, so we can ignore this node
if (target && isBlocked(target, this.blockClass)) {
return;
}
@@ -604,7 +595,11 @@ export default class MutationBuffer {
this.addedSet.add(n);
this.droppedSet.delete(n);
}
n.childNodes.forEach((childN) => this.genAdds(childN));
// 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))
n.childNodes.forEach((childN) => this.genAdds(childN));
};
}

View File

@@ -332,6 +332,212 @@ exports[`block 1`] = `
]"
`;
exports[`block 2 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {}
},
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 1,
\\"name\\": \\"html\\",
\\"publicId\\": \\"\\",
\\"systemId\\": \\"\\",
\\"id\\": 2
},
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {
\\"lang\\": \\"en\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 5
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"charset\\": \\"UTF-8\\"
},
\\"childNodes\\": [],
\\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 7
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"name\\": \\"viewport\\",
\\"content\\": \\"width=device-width, initial-scale=1.0\\"
},
\\"childNodes\\": [],
\\"id\\": 8
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 9
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"http-equiv\\": \\"X-UA-Compatible\\",
\\"content\\": \\"ie=edge\\"
},
\\"childNodes\\": [],
\\"id\\": 10
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 11
},
{
\\"type\\": 2,
\\"tagName\\": \\"title\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"Block record\\",
\\"id\\": 13
}
],
\\"id\\": 12
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 14
}
],
\\"id\\": 4
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 15
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 17
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"class\\": \\"rr-block\\",
\\"rr_width\\": \\"50px\\",
\\"rr_height\\": \\"50px\\"
},
\\"childNodes\\": [],
\\"id\\": 18
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 19
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 21
}
],
\\"id\\": 20
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
\\"id\\": 22
}
],
\\"id\\": 16
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 16,
\\"nextId\\": 18,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"button\\",
\\"attributes\\": {
\\"class\\": \\"rr-block\\",
\\"rr_width\\": \\"100px\\",
\\"rr_height\\": \\"100px\\"
},
\\"childNodes\\": [],
\\"id\\": 23
}
}
]
}
}
]"
`;
exports[`canvas 1`] = `
"[
{

View File

@@ -185,7 +185,9 @@ describe('record integration tests', function (this: ISuite) {
// toggle the select box
await page.click('.select2-container', { clickCount: 2, delay: 100 });
// test storage of !important style
await page.evaluate('document.getElementById("select2-drop").setAttribute("style", document.getElementById("select2-drop").style.cssText + "color:black !important")');
await page.evaluate(
'document.getElementById("select2-drop").setAttribute("style", document.getElementById("select2-drop").style.cssText + "color:black !important")',
);
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'select2');
});
@@ -317,6 +319,26 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots, __filename, 'block');
});
it('should not record blocked elements dynamically added', async () => {
const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'block.html'));
await page.evaluate(() => {
const el = document.createElement('button');
el.className = 'rr-block';
el.style.width = '100px';
el.style.height = '100px';
el.innerText = 'Should not be recorded';
const nextElement = document.querySelector('.rr-block')!;
nextElement.parentNode!.insertBefore(el, nextElement);
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'block 2');
});
it('should record DOM node movement 1', async () => {
const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank');