Masking: Avoid the repeated calls to closest when recursing through the DOM (#1349)

* masking performance: avoid the repeated calls to `closest` when recursing through the DOM
 - needsMask===true means that an ancestor has tested positively for masking, and so this node and all descendents should be masked
 - needsMask===false means that no ancestors have tested positively for masking, we should check each node encountered
 - needsMask===undefined means that we don't know whether ancestors are masked or not (e.g. after a mutation) and should look up the tree
* Add tests including an explicit characterData mutation tests 
* Further performance improvement: avoid calls to `el.matches` when on a leaf node, e.g. a `<br/>`
---------

Authored-by: eoghanmurray <eoghan@getthere.ie>
Based on initial PR #1338 by Alexey Babik <alexey.babik@noibu.com>
This commit is contained in:
Eoghan Murray
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 4157f28e7b
commit d1d0c7f366
6 changed files with 323 additions and 24 deletions

View File

@@ -515,6 +515,7 @@ export default class MutationBuffer {
m.target,
this.maskTextClass,
this.maskTextSelector,
true, // checkAncestors
) && value
? this.maskTextFn
? this.maskTextFn(value, closestElementOfNode(m.target))

View File

@@ -793,6 +793,243 @@ exports[`record integration tests can mask character data mutations 1`] = `
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [
{
\\"id\\": 22,
\\"value\\": \\"****** *******\\"
}
],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": []
}
}
]"
`;
exports[`record integration tests can mask character data mutations with regexp 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\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 4
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 6
},
{
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"mutation observer\\",
\\"id\\": 8
}
],
\\"id\\": 7
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 9
},
{
\\"type\\": 2,
\\"tagName\\": \\"ul\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 11
},
{
\\"type\\": 2,
\\"tagName\\": \\"li\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 12
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 13
}
],
\\"id\\": 10
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 14
},
{
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 15
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n \\",
\\"id\\": 16
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 18
}
],
\\"id\\": 17
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\",
\\"id\\": 19
}
],
\\"id\\": 5
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [
{
\\"id\\": 10,
\\"attributes\\": {
\\"class\\": \\"custom-mask\\"
}
},
{
\\"id\\": 7,
\\"attributes\\": {
\\"class\\": \\"custom-mask\\"
}
}
],
\\"removes\\": [
{
\\"parentId\\": 7,
\\"id\\": 8
}
],
\\"adds\\": [
{
\\"parentId\\": 10,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"li\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 20
}
},
{
\\"parentId\\": 20,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"*** **** ****\\",
\\"id\\": 21
}
},
{
\\"parentId\\": 7,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"*******\\",
\\"id\\": 22
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [
{
\\"id\\": 21,
\\"value\\": \\"********** ****** ** ****** *** **** ****\\"
}
],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": []
}
}
]"
`;

View File

@@ -1207,6 +1207,45 @@ describe('record integration tests', function (this: ISuite) {
p.innerText = 'mutated';
});
await page.evaluate(() => {
// generate a characterData mutation; innerText doesn't do that
const p = document.querySelector('p') as HTMLParagraphElement;
(p.childNodes[0] as Text).insertData(0, 'doubly ');
});
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('can mask character data mutations with regexp', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'mutation-observer.html', {
maskTextClass: /custom/,
}),
);
await page.evaluate(() => {
const li = document.createElement('li');
const ul = document.querySelector('ul') as HTMLUListElement;
const p = document.querySelector('p') as HTMLParagraphElement;
[ul, p].forEach((element) => {
element.className = 'custom-mask';
});
ul.appendChild(li);
li.innerText = 'new list item';
p.innerText = 'mutated';
});
await page.evaluate(() => {
// generate a characterData mutation; innerText doesn't do that
const li = document.querySelector('li:not(:empty)') as HTMLLIElement;
(li.childNodes[0] as Text).insertData(0, 'descendent should be masked ');
});
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];

View File

@@ -693,6 +693,7 @@ export function generateRecordSnippet(options: recordOptions<eventWithTime>) {
maskAllInputs: ${options.maskAllInputs},
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
userTriggeredOnInput: ${options.userTriggeredOnInput},
maskTextClass: ${options.maskTextClass},
maskTextFn: ${options.maskTextFn},
maskInputFn: ${options.maskInputFn},
recordCanvas: ${options.recordCanvas},