Skip to content

Commit

Permalink
The split into needMaskingText/elementNeedsTextMasked was ugly and in…
Browse files Browse the repository at this point in the history
…correct as a newly added text node needs to check all ancestors as well as just the parentNode. This problem suggested a new approach which facilitated the removal of the ugly `needsMask: false` initialization:

 - needsMask===true means that an ancestor has tested positively for masking, and so this node and all descendends 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
  • Loading branch information
eoghanmurray committed Nov 8, 2023
1 parent 0dda219 commit dac89a1
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 42 deletions.
72 changes: 32 additions & 40 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,44 +306,35 @@ export function classMatchesRegex(
return classMatchesRegex(node.parentNode, regex, checkAncestors);
}

// used on newly added mutations
export function needMaskingText(
node: Text,
node: Node,
maskTextClass: string | RegExp,
maskTextSelector: string | null,
checkAncestors: boolean,
): boolean {
try {
const el = node.parentElement;
const el: HTMLElement | null =
node.nodeType === node.ELEMENT_NODE
? (node as HTMLElement)
: node.parentElement;
if (el === null) return false;
if (typeof maskTextClass === 'string') {
if (el.closest(`.${maskTextClass}`)) return true;
} else {
// TODO: this doesn't check ancestors for mutations
if (classMatchesRegex(el, maskTextClass, true)) return true;
}
if (maskTextSelector) {
if (el.closest(maskTextSelector)) return true;
}
} catch (e) {
//
}
return false;
}

// used upon first serialization
function elementNeedsTextMasked(
el: Element,
maskTextClass: string | RegExp,
maskTextSelector: string | null,
): boolean {
try {
if (typeof maskTextClass === 'string') {
if (el.classList.contains(maskTextClass)) return true;
if (checkAncestors) {
// we haven't already checked parents
if (el.closest(`.${maskTextClass}`)) return true;
} else {
if (el.classList.contains(maskTextClass)) return true;
}
} else {
if (classMatchesRegex(el, maskTextClass, true)) return true;
if (classMatchesRegex(el, maskTextClass, checkAncestors)) return true;
}
if (maskTextSelector) {
if (el.matches(maskTextSelector)) return true;
if (checkAncestors) {
// we haven't already checked parents
if (el.closest(maskTextSelector)) return true;
} else {
if (el.matches(maskTextSelector)) return true;
}
}
} catch (e) {
//
Expand Down Expand Up @@ -942,7 +933,7 @@ export function serializeNodeWithId(
inlineStylesheet: boolean;
newlyAddedElement?: boolean;
maskInputOptions?: MaskInputOptions;
needsMask: boolean;
needsMask?: boolean;
maskTextFn: MaskTextFn | undefined;
maskInputFn: MaskInputFn | undefined;
slimDOMOptions: SlimDOMOptions;
Expand Down Expand Up @@ -990,6 +981,18 @@ export function serializeNodeWithId(
} = options;
let { needsMask } = options;
let { preserveWhiteSpace = true } = options;

if (!needsMask) {
// perf: if needsMask = true, children won't also need to check
let checkAncestors = needsMask === undefined; // if false, we've already checked ancestors
needsMask = needMaskingText(
n as Element,
maskTextClass,
maskTextSelector,
checkAncestors,
);
}

const _serializedNode = serializeNode(n, {
doc,
mirror,
Expand Down Expand Up @@ -1047,16 +1050,6 @@ export function serializeNodeWithId(
const shadowRoot = (n as HTMLElement).shadowRoot;
if (shadowRoot && isNativeShadowDom(shadowRoot))
serializedNode.isShadowHost = true;
if (!needsMask) {
// we've already serialized this Element, but that's okay as masking
// is only relevant for child Text nodes.
// perf: if true, children won't also need to check
needsMask = elementNeedsTextMasked(
n as Element,
maskTextClass,
maskTextSelector,
);
}
}
if (
(serializedNode.type === NodeType.Document ||
Expand Down Expand Up @@ -1327,7 +1320,6 @@ function snapshot(
skipChild: false,
inlineStylesheet,
maskInputOptions,
needsMask: false,
maskTextFn,
maskInputFn,
slimDOMOptions,
Expand Down
3 changes: 1 addition & 2 deletions packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,6 @@ export default class MutationBuffer {
mirror: this.mirror,
blockClass: this.blockClass,
blockSelector: this.blockSelector,
needsMask: false,
maskTextClass: this.maskTextClass,
maskTextSelector: this.maskTextSelector,
skipChild: true,
Expand Down Expand Up @@ -513,7 +512,7 @@ export default class MutationBuffer {
this.texts.push({
value:
needMaskingText(
m.target as Text,
m.target,
this.maskTextClass,
this.maskTextSelector,
) && value
Expand Down
203 changes: 203 additions & 0 deletions packages/rrweb/test/__snapshots__/integration.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,209 @@ exports[`record integration tests can mask character data mutations 1`] = `
]"
`;

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\\": 7,
\\"attributes\\": {
\\"class\\": \\"custom-mask\\"
}
}
],
\\"removes\\": [
{
\\"parentId\\": 7,
\\"id\\": 8
}
],
\\"adds\\": [
{
\\"parentId\\": 10,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"li\\",
\\"attributes\\": {
\\"class\\": \\"custom-mask\\"
},
\\"childNodes\\": [],
\\"id\\": 20
}
},
{
\\"parentId\\": 20,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"*** **** ****\\",
\\"id\\": 21
}
},
{
\\"parentId\\": 7,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"*******\\",
\\"id\\": 22
}
}
]
}
}
]"
`;

exports[`record integration tests can record attribute mutation 1`] = `
"[
{
Expand Down
27 changes: 27 additions & 0 deletions packages/rrweb/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,33 @@ describe('record integration tests', function (this: ISuite) {
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;
[li, p].forEach((element) => {
element.className = 'custom-mask';
});
ul.appendChild(li);
li.innerText = 'new list item';
p.innerText = 'mutated';
});

const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});

it('should record after DOMContentLoaded event', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
Expand Down
1 change: 1 addition & 0 deletions packages/rrweb/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down

0 comments on commit dac89a1

Please sign in to comment.