Add options to mask texts (#540)

* feat: add options to mask texts

* feat: add the default mask function

* refactor: rename options to identify the difference between  mask text and mask input

* test: add tests about masking

* doc: add options about masking

* chore: bump up rrweb-snapshot version
This commit is contained in:
re-fort
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 861fac54ea
commit b1b25447ff
12 changed files with 895 additions and 10 deletions

View File

@@ -142,10 +142,13 @@ The parameter of `rrweb.record` accepts the following options.
| checkoutEveryNms | - | take a full snapshot after every N ms<br />refer to the [checkout](#checkout) chapter | | checkoutEveryNms | - | take a full snapshot after every N ms<br />refer to the [checkout](#checkout) chapter |
| blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter | | blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter |
| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | | ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter |
| blockSelector | null | Use a string or RegExp to configure which selector should be blocked, refer to the [privacy](#privacy) chapter | | maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter |
| blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter |
| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter |
| maskAllInputs | false | mask all input content as \* | | maskAllInputs | false | mask all input content as \* |
| maskInputOptions | {} | mask some kinds of input \*<br />refer to the [list](https://github.com/rrweb-io/rrweb-snapshot/blob/6728d12b3cddd96951c86d948578f99ada5749ff/src/types.ts#L72) | | maskInputOptions | {} | mask some kinds of input \*<br />refer to the [list](https://github.com/rrweb-io/rrweb-snapshot/blob/6728d12b3cddd96951c86d948578f99ada5749ff/src/types.ts#L72) |
| maskInputFn | - | customize mask input content recording logic | | maskInputFn | - | customize mask input content recording logic |
| maskTextFn | - | customize mask text content recording logic |
| slimDOMOptions | {} | remove unnecessary parts of the DOM <br />refer to the [list](https://github.com/rrweb-io/rrweb-snapshot/blob/6728d12b3cddd96951c86d948578f99ada5749ff/src/types.ts#L91) | | slimDOMOptions | {} | remove unnecessary parts of the DOM <br />refer to the [list](https://github.com/rrweb-io/rrweb-snapshot/blob/6728d12b3cddd96951c86d948578f99ada5749ff/src/types.ts#L91) |
| inlineStylesheet | true | whether to inline the stylesheet in the events | | inlineStylesheet | true | whether to inline the stylesheet in the events |
| hooks | {} | hooks for events<br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) | | hooks | {} | hooks for events<br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/9488deb6d54a5f04350c063d942da5e96ab74075/src/types.ts#L207) |
@@ -163,6 +166,7 @@ You may find some contents on the webpage which are not willing to be recorded,
- An element with the class name `.rr-ignore` will not record its input events. - An element with the class name `.rr-ignore` will not record its input events.
- `input[type="password"]` will be ignored as default. - `input[type="password"]` will be ignored as default.
- Mask options to mask the content in input elements. - Mask options to mask the content in input elements.
- A text of elements with the class name `.rr-mask` and its children will be masked.
#### Checkout #### Checkout

View File

@@ -66,6 +66,6 @@
"@xstate/fsm": "^1.4.0", "@xstate/fsm": "^1.4.0",
"fflate": "^0.4.4", "fflate": "^0.4.4",
"mitt": "^1.1.3", "mitt": "^1.1.3",
"rrweb-snapshot": "^1.1.1" "rrweb-snapshot": "^1.1.2"
} }
} }

View File

@@ -43,11 +43,14 @@ function record<T = eventWithTime>(
blockClass = 'rr-block', blockClass = 'rr-block',
blockSelector = null, blockSelector = null,
ignoreClass = 'rr-ignore', ignoreClass = 'rr-ignore',
maskTextClass = 'rr-mask',
maskTextSelector = null,
inlineStylesheet = true, inlineStylesheet = true,
maskAllInputs, maskAllInputs,
maskInputOptions: _maskInputOptions, maskInputOptions: _maskInputOptions,
slimDOMOptions: _slimDOMOptions, slimDOMOptions: _slimDOMOptions,
maskInputFn, maskInputFn,
maskTextFn,
hooks, hooks,
packFn, packFn,
sampling = {}, sampling = {},
@@ -203,8 +206,11 @@ function record<T = eventWithTime>(
bypassOptions: { bypassOptions: {
blockClass, blockClass,
blockSelector, blockSelector,
maskTextClass,
maskTextSelector,
inlineStylesheet, inlineStylesheet,
maskInputOptions, maskInputOptions,
maskTextFn,
recordCanvas, recordCanvas,
slimDOMOptions, slimDOMOptions,
iframeManager, iframeManager,
@@ -228,8 +234,11 @@ function record<T = eventWithTime>(
const [node, idNodeMap] = snapshot(document, { const [node, idNodeMap] = snapshot(document, {
blockClass, blockClass,
blockSelector, blockSelector,
maskTextClass,
maskTextSelector,
inlineStylesheet, inlineStylesheet,
maskAllInputs: maskInputOptions, maskAllInputs: maskInputOptions,
maskTextFn,
slimDOM: slimDOMOptions, slimDOM: slimDOMOptions,
recordCanvas, recordCanvas,
onSerialize: (n) => { onSerialize: (n) => {
@@ -396,6 +405,8 @@ function record<T = eventWithTime>(
), ),
blockClass, blockClass,
ignoreClass, ignoreClass,
maskTextClass,
maskTextSelector,
maskInputOptions, maskInputOptions,
inlineStylesheet, inlineStylesheet,
sampling, sampling,
@@ -403,6 +414,7 @@ function record<T = eventWithTime>(
collectFonts, collectFonts,
doc, doc,
maskInputFn, maskInputFn,
maskTextFn,
logOptions, logOptions,
blockSelector, blockSelector,
slimDOMOptions, slimDOMOptions,

View File

@@ -6,15 +6,18 @@ import {
SlimDOMOptions, SlimDOMOptions,
IGNORED_NODE, IGNORED_NODE,
isShadowRoot, isShadowRoot,
needMaskingText,
} from 'rrweb-snapshot'; } from 'rrweb-snapshot';
import { import {
mutationRecord, mutationRecord,
blockClass, blockClass,
maskTextClass,
mutationCallBack, mutationCallBack,
textCursor, textCursor,
attributeCursor, attributeCursor,
removedNodeMutation, removedNodeMutation,
addedNodeMutation, addedNodeMutation,
MaskTextFn,
} from '../types'; } from '../types';
import { import {
mirror, mirror,
@@ -159,8 +162,11 @@ export default class MutationBuffer {
private emissionCallback: mutationCallBack; private emissionCallback: mutationCallBack;
private blockClass: blockClass; private blockClass: blockClass;
private blockSelector: string | null; private blockSelector: string | null;
private maskTextClass: maskTextClass;
private maskTextSelector: string | null;
private inlineStylesheet: boolean; private inlineStylesheet: boolean;
private maskInputOptions: MaskInputOptions; private maskInputOptions: MaskInputOptions;
private maskTextFn: MaskTextFn | undefined;
private recordCanvas: boolean; private recordCanvas: boolean;
private slimDOMOptions: SlimDOMOptions; private slimDOMOptions: SlimDOMOptions;
private doc: Document; private doc: Document;
@@ -172,8 +178,11 @@ export default class MutationBuffer {
cb: mutationCallBack, cb: mutationCallBack,
blockClass: blockClass, blockClass: blockClass,
blockSelector: string | null, blockSelector: string | null,
maskTextClass: maskTextClass,
maskTextSelector: string | null,
inlineStylesheet: boolean, inlineStylesheet: boolean,
maskInputOptions: MaskInputOptions, maskInputOptions: MaskInputOptions,
maskTextFn: MaskTextFn | undefined,
recordCanvas: boolean, recordCanvas: boolean,
slimDOMOptions: SlimDOMOptions, slimDOMOptions: SlimDOMOptions,
doc: Document, doc: Document,
@@ -182,8 +191,11 @@ export default class MutationBuffer {
) { ) {
this.blockClass = blockClass; this.blockClass = blockClass;
this.blockSelector = blockSelector; this.blockSelector = blockSelector;
this.maskTextClass = maskTextClass;
this.maskTextSelector = maskTextSelector;
this.inlineStylesheet = inlineStylesheet; this.inlineStylesheet = inlineStylesheet;
this.maskInputOptions = maskInputOptions; this.maskInputOptions = maskInputOptions;
this.maskTextFn = maskTextFn;
this.recordCanvas = recordCanvas; this.recordCanvas = recordCanvas;
this.slimDOMOptions = slimDOMOptions; this.slimDOMOptions = slimDOMOptions;
this.emissionCallback = cb; this.emissionCallback = cb;
@@ -266,9 +278,12 @@ export default class MutationBuffer {
map: mirror.map, map: mirror.map,
blockClass: this.blockClass, blockClass: this.blockClass,
blockSelector: this.blockSelector, blockSelector: this.blockSelector,
maskTextClass: this.maskTextClass,
maskTextSelector: this.maskTextSelector,
skipChild: true, skipChild: true,
inlineStylesheet: this.inlineStylesheet, inlineStylesheet: this.inlineStylesheet,
maskInputOptions: this.maskInputOptions, maskInputOptions: this.maskInputOptions,
maskTextFn: this.maskTextFn,
slimDOMOptions: this.slimDOMOptions, slimDOMOptions: this.slimDOMOptions,
recordCanvas: this.recordCanvas, recordCanvas: this.recordCanvas,
onSerialize: (currentN) => { onSerialize: (currentN) => {
@@ -409,7 +424,16 @@ export default class MutationBuffer {
const value = m.target.textContent; const value = m.target.textContent;
if (!isBlocked(m.target, this.blockClass) && value !== m.oldValue) { if (!isBlocked(m.target, this.blockClass) && value !== m.oldValue) {
this.texts.push({ this.texts.push({
value, value:
needMaskingText(
m.target,
this.maskTextClass,
this.maskTextSelector,
) && value
? this.maskTextFn
? this.maskTextFn(value)
: value.replace(/[\S]/g, '*')
: value,
node: m.target, node: m.target,
}); });
} }

View File

@@ -26,6 +26,7 @@ import {
inputCallback, inputCallback,
hookResetter, hookResetter,
blockClass, blockClass,
maskTextClass,
IncrementalSource, IncrementalSource,
hooksParam, hooksParam,
Arguments, Arguments,
@@ -36,6 +37,7 @@ import {
fontCallback, fontCallback,
fontParam, fontParam,
MaskInputFn, MaskInputFn,
MaskTextFn,
logCallback, logCallback,
LogRecordOptions, LogRecordOptions,
Logger, Logger,
@@ -62,8 +64,11 @@ export function initMutationObserver(
doc: Document, doc: Document,
blockClass: blockClass, blockClass: blockClass,
blockSelector: string | null, blockSelector: string | null,
maskTextClass: maskTextClass,
maskTextSelector: string | null,
inlineStylesheet: boolean, inlineStylesheet: boolean,
maskInputOptions: MaskInputOptions, maskInputOptions: MaskInputOptions,
maskTextFn: MaskTextFn | undefined,
recordCanvas: boolean, recordCanvas: boolean,
slimDOMOptions: SlimDOMOptions, slimDOMOptions: SlimDOMOptions,
iframeManager: IframeManager, iframeManager: IframeManager,
@@ -77,8 +82,11 @@ export function initMutationObserver(
cb, cb,
blockClass, blockClass,
blockSelector, blockSelector,
maskTextClass,
maskTextSelector,
inlineStylesheet, inlineStylesheet,
maskInputOptions, maskInputOptions,
maskTextFn,
recordCanvas, recordCanvas,
slimDOMOptions, slimDOMOptions,
doc, doc,
@@ -777,8 +785,11 @@ export function initObservers(
o.doc, o.doc,
o.blockClass, o.blockClass,
o.blockSelector, o.blockSelector,
o.maskTextClass,
o.maskTextSelector,
o.inlineStylesheet, o.inlineStylesheet,
o.maskInputOptions, o.maskInputOptions,
o.maskTextFn,
o.recordCanvas, o.recordCanvas,
o.slimDOMOptions, o.slimDOMOptions,
o.iframeManager, o.iframeManager,

View File

@@ -1,4 +1,4 @@
import { mutationCallBack, blockClass } from '../types'; import { mutationCallBack, blockClass, maskTextClass, MaskTextFn } from '../types';
import { MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot'; import { MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot';
import { IframeManager } from './iframe-manager'; import { IframeManager } from './iframe-manager';
import { initMutationObserver } from './observer'; import { initMutationObserver } from './observer';
@@ -6,8 +6,11 @@ import { initMutationObserver } from './observer';
type BypassOptions = { type BypassOptions = {
blockClass: blockClass; blockClass: blockClass;
blockSelector: string | null; blockSelector: string | null;
maskTextClass: maskTextClass;
maskTextSelector: string | null;
inlineStylesheet: boolean; inlineStylesheet: boolean;
maskInputOptions: MaskInputOptions; maskInputOptions: MaskInputOptions;
maskTextFn: MaskTextFn | undefined;
recordCanvas: boolean; recordCanvas: boolean;
slimDOMOptions: SlimDOMOptions; slimDOMOptions: SlimDOMOptions;
iframeManager: IframeManager; iframeManager: IframeManager;
@@ -31,8 +34,11 @@ export class ShadowDomManager {
doc, doc,
this.bypassOptions.blockClass, this.bypassOptions.blockClass,
this.bypassOptions.blockSelector, this.bypassOptions.blockSelector,
this.bypassOptions.maskTextClass,
this.bypassOptions.maskTextSelector,
this.bypassOptions.inlineStylesheet, this.bypassOptions.inlineStylesheet,
this.bypassOptions.maskInputOptions, this.bypassOptions.maskInputOptions,
this.bypassOptions.maskTextFn,
this.bypassOptions.recordCanvas, this.bypassOptions.recordCanvas,
this.bypassOptions.slimDOMOptions, this.bypassOptions.slimDOMOptions,
this.bypassOptions.iframeManager, this.bypassOptions.iframeManager,

View File

@@ -163,6 +163,8 @@ export type eventWithTime = event & {
export type blockClass = string | RegExp; export type blockClass = string | RegExp;
export type maskTextClass = string | RegExp;
export type SamplingStrategy = Partial<{ export type SamplingStrategy = Partial<{
/** /**
* false means not to record mouse/touch move events * false means not to record mouse/touch move events
@@ -196,9 +198,12 @@ export type recordOptions<T> = {
blockClass?: blockClass; blockClass?: blockClass;
blockSelector?: string; blockSelector?: string;
ignoreClass?: string; ignoreClass?: string;
maskTextClass?: maskTextClass;
maskTextSelector?: string;
maskAllInputs?: boolean; maskAllInputs?: boolean;
maskInputOptions?: MaskInputOptions; maskInputOptions?: MaskInputOptions;
maskInputFn?: MaskInputFn; maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
slimDOMOptions?: SlimDOMOptions | 'all' | true; slimDOMOptions?: SlimDOMOptions | 'all' | true;
inlineStylesheet?: boolean; inlineStylesheet?: boolean;
hooks?: hooksParam; hooks?: hooksParam;
@@ -222,8 +227,11 @@ export type observerParam = {
blockClass: blockClass; blockClass: blockClass;
blockSelector: string | null; blockSelector: string | null;
ignoreClass: string; ignoreClass: string;
maskTextClass: maskTextClass;
maskTextSelector: string | null;
maskInputOptions: MaskInputOptions; maskInputOptions: MaskInputOptions;
maskInputFn?: MaskInputFn; maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
inlineStylesheet: boolean; inlineStylesheet: boolean;
styleSheetRuleCb: styleSheetRuleCallback; styleSheetRuleCb: styleSheetRuleCallback;
canvasMutationCb: canvasMutationCallback; canvasMutationCb: canvasMutationCallback;
@@ -583,6 +591,8 @@ export enum ReplayerEvents {
export type MaskInputFn = (text: string) => string; export type MaskInputFn = (text: string) => string;
export type MaskTextFn = (text: string) => string;
// store the state that would be changed during the process(unmount from dom and mount again) // store the state that would be changed during the process(unmount from dom and mount again)
export type ElementState = { export type ElementState = {
// [scrollLeft,scrollTop] // [scrollLeft,scrollTop]

View File

@@ -4111,6 +4111,746 @@ exports[`mask 1`] = `
]" ]"
`; `;
exports[`mask-character-data 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\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 3
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 5
},
{
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"mutation observer\\",
\\"id\\": 7
}
],
\\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 8
},
{
\\"type\\": 2,
\\"tagName\\": \\"ul\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 10
},
{
\\"type\\": 2,
\\"tagName\\": \\"li\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 11
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 12
}
],
\\"id\\": 9
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n \\",
\\"id\\": 13
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 15
}
],
\\"id\\": 14
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 16
}
],
\\"id\\": 4
}
],
\\"id\\": 2
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [
{
\\"id\\": 6,
\\"attributes\\": {
\\"class\\": \\"rr-mask\\"
}
}
],
\\"removes\\": [
{
\\"parentId\\": 6,
\\"id\\": 7
}
],
\\"adds\\": [
{
\\"parentId\\": 9,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"li\\",
\\"attributes\\": {
\\"class\\": \\"rr-mask\\"
},
\\"childNodes\\": [],
\\"id\\": 17
}
},
{
\\"parentId\\": 17,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"*** **** ****\\",
\\"id\\": 18
}
},
{
\\"parentId\\": 6,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"*******\\",
\\"id\\": 19
}
}
]
}
}
]"
`;
exports[`mask-text 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\\": \\"Mask text\\",
\\"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\\": \\"p\\",
\\"attributes\\": {
\\"class\\": \\"rr-mask\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"*****\\",
\\"id\\": 19
}
],
\\"id\\": 18
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 20
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"class\\": \\"rr-mask\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 22
},
{
\\"type\\": 2,
\\"tagName\\": \\"span\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"*****\\",
\\"id\\": 24
}
],
\\"id\\": 23
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 25
}
],
\\"id\\": 21
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 26
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"data-masking\\": \\"true\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 28
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 30
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"*****\\",
\\"id\\": 32
}
],
\\"id\\": 31
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 33
}
],
\\"id\\": 29
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 34
}
],
\\"id\\": 27
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 35
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 37
}
],
\\"id\\": 36
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
\\"id\\": 38
}
],
\\"id\\": 16
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
}
]"
`;
exports[`mask-text-fn 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\\": \\"Mask text\\",
\\"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\\": \\"p\\",
\\"attributes\\": {
\\"class\\": \\"rr-mask\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"****1\\",
\\"id\\": 19
}
],
\\"id\\": 18
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 20
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"class\\": \\"rr-mask\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 22
},
{
\\"type\\": 2,
\\"tagName\\": \\"span\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"****2\\",
\\"id\\": 24
}
],
\\"id\\": 23
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 25
}
],
\\"id\\": 21
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 26
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"data-masking\\": \\"true\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 28
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 30
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"****3\\",
\\"id\\": 32
}
],
\\"id\\": 31
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 33
}
],
\\"id\\": 29
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 34
}
],
\\"id\\": 27
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 35
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 37
}
],
\\"id\\": 36
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
\\"id\\": 38
}
],
\\"id\\": 16
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
}
]"
`;
exports[`maskInputOptions 1`] = ` exports[`maskInputOptions 1`] = `
"[ "[
{ {

20
test/html/mask-text.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Mask text</title>
</head>
<body>
<p class="rr-mask">mask1</p>
<div class="rr-mask">
<span>mask2</span>
</div>
<div data-masking="true">
<div>
<div>mask3</div>
</div>
</div>
</body>
</html>

View File

@@ -72,8 +72,10 @@ describe('record integration tests', function (this: ISuite) {
emit: event => { emit: event => {
window.snapshots.push(event); window.snapshots.push(event);
}, },
maskTextSelector: ${JSON.stringify(options.maskTextSelector)},
maskAllInputs: ${options.maskAllInputs}, maskAllInputs: ${options.maskAllInputs},
maskInputOptions: ${JSON.stringify(options.maskAllInputs)}, maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
maskTextFn: ${options.maskTextFn},
recordCanvas: ${options.recordCanvas}, recordCanvas: ${options.recordCanvas},
recordLog: ${options.recordLog}, recordLog: ${options.recordLog},
}); });
@@ -456,4 +458,52 @@ describe('record integration tests', function (this: ISuite) {
const snapshots = await page.evaluate('window.snapshots'); const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'shadow-dom'); assertSnapshot(snapshots, __filename, 'shadow-dom');
}); });
it('should mask texts', async () => {
const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'mask-text.html', {
maskTextSelector: '[data-masking="true"]',
}),
);
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'mask-text');
});
it('should mask texts using maskTextFn', async () => {
const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'mask-text.html', {
maskTextSelector: '[data-masking="true"]',
maskTextFn: (t: string) => t.replace(/[a-z]/g, '*'),
}),
);
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'mask-text-fn');
});
it('can mask character data mutations', async () => {
const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'mutation-observer.html'));
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 = 'rr-mask';
});
ul.appendChild(li);
li.innerText = 'new list item';
p.innerText = 'mutated';
});
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots, __filename, 'mask-character-data');
});
}); });

8
typings/types.d.ts vendored
View File

@@ -3,6 +3,7 @@ import { serializedNodeWithId, idNodeMap, INode, MaskInputOptions, SlimDOMOption
import { PackFn, UnpackFn } from './packer/base'; import { PackFn, UnpackFn } from './packer/base';
import { FontFaceDescriptors } from 'css-font-loading-module'; import { FontFaceDescriptors } from 'css-font-loading-module';
import { IframeManager } from './record/iframe-manager'; import { IframeManager } from './record/iframe-manager';
import { MaskTextFn } from '../src/types';
export declare enum EventType { export declare enum EventType {
DomContentLoaded = 0, DomContentLoaded = 0,
Load = 1, Load = 1,
@@ -109,6 +110,7 @@ export declare type eventWithTime = event & {
delay?: number; delay?: number;
}; };
export declare type blockClass = string | RegExp; export declare type blockClass = string | RegExp;
export declare type maskTextClass = string | RegExp;
export declare type SamplingStrategy = Partial<{ export declare type SamplingStrategy = Partial<{
mousemove: boolean | number; mousemove: boolean | number;
mousemoveCallback: number; mousemoveCallback: number;
@@ -123,9 +125,12 @@ export declare type recordOptions<T> = {
blockClass?: blockClass; blockClass?: blockClass;
blockSelector?: string; blockSelector?: string;
ignoreClass?: string; ignoreClass?: string;
maskTextClass?: maskTextClass;
maskTextSelector?: string | null;
maskAllInputs?: boolean; maskAllInputs?: boolean;
maskInputOptions?: MaskInputOptions; maskInputOptions?: MaskInputOptions;
maskInputFn?: MaskInputFn; maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
slimDOMOptions?: SlimDOMOptions | 'all' | true; slimDOMOptions?: SlimDOMOptions | 'all' | true;
inlineStylesheet?: boolean; inlineStylesheet?: boolean;
hooks?: hooksParam; hooks?: hooksParam;
@@ -146,9 +151,12 @@ export declare type observerParam = {
mediaInteractionCb: mediaInteractionCallback; mediaInteractionCb: mediaInteractionCallback;
blockClass: blockClass; blockClass: blockClass;
blockSelector: string | null; blockSelector: string | null;
maskTextClass: maskTextClass;
maskTextSelector: string | null;
ignoreClass: string; ignoreClass: string;
maskInputOptions: MaskInputOptions; maskInputOptions: MaskInputOptions;
maskInputFn?: MaskInputFn; maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
inlineStylesheet: boolean; inlineStylesheet: boolean;
styleSheetRuleCb: styleSheetRuleCallback; styleSheetRuleCb: styleSheetRuleCallback;
canvasMutationCb: canvasMutationCallback; canvasMutationCb: canvasMutationCallback;

View File

@@ -2767,10 +2767,10 @@ rollup@^2.3.3:
optionalDependencies: optionalDependencies:
fsevents "~2.1.2" fsevents "~2.1.2"
rrweb-snapshot@^1.1.1: rrweb-snapshot@^1.1.2:
version "1.1.1" version "1.1.2"
resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.1.tgz#71da8792f43b8bd7017851edcd02e3d7c7cfef9f" resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.2.tgz#e9a5ce11f2dba8ff58e22f7f0b96954ef9133be2"
integrity sha512-xRX7s2/MA/Ifnul4ImAquD1w/Nkz6WOACm3xdKDdQrCD/xKdgcu1yWoJ8eSIXyfVSuIt4VfrhxJdeHyhC1gmGQ== integrity sha512-J/BCClbk1fs9ilU9Bn8J/vUdumUllvNsmC1rVSz7rxqVzo+MthpI83gll1S3rBQVcxmQcLGmJWpkyO7M2rGyTw==
run-async@^2.2.0: run-async@^2.2.0:
version "2.4.1" version "2.4.1"