From e4cb91e1e6f4f2ceef360b7b6b47a331395cc2cf Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] fix block strategy If an element was blocked, its child nodes should also be blocked. The interactions and mutations on the element and its child nodes also need to be blocked. --- src/record/observer.ts | 34 +++- src/utils.ts | 14 ++ test/__snapshots__/integration.test.ts.snap | 184 ++++++++++++++++++++ test/html/block.html | 12 ++ test/integration.test.ts | 18 ++ 5 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 test/html/block.html diff --git a/src/record/observer.ts b/src/record/observer.ts index 53622267..c3112532 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -6,6 +6,7 @@ import { hookSetter, getWindowHeight, getWindowWidth, + isBlocked, } from '../utils'; import { mutationCallBack, @@ -53,6 +54,9 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver { const addsSet = new Set(); const genAdds = (n: Node) => { + if (isBlocked(n)) { + return; + } addsSet.add(n); n.childNodes.forEach(childN => genAdds(childN)); }; @@ -68,7 +72,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver { switch (type) { case 'characterData': { const value = target.textContent; - if (value !== oldValue) { + if (!isBlocked(target) && value !== oldValue) { texts.push({ value, node: target, @@ -78,7 +82,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver { } case 'attributes': { const value = (target as HTMLElement).getAttribute(attributeName!); - if (value === oldValue) { + if (isBlocked(target) || value === oldValue) { return; } let item: attributeCursor | undefined = attributes.find( @@ -93,10 +97,14 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver { } // overwrite attribute if the mutations was triggered in same time item.attributes[attributeName!] = value; + break; } case 'childList': { addedNodes.forEach(n => genAdds(n)); removedNodes.forEach(n => { + if (isBlocked(n)) { + return; + } // removed node has not been serialized yet, just remove it from the Set if (addsSet.has(n)) { addsSet.delete(n); @@ -171,7 +179,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver { } }); - cb({ + const payload = { texts: texts .map(text => ({ id: mirror.getId(text.node as INode), @@ -188,7 +196,17 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver { .filter(attribute => mirror.has(attribute.id)), removes, adds, - }); + }; + // payload may be empty if the mutations happened in some blocked elements + if ( + !payload.texts.length && + !payload.attributes.length && + !payload.removes.length && + !payload.adds.length + ) { + return; + } + cb(payload); }); observer.observe(document, { attributes: true, @@ -243,6 +261,9 @@ function initMouseInteractionObserver( const handlers: listenerHandler[] = []; const getHandler = (eventKey: keyof typeof MouseInteractions) => { return (event: MouseEvent) => { + if (isBlocked(event.target as Node)) { + return; + } const id = mirror.getId(event.target as INode); const { clientX, clientY } = event; cb({ @@ -267,7 +288,7 @@ function initMouseInteractionObserver( function initScrollObserver(cb: scrollCallback): listenerHandler { const updatePosition = throttle(evt => { - if (!evt.target) { + if (!evt.target || isBlocked(evt.target as Node)) { return; } const id = mirror.getId(evt.target as INode); @@ -318,7 +339,8 @@ function initInputObserver(cb: inputCallback): listenerHandler { if ( !target || !(target as Element).tagName || - INPUT_TAGS.indexOf((target as Element).tagName) < 0 + INPUT_TAGS.indexOf((target as Element).tagName) < 0 || + isBlocked(target as Node) ) { return; } diff --git a/src/utils.ts b/src/utils.ts index 4d588cfd..b770259e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -112,3 +112,17 @@ export function getWindowWidth(): number { (document.body && document.body.clientWidth) ); } + +const BLOCK_CLASS = 'rr-block'; +export function isBlocked(node: Node | null): boolean { + if (!node) { + return false; + } + if (node.nodeType === node.ELEMENT_NODE) { + return ( + (node as HTMLElement).classList.contains(BLOCK_CLASS) || + isBlocked(node.parentNode) + ); + } + return isBlocked(node.parentNode); +} diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index 7d9ebb6e..4d42c7d6 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -157,6 +157,190 @@ exports[`attributes 1`] = ` ]" `; +exports[`block 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {}, + \\"timestamp\\": 1542268800000 + }, + { + \\"type\\": 1, + \\"data\\": {}, + \\"timestamp\\": 1542268800000 + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + }, + \\"timestamp\\": 1542268800000 + }, + { + \\"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\\": \\"1904px\\", + \\"rr_height\\": \\"21px\\" + }, + \\"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 + } + }, + \\"timestamp\\": 1542268800000 + } +]" +`; + exports[`character-data 1`] = ` "[ { diff --git a/test/html/block.html b/test/html/block.html new file mode 100644 index 00000000..84088997 --- /dev/null +++ b/test/html/block.html @@ -0,0 +1,12 @@ + + + + + + + Block record + + +
+ + diff --git a/test/integration.test.ts b/test/integration.test.ts index 46458461..8f062946 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -252,4 +252,22 @@ describe('record integration tests', () => { ); assert(result.pass, result.pass ? '' : result.report()); }); + + it('should not record blocked elements and its child nodes', async () => { + const page: puppeteer.Page = await this.browser.newPage(); + await page.goto('about:blank'); + await page.setContent(getHtml.call(this, 'block.html')); + + await page.type('input', 'should not be record'); + await page.evaluate(`document.getElementById('text').innerText = '1'`); + await page.click('#text'); + + const snapshots = await page.evaluate('window.snapshots'); + const result = matchSnapshot( + stringifySnapshots(snapshots), + __filename, + 'block', + ); + assert(result.pass, result.pass ? '' : result.report()); + }); });