Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/improve custom mask text selector and mask text fn #1212

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions packages/rrweb-snapshot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import snapshot, {
classMatchesRegex,
IGNORED_NODE,
genId,
getMatchedCustomMaskTextFn,
} from './snapshot';
import rebuild, {
buildNodeWithSN,
Expand All @@ -32,4 +33,5 @@ export {
classMatchesRegex,
IGNORED_NODE,
genId,
getMatchedCustomMaskTextFn,
};
60 changes: 56 additions & 4 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
getInputType,
} from './utils';

import { maskTextRule } from '@rrweb/types';

let _id = 1;
const tagNameRegex = new RegExp('[^a-z0-9-_:]');

Expand Down Expand Up @@ -317,6 +319,7 @@ export function needMaskingText(
node: Node,
maskTextClass: string | RegExp,
maskTextSelector: string | null,
customMaskTextRule: maskTextRule[],
): boolean {
try {
const el: HTMLElement | null =
Expand All @@ -336,12 +339,40 @@ export function needMaskingText(
if (el.matches(maskTextSelector)) return true;
if (el.closest(maskTextSelector)) return true;
}
if (customMaskTextRule) {
for (let rule of customMaskTextRule) {
if (el.matches(rule.cssSelector)) return true;
if (el.closest(rule.cssSelector)) return true;
}
}
} catch (e) {
//
}
return false;
}

export function getMatchedCustomMaskTextFn(
node: Node,
customMaskTextRule: maskTextRule[],
): ((originText: string) => string) | null {
try {
const el: HTMLElement | null =
node.nodeType === node.ELEMENT_NODE
? (node as HTMLElement)
: node.parentElement;
if (el === null) return null;

for (let rule of customMaskTextRule) {
if (el.matches(rule.cssSelector)) return rule.maskFn;
if (el.closest(rule.cssSelector)) return rule.maskFn;
}
} catch (error) {
return null;
}

return null;
}

// https://stackoverflow.com/a/36155560
function onceIframeLoaded(
iframeEl: HTMLIFrameElement,
Expand Down Expand Up @@ -435,6 +466,7 @@ function serializeNode(
blockSelector: string | null;
maskTextClass: string | RegExp;
maskTextSelector: string | null;
customMaskTextRule: maskTextRule[];
inlineStylesheet: boolean;
maskInputOptions: MaskInputOptions;
maskTextFn: MaskTextFn | undefined;
Expand All @@ -456,6 +488,7 @@ function serializeNode(
blockSelector,
maskTextClass,
maskTextSelector,
customMaskTextRule,
inlineStylesheet,
maskInputOptions = {},
maskTextFn,
Expand Down Expand Up @@ -509,6 +542,7 @@ function serializeNode(
return serializeTextNode(n as Text, {
maskTextClass,
maskTextSelector,
customMaskTextRule,
maskTextFn,
rootId,
});
Expand Down Expand Up @@ -540,11 +574,18 @@ function serializeTextNode(
options: {
maskTextClass: string | RegExp;
maskTextSelector: string | null;
customMaskTextRule: maskTextRule[];
maskTextFn: MaskTextFn | undefined;
rootId: number | undefined;
},
): serializedNode {
const { maskTextClass, maskTextSelector, maskTextFn, rootId } = options;
const {
maskTextClass,
maskTextSelector,
customMaskTextRule,
maskTextFn,
rootId,
} = options;
// The parent node may not be a html element which has a tagName attribute.
// So just let it be undefined which is ok in this use case.
const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName;
Expand Down Expand Up @@ -579,10 +620,12 @@ function serializeTextNode(
!isStyle &&
!isScript &&
textContent &&
needMaskingText(n, maskTextClass, maskTextSelector)
needMaskingText(n, maskTextClass, maskTextSelector, customMaskTextRule)
) {
textContent = maskTextFn
? maskTextFn(textContent)
const customMaskFn = getMatchedCustomMaskTextFn(n, customMaskTextRule);
const maskFn = customMaskFn ?? maskTextFn;
textContent = maskFn
? maskFn(textContent)
: textContent.replace(/[\S]/g, '*');
}

Expand Down Expand Up @@ -930,6 +973,7 @@ export function serializeNodeWithId(
blockSelector: string | null;
maskTextClass: string | RegExp;
maskTextSelector: string | null;
customMaskTextRule: maskTextRule[];
skipChild: boolean;
inlineStylesheet: boolean;
newlyAddedElement?: boolean;
Expand Down Expand Up @@ -962,6 +1006,7 @@ export function serializeNodeWithId(
blockSelector,
maskTextClass,
maskTextSelector,
customMaskTextRule,
skipChild = false,
inlineStylesheet = true,
maskInputOptions = {},
Expand All @@ -987,6 +1032,7 @@ export function serializeNodeWithId(
blockSelector,
maskTextClass,
maskTextSelector,
customMaskTextRule,
inlineStylesheet,
maskInputOptions,
maskTextFn,
Expand Down Expand Up @@ -1059,6 +1105,7 @@ export function serializeNodeWithId(
blockSelector,
maskTextClass,
maskTextSelector,
customMaskTextRule,
skipChild,
inlineStylesheet,
maskInputOptions,
Expand Down Expand Up @@ -1119,6 +1166,7 @@ export function serializeNodeWithId(
blockSelector,
maskTextClass,
maskTextSelector,
customMaskTextRule,
skipChild: false,
inlineStylesheet,
maskInputOptions,
Expand Down Expand Up @@ -1166,6 +1214,7 @@ export function serializeNodeWithId(
blockSelector,
maskTextClass,
maskTextSelector,
customMaskTextRule,
skipChild: false,
inlineStylesheet,
maskInputOptions,
Expand Down Expand Up @@ -1207,6 +1256,7 @@ function snapshot(
blockSelector?: string | null;
maskTextClass?: string | RegExp;
maskTextSelector?: string | null;
customMaskTextRule?: maskTextRule[];
inlineStylesheet?: boolean;
maskAllInputs?: boolean | MaskInputOptions;
maskTextFn?: MaskTextFn;
Expand Down Expand Up @@ -1236,6 +1286,7 @@ function snapshot(
blockSelector = null,
maskTextClass = 'rr-mask',
maskTextSelector = null,
customMaskTextRule = [],
inlineStylesheet = true,
inlineImages = false,
recordCanvas = false,
Expand Down Expand Up @@ -1302,6 +1353,7 @@ function snapshot(
blockSelector,
maskTextClass,
maskTextSelector,
customMaskTextRule,
skipChild: false,
inlineStylesheet,
maskInputOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ exports[`integration tests [html file]: picture-in-frame.html 1`] = `
</body></html>"
`;

exports[`integration tests [html file]: picture-with-inline-onload.html 1`] = `
"<html xmlns=\\"http://www.w3.org/1999/xhtml\\"><head></head><body>
<img src=\\"http://localhost:3030/images/robot.png\\" alt=\\"This is a robot\\" style=\\"opacity: 1;\\" _onload=\\"this.style.opacity=1\\" />
</body></html>"
`;

exports[`integration tests [html file]: preload.html 1`] = `
"<!DOCTYPE html><html lang=\\"en\\"><head>
<meta charset=\\"UTF-8\\" />
Expand Down
2 changes: 2 additions & 0 deletions packages/rrweb-snapshot/test/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ describe('style elements', () => {
blockSelector: null,
maskTextClass: 'maskmask',
maskTextSelector: null,
customMaskTextRule: [],
skipChild: false,
inlineStylesheet: true,
maskTextFn: undefined,
Expand Down Expand Up @@ -190,6 +191,7 @@ describe('scrollTop/scrollLeft', () => {
blockSelector: null,
maskTextClass: 'maskmask',
maskTextSelector: null,
customMaskTextRule: [],
skipChild: false,
inlineStylesheet: true,
maskTextFn: undefined,
Expand Down
4 changes: 4 additions & 0 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ function record<T = eventWithTime>(
ignoreClass = 'rr-ignore',
maskTextClass = 'rr-mask',
maskTextSelector = null,
customMaskTextRule = [],
inlineStylesheet = true,
maskAllInputs,
maskInputOptions: _maskInputOptions,
Expand Down Expand Up @@ -324,6 +325,7 @@ function record<T = eventWithTime>(
blockSelector,
maskTextClass,
maskTextSelector,
customMaskTextRule,
inlineStylesheet,
maskInputOptions,
dataURLOptions,
Expand Down Expand Up @@ -367,6 +369,7 @@ function record<T = eventWithTime>(
blockSelector,
maskTextClass,
maskTextSelector,
customMaskTextRule,
inlineStylesheet,
maskAllInputs: maskInputOptions,
maskTextFn,
Expand Down Expand Up @@ -523,6 +526,7 @@ function record<T = eventWithTime>(
ignoreClass,
maskTextClass,
maskTextSelector,
customMaskTextRule,
maskInputOptions,
inlineStylesheet,
sampling,
Expand Down
31 changes: 21 additions & 10 deletions packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ignoreAttribute,
isShadowRoot,
needMaskingText,
getMatchedCustomMaskTextFn,
maskInputValue,
Mirror,
isNativeShadowDom,
Expand Down Expand Up @@ -164,6 +165,7 @@ export default class MutationBuffer {
private blockSelector: observerParam['blockSelector'];
private maskTextClass: observerParam['maskTextClass'];
private maskTextSelector: observerParam['maskTextSelector'];
private customMaskTextRule: observerParam['customMaskTextRule'];
private inlineStylesheet: observerParam['inlineStylesheet'];
private maskInputOptions: observerParam['maskInputOptions'];
private maskTextFn: observerParam['maskTextFn'];
Expand All @@ -189,6 +191,7 @@ export default class MutationBuffer {
'blockSelector',
'maskTextClass',
'maskTextSelector',
'customMaskTextRule',
'inlineStylesheet',
'maskInputOptions',
'maskTextFn',
Expand Down Expand Up @@ -290,6 +293,7 @@ export default class MutationBuffer {
blockSelector: this.blockSelector,
maskTextClass: this.maskTextClass,
maskTextSelector: this.maskTextSelector,
customMaskTextRule: this.customMaskTextRule,
skipChild: true,
newlyAddedElement: true,
inlineStylesheet: this.inlineStylesheet,
Expand Down Expand Up @@ -469,17 +473,24 @@ export default class MutationBuffer {
!isBlocked(m.target, this.blockClass, this.blockSelector, false) &&
value !== m.oldValue
) {
let textValue = value;
if (
needMaskingText(
m.target,
this.maskTextClass,
this.maskTextSelector,
this.customMaskTextRule,
)
) {
const customMaskFn = getMatchedCustomMaskTextFn(
m.target,
this.customMaskTextRule,
);
const maskFn = customMaskFn ?? this.maskTextFn;
textValue = maskFn ? maskFn(value) : value.replace(/[\S]/g, '*');
}
this.texts.push({
value:
needMaskingText(
m.target,
this.maskTextClass,
this.maskTextSelector,
) && value
? this.maskTextFn
? this.maskTextFn(value)
: value.replace(/[\S]/g, '*')
: value,
value: textValue,
node: m.target,
});
}
Expand Down
9 changes: 9 additions & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
KeepIframeSrcFn,
listenerHandler,
maskTextClass,
maskTextRule,
mediaInteractionCallback,
mouseInteractionCallBack,
mousemoveCallBack,
Expand All @@ -48,6 +49,12 @@ export type recordOptions<T> = {
ignoreClass?: string;
maskTextClass?: maskTextClass;
maskTextSelector?: string;
/**
* only the first matched rule will be applied
* customMaskTextRule has higher priority than maskTextSelector & maskTextFn
* once one of customMaskTextRule match, maskTextSelector & maskTextFn won't be applied
*/
customMaskTextRule?: maskTextRule[];
maskAllInputs?: boolean;
maskInputOptions?: MaskInputOptions;
maskInputFn?: MaskInputFn;
Expand Down Expand Up @@ -86,6 +93,7 @@ export type observerParam = {
ignoreClass: string;
maskTextClass: maskTextClass;
maskTextSelector: string | null;
customMaskTextRule: maskTextRule[];
maskInputOptions: MaskInputOptions;
maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
Expand Down Expand Up @@ -128,6 +136,7 @@ export type MutationBufferParam = Pick<
| 'blockSelector'
| 'maskTextClass'
| 'maskTextSelector'
| 'customMaskTextRule'
| 'inlineStylesheet'
| 'maskInputOptions'
| 'maskTextFn'
Expand Down
20 changes: 20 additions & 0 deletions packages/rrweb/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,26 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots);
});

it('should mask texts with custom selector & function', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'mask-text.html', {
customMaskTextRule: [
{
cssSelector: '[data-masking="true"]',
maskFn: (t: string) => t.replace(/[a-z]/g, '*'),
},
],
}),
);

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

it('can mask character data mutations', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
Expand Down
Loading