From 18ad3dae1a0e1ba1672e9385beb5b76ff77206e8 Mon Sep 17 00:00:00 2001 From: re-fort Date: Thu, 22 Apr 2021 17:03:52 +0900 Subject: [PATCH] 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 --- guide.md | 10 +- package.json | 2 +- src/record/index.ts | 12 + src/record/mutation.ts | 26 +- src/record/observer.ts | 11 + src/record/shadow-dom-manager.ts | 8 +- src/types.ts | 10 + test/__snapshots__/integration.test.ts.snap | 740 ++++++++++++++++++++ test/html/mask-text.html | 20 + test/integration.test.ts | 50 ++ typings/types.d.ts | 8 + yarn.lock | 8 +- 12 files changed, 895 insertions(+), 10 deletions(-) create mode 100644 test/html/mask-text.html diff --git a/guide.md b/guide.md index 87fc4700..4cd3403c 100644 --- a/guide.md +++ b/guide.md @@ -142,10 +142,13 @@ The parameter of `rrweb.record` accepts the following options. | checkoutEveryNms | - | take a full snapshot after every N ms
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 | | 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 \* | -| maskInputOptions | {} | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb-snapshot/blob/6728d12b3cddd96951c86d948578f99ada5749ff/src/types.ts#L72) | -| maskInputFn | - | customize mask input content recording logic | +| maskInputOptions | {} | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb-snapshot/blob/6728d12b3cddd96951c86d948578f99ada5749ff/src/types.ts#L72) | +| maskInputFn | - | customize mask input content recording logic | +| maskTextFn | - | customize mask text content recording logic | | slimDOMOptions | {} | remove unnecessary parts of the DOM
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 | | hooks | {} | hooks for events
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. - `input[type="password"]` will be ignored as default. - 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 diff --git a/package.json b/package.json index 5af64ac8..4c3d86fe 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,6 @@ "@xstate/fsm": "^1.4.0", "fflate": "^0.4.4", "mitt": "^1.1.3", - "rrweb-snapshot": "^1.1.1" + "rrweb-snapshot": "^1.1.2" } } diff --git a/src/record/index.ts b/src/record/index.ts index eb8638f6..3a7668dc 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -43,11 +43,14 @@ function record( blockClass = 'rr-block', blockSelector = null, ignoreClass = 'rr-ignore', + maskTextClass = 'rr-mask', + maskTextSelector = null, inlineStylesheet = true, maskAllInputs, maskInputOptions: _maskInputOptions, slimDOMOptions: _slimDOMOptions, maskInputFn, + maskTextFn, hooks, packFn, sampling = {}, @@ -203,8 +206,11 @@ function record( bypassOptions: { blockClass, blockSelector, + maskTextClass, + maskTextSelector, inlineStylesheet, maskInputOptions, + maskTextFn, recordCanvas, slimDOMOptions, iframeManager, @@ -228,8 +234,11 @@ function record( const [node, idNodeMap] = snapshot(document, { blockClass, blockSelector, + maskTextClass, + maskTextSelector, inlineStylesheet, maskAllInputs: maskInputOptions, + maskTextFn, slimDOM: slimDOMOptions, recordCanvas, onSerialize: (n) => { @@ -396,6 +405,8 @@ function record( ), blockClass, ignoreClass, + maskTextClass, + maskTextSelector, maskInputOptions, inlineStylesheet, sampling, @@ -403,6 +414,7 @@ function record( collectFonts, doc, maskInputFn, + maskTextFn, logOptions, blockSelector, slimDOMOptions, diff --git a/src/record/mutation.ts b/src/record/mutation.ts index 3717afc8..4b65e796 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -6,15 +6,18 @@ import { SlimDOMOptions, IGNORED_NODE, isShadowRoot, + needMaskingText, } from 'rrweb-snapshot'; import { mutationRecord, blockClass, + maskTextClass, mutationCallBack, textCursor, attributeCursor, removedNodeMutation, addedNodeMutation, + MaskTextFn, } from '../types'; import { mirror, @@ -159,8 +162,11 @@ export default class MutationBuffer { private emissionCallback: mutationCallBack; private blockClass: blockClass; private blockSelector: string | null; + private maskTextClass: maskTextClass; + private maskTextSelector: string | null; private inlineStylesheet: boolean; private maskInputOptions: MaskInputOptions; + private maskTextFn: MaskTextFn | undefined; private recordCanvas: boolean; private slimDOMOptions: SlimDOMOptions; private doc: Document; @@ -172,8 +178,11 @@ export default class MutationBuffer { cb: mutationCallBack, blockClass: blockClass, blockSelector: string | null, + maskTextClass: maskTextClass, + maskTextSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, + maskTextFn: MaskTextFn | undefined, recordCanvas: boolean, slimDOMOptions: SlimDOMOptions, doc: Document, @@ -182,8 +191,11 @@ export default class MutationBuffer { ) { this.blockClass = blockClass; this.blockSelector = blockSelector; + this.maskTextClass = maskTextClass; + this.maskTextSelector = maskTextSelector; this.inlineStylesheet = inlineStylesheet; this.maskInputOptions = maskInputOptions; + this.maskTextFn = maskTextFn; this.recordCanvas = recordCanvas; this.slimDOMOptions = slimDOMOptions; this.emissionCallback = cb; @@ -266,9 +278,12 @@ export default class MutationBuffer { map: mirror.map, blockClass: this.blockClass, blockSelector: this.blockSelector, + maskTextClass: this.maskTextClass, + maskTextSelector: this.maskTextSelector, skipChild: true, inlineStylesheet: this.inlineStylesheet, maskInputOptions: this.maskInputOptions, + maskTextFn: this.maskTextFn, slimDOMOptions: this.slimDOMOptions, recordCanvas: this.recordCanvas, onSerialize: (currentN) => { @@ -409,7 +424,16 @@ export default class MutationBuffer { const value = m.target.textContent; if (!isBlocked(m.target, this.blockClass) && value !== m.oldValue) { 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, }); } diff --git a/src/record/observer.ts b/src/record/observer.ts index c4fa81d1..1bc6babd 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -26,6 +26,7 @@ import { inputCallback, hookResetter, blockClass, + maskTextClass, IncrementalSource, hooksParam, Arguments, @@ -36,6 +37,7 @@ import { fontCallback, fontParam, MaskInputFn, + MaskTextFn, logCallback, LogRecordOptions, Logger, @@ -62,8 +64,11 @@ export function initMutationObserver( doc: Document, blockClass: blockClass, blockSelector: string | null, + maskTextClass: maskTextClass, + maskTextSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, + maskTextFn: MaskTextFn | undefined, recordCanvas: boolean, slimDOMOptions: SlimDOMOptions, iframeManager: IframeManager, @@ -77,8 +82,11 @@ export function initMutationObserver( cb, blockClass, blockSelector, + maskTextClass, + maskTextSelector, inlineStylesheet, maskInputOptions, + maskTextFn, recordCanvas, slimDOMOptions, doc, @@ -777,8 +785,11 @@ export function initObservers( o.doc, o.blockClass, o.blockSelector, + o.maskTextClass, + o.maskTextSelector, o.inlineStylesheet, o.maskInputOptions, + o.maskTextFn, o.recordCanvas, o.slimDOMOptions, o.iframeManager, diff --git a/src/record/shadow-dom-manager.ts b/src/record/shadow-dom-manager.ts index ea57a111..6681e11b 100644 --- a/src/record/shadow-dom-manager.ts +++ b/src/record/shadow-dom-manager.ts @@ -1,4 +1,4 @@ -import { mutationCallBack, blockClass } from '../types'; +import { mutationCallBack, blockClass, maskTextClass, MaskTextFn } from '../types'; import { MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot'; import { IframeManager } from './iframe-manager'; import { initMutationObserver } from './observer'; @@ -6,8 +6,11 @@ import { initMutationObserver } from './observer'; type BypassOptions = { blockClass: blockClass; blockSelector: string | null; + maskTextClass: maskTextClass; + maskTextSelector: string | null; inlineStylesheet: boolean; maskInputOptions: MaskInputOptions; + maskTextFn: MaskTextFn | undefined; recordCanvas: boolean; slimDOMOptions: SlimDOMOptions; iframeManager: IframeManager; @@ -31,8 +34,11 @@ export class ShadowDomManager { doc, this.bypassOptions.blockClass, this.bypassOptions.blockSelector, + this.bypassOptions.maskTextClass, + this.bypassOptions.maskTextSelector, this.bypassOptions.inlineStylesheet, this.bypassOptions.maskInputOptions, + this.bypassOptions.maskTextFn, this.bypassOptions.recordCanvas, this.bypassOptions.slimDOMOptions, this.bypassOptions.iframeManager, diff --git a/src/types.ts b/src/types.ts index ed283b39..3be785f0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -163,6 +163,8 @@ export type eventWithTime = event & { export type blockClass = string | RegExp; +export type maskTextClass = string | RegExp; + export type SamplingStrategy = Partial<{ /** * false means not to record mouse/touch move events @@ -196,9 +198,12 @@ export type recordOptions = { blockClass?: blockClass; blockSelector?: string; ignoreClass?: string; + maskTextClass?: maskTextClass; + maskTextSelector?: string; maskAllInputs?: boolean; maskInputOptions?: MaskInputOptions; maskInputFn?: MaskInputFn; + maskTextFn?: MaskTextFn; slimDOMOptions?: SlimDOMOptions | 'all' | true; inlineStylesheet?: boolean; hooks?: hooksParam; @@ -222,8 +227,11 @@ export type observerParam = { blockClass: blockClass; blockSelector: string | null; ignoreClass: string; + maskTextClass: maskTextClass; + maskTextSelector: string | null; maskInputOptions: MaskInputOptions; maskInputFn?: MaskInputFn; + maskTextFn?: MaskTextFn; inlineStylesheet: boolean; styleSheetRuleCb: styleSheetRuleCallback; canvasMutationCb: canvasMutationCallback; @@ -583,6 +591,8 @@ export enum ReplayerEvents { 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) export type ElementState = { // [scrollLeft,scrollTop] diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index 35bd2406..ac6f2cd3 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -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`] = ` "[ { diff --git a/test/html/mask-text.html b/test/html/mask-text.html new file mode 100644 index 00000000..2abaaaa5 --- /dev/null +++ b/test/html/mask-text.html @@ -0,0 +1,20 @@ + + + + + + + Mask text + + +

mask1

+
+ mask2 +
+
+
+
mask3
+
+
+ + diff --git a/test/integration.test.ts b/test/integration.test.ts index c9d1f0e1..207b8782 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -72,8 +72,10 @@ describe('record integration tests', function (this: ISuite) { emit: event => { window.snapshots.push(event); }, + maskTextSelector: ${JSON.stringify(options.maskTextSelector)}, maskAllInputs: ${options.maskAllInputs}, maskInputOptions: ${JSON.stringify(options.maskAllInputs)}, + maskTextFn: ${options.maskTextFn}, recordCanvas: ${options.recordCanvas}, recordLog: ${options.recordLog}, }); @@ -456,4 +458,52 @@ describe('record integration tests', function (this: ISuite) { const snapshots = await page.evaluate('window.snapshots'); 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'); + }); }); diff --git a/typings/types.d.ts b/typings/types.d.ts index 442fd5b6..1d56b52a 100644 --- a/typings/types.d.ts +++ b/typings/types.d.ts @@ -3,6 +3,7 @@ import { serializedNodeWithId, idNodeMap, INode, MaskInputOptions, SlimDOMOption import { PackFn, UnpackFn } from './packer/base'; import { FontFaceDescriptors } from 'css-font-loading-module'; import { IframeManager } from './record/iframe-manager'; +import { MaskTextFn } from '../src/types'; export declare enum EventType { DomContentLoaded = 0, Load = 1, @@ -109,6 +110,7 @@ export declare type eventWithTime = event & { delay?: number; }; export declare type blockClass = string | RegExp; +export declare type maskTextClass = string | RegExp; export declare type SamplingStrategy = Partial<{ mousemove: boolean | number; mousemoveCallback: number; @@ -123,9 +125,12 @@ export declare type recordOptions = { blockClass?: blockClass; blockSelector?: string; ignoreClass?: string; + maskTextClass?: maskTextClass; + maskTextSelector?: string | null; maskAllInputs?: boolean; maskInputOptions?: MaskInputOptions; maskInputFn?: MaskInputFn; + maskTextFn?: MaskTextFn; slimDOMOptions?: SlimDOMOptions | 'all' | true; inlineStylesheet?: boolean; hooks?: hooksParam; @@ -146,9 +151,12 @@ export declare type observerParam = { mediaInteractionCb: mediaInteractionCallback; blockClass: blockClass; blockSelector: string | null; + maskTextClass: maskTextClass; + maskTextSelector: string | null; ignoreClass: string; maskInputOptions: MaskInputOptions; maskInputFn?: MaskInputFn; + maskTextFn?: MaskTextFn; inlineStylesheet: boolean; styleSheetRuleCb: styleSheetRuleCallback; canvasMutationCb: canvasMutationCallback; diff --git a/yarn.lock b/yarn.lock index ff79e7ec..fb4e92ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2767,10 +2767,10 @@ rollup@^2.3.3: optionalDependencies: fsevents "~2.1.2" -rrweb-snapshot@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.1.tgz#71da8792f43b8bd7017851edcd02e3d7c7cfef9f" - integrity sha512-xRX7s2/MA/Ifnul4ImAquD1w/Nkz6WOACm3xdKDdQrCD/xKdgcu1yWoJ8eSIXyfVSuIt4VfrhxJdeHyhC1gmGQ== +rrweb-snapshot@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.2.tgz#e9a5ce11f2dba8ff58e22f7f0b96954ef9133be2" + integrity sha512-J/BCClbk1fs9ilU9Bn8J/vUdumUllvNsmC1rVSz7rxqVzo+MthpI83gll1S3rBQVcxmQcLGmJWpkyO7M2rGyTw== run-async@^2.2.0: version "2.4.1"