From 8bb1c791f50725e8a696d5fdcd35a6cadf4114d8 Mon Sep 17 00:00:00 2001 From: yz-yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] mask input options and sampling options (#252) * part of #80, support mask input options * close #188 enhance sampling options Use a more general sampling strategy interface to describe the configuration of sampling events collection. Implemented mousmove, mouse interaction, scroll and input sampling strategy. --- package.json | 2 +- src/record/index.ts | 41 +- src/record/mutation.ts | 97 +-- src/record/observer.ts | 87 ++- src/types.ts | 38 +- test/__snapshots__/integration.test.ts.snap | 688 ++++++++++++++++++++ test/integration.test.ts | 25 +- tslint.json | 3 +- 8 files changed, 888 insertions(+), 93 deletions(-) diff --git a/package.json b/package.json index 1c81fa47..c2a40c54 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@xstate/fsm": "^1.4.0", "mitt": "^1.1.3", "pako": "^1.0.11", - "rrweb-snapshot": "^0.7.27", + "rrweb-snapshot": "^0.8.0", "smoothscroll-polyfill": "^0.4.3" } } diff --git a/src/record/index.ts b/src/record/index.ts index d04ca288..cf07c251 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -1,4 +1,4 @@ -import { snapshot } from 'rrweb-snapshot'; +import { snapshot, MaskInputOptions } from 'rrweb-snapshot'; import initObservers from './observer'; import { mirror, @@ -35,15 +35,44 @@ function record( blockClass = 'rr-block', ignoreClass = 'rr-ignore', inlineStylesheet = true, - maskAllInputs = false, + maskAllInputs, + maskInputOptions: _maskInputOptions, hooks, - mousemoveWait = 50, packFn, + sampling = {}, + mousemoveWait, } = options; // runtime checks for user options if (!emit) { throw new Error('emit function is required'); } + // move departed options to new options + if (mousemoveWait !== undefined && sampling.mousemove === undefined) { + sampling.mousemove = mousemoveWait; + } + + const maskInputOptions: MaskInputOptions = + maskAllInputs === true + ? { + color: true, + date: true, + 'datetime-local': true, + email: true, + month: true, + number: true, + range: true, + search: true, + tel: true, + text: true, + time: true, + url: true, + week: true, + textarea: true, + select: true, + } + : _maskInputOptions !== undefined + ? _maskInputOptions + : {}; polyfill(); @@ -83,7 +112,7 @@ function record( document, blockClass, inlineStylesheet, - maskAllInputs, + maskInputOptions, ); if (!node) { @@ -217,9 +246,9 @@ function record( ), blockClass, ignoreClass, - maskAllInputs, + maskInputOptions, inlineStylesheet, - mousemoveWait, + sampling, }, hooks, ), diff --git a/src/record/mutation.ts b/src/record/mutation.ts index 8cc4b846..e11ffca0 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -1,4 +1,9 @@ -import { INode, serializeNodeWithId, transformAttribute } from 'rrweb-snapshot'; +import { + INode, + serializeNodeWithId, + transformAttribute, + MaskInputOptions, +} from 'rrweb-snapshot'; import { mutationRecord, blockClass, @@ -50,17 +55,17 @@ export default class MutationBuffer { private emissionCallback: mutationCallBack; private blockClass: blockClass; private inlineStylesheet: boolean; - private maskAllInputs: boolean; + private maskInputOptions: MaskInputOptions; constructor( cb: mutationCallBack, blockClass: blockClass, inlineStylesheet: boolean, - maskAllInputs: boolean, + maskInputOptions: MaskInputOptions, ) { this.blockClass = blockClass; this.inlineStylesheet = inlineStylesheet; - this.maskAllInputs = maskAllInputs; + this.maskInputOptions = maskInputOptions; this.emissionCallback = cb; } @@ -89,7 +94,7 @@ export default class MutationBuffer { this.blockClass, true, this.inlineStylesheet, - this.maskAllInputs, + this.maskInputOptions, )!, }); }; @@ -130,6 +135,47 @@ export default class MutationBuffer { this.emit(); }; + public emit = () => { + const payload = { + texts: this.texts + .map((text) => ({ + id: mirror.getId(text.node as INode), + value: text.value, + })) + // text mutation's id was not in the mirror map means the target node has been removed + .filter((text) => mirror.has(text.id)), + attributes: this.attributes + .map((attribute) => ({ + id: mirror.getId(attribute.node as INode), + attributes: attribute.attributes, + })) + // attribute mutation's id was not in the mirror map means the target node has been removed + .filter((attribute) => mirror.has(attribute.id)), + removes: this.removes, + adds: this.adds, + }; + // payload may be empty if the mutations happened in some blocked elements + if ( + !payload.texts.length && + !payload.attributes.length && + !payload.removes.length && + !payload.adds.length + ) { + return; + } + this.emissionCallback(payload); + + // reset + this.texts = []; + this.attributes = []; + this.removes = []; + this.adds = []; + this.addedSet = new Set(); + this.movedSet = new Set(); + this.droppedSet = new Set(); + this.movedMap = {}; + }; + private processMutation = (m: mutationRecord) => { switch (m.type) { case 'characterData': { @@ -231,47 +277,6 @@ export default class MutationBuffer { } n.childNodes.forEach((childN) => this.genAdds(childN)); }; - - public emit = () => { - const payload = { - texts: this.texts - .map((text) => ({ - id: mirror.getId(text.node as INode), - value: text.value, - })) - // text mutation's id was not in the mirror map means the target node has been removed - .filter((text) => mirror.has(text.id)), - attributes: this.attributes - .map((attribute) => ({ - id: mirror.getId(attribute.node as INode), - attributes: attribute.attributes, - })) - // attribute mutation's id was not in the mirror map means the target node has been removed - .filter((attribute) => mirror.has(attribute.id)), - removes: this.removes, - adds: this.adds, - }; - // payload may be empty if the mutations happened in some blocked elements - if ( - !payload.texts.length && - !payload.attributes.length && - !payload.removes.length && - !payload.adds.length - ) { - return; - } - this.emissionCallback(payload); - - // reset - this.texts = []; - this.attributes = []; - this.removes = []; - this.adds = []; - this.addedSet = new Set(); - this.movedSet = new Set(); - this.droppedSet = new Set(); - this.movedMap = {}; - }; } /** diff --git a/src/record/observer.ts b/src/record/observer.ts index 55fe3b1c..24b1fdaf 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -1,4 +1,4 @@ -import { INode } from 'rrweb-snapshot'; +import { INode, MaskInputOptions } from 'rrweb-snapshot'; import { mirror, throttle, @@ -29,6 +29,7 @@ import { Arguments, mediaInteractionCallback, MediaInteractions, + SamplingStrategy, } from '../types'; import MutationBuffer from './mutation'; @@ -36,14 +37,14 @@ function initMutationObserver( cb: mutationCallBack, blockClass: blockClass, inlineStylesheet: boolean, - maskAllInputs: boolean, + maskInputOptions: MaskInputOptions, ): MutationObserver { // see mutation.ts for details const mutationBuffer = new MutationBuffer( cb, blockClass, inlineStylesheet, - maskAllInputs, + maskInputOptions, ); const observer = new MutationObserver(mutationBuffer.processMutations); observer.observe(document, { @@ -59,8 +60,15 @@ function initMutationObserver( function initMoveObserver( cb: mousemoveCallBack, - mousemoveWait: number, + sampling: SamplingStrategy, ): listenerHandler { + if (sampling.mousemove === false) { + return () => {}; + } + + const threshold = + typeof sampling.mousemove === 'number' ? sampling.mousemove : 50; + let positions: mousePosition[] = []; let timeBaseline: number | null; const wrappedCb = throttle((isTouch: boolean) => { @@ -92,7 +100,7 @@ function initMoveObserver( }); wrappedCb(isTouchEvent(evt)); }, - mousemoveWait, + threshold, { trailing: false, }, @@ -109,7 +117,17 @@ function initMoveObserver( function initMouseInteractionObserver( cb: mouseInteractionCallBack, blockClass: blockClass, + sampling: SamplingStrategy, ): listenerHandler { + if (sampling.mouseInteraction === false) { + return () => {}; + } + const disableMap: Record = + sampling.mouseInteraction === true || + sampling.mouseInteraction === undefined + ? {} + : sampling.mouseInteraction; + const handlers: listenerHandler[] = []; const getHandler = (eventKey: keyof typeof MouseInteractions) => { return (event: MouseEvent | TouchEvent) => { @@ -129,7 +147,12 @@ function initMouseInteractionObserver( }; }; Object.keys(MouseInteractions) - .filter((key) => Number.isNaN(Number(key)) && !key.endsWith('_Departed')) + .filter( + (key) => + Number.isNaN(Number(key)) && + !key.endsWith('_Departed') && + disableMap[key] !== false, + ) .forEach((eventKey: keyof typeof MouseInteractions) => { const eventName = eventKey.toLowerCase(); const handler = getHandler(eventKey); @@ -143,6 +166,7 @@ function initMouseInteractionObserver( function initScrollObserver( cb: scrollCallback, blockClass: blockClass, + sampling: SamplingStrategy, ): listenerHandler { const updatePosition = throttle((evt) => { if (!evt.target || isBlocked(evt.target as Node, blockClass)) { @@ -163,7 +187,7 @@ function initScrollObserver( y: (evt.target as HTMLElement).scrollTop, }); } - }, 100); + }, sampling.scroll || 100); return on('scroll', updatePosition); } @@ -182,27 +206,13 @@ function initViewportResizeObserver( } export const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; -export const MASK_TYPES = [ - 'color', - 'date', - 'datetime-local', - 'email', - 'month', - 'number', - 'range', - 'search', - 'tel', - 'text', - 'time', - 'url', - 'week', -]; const lastInputValueMap: WeakMap = new WeakMap(); function initInputObserver( cb: inputCallback, blockClass: blockClass, ignoreClass: string, - maskAllInputs: boolean, + maskInputOptions: MaskInputOptions, + sampling: SamplingStrategy, ): listenerHandler { function eventHandler(event: Event) { const { target } = event; @@ -223,11 +233,14 @@ function initInputObserver( } let text = (target as HTMLInputElement).value; let isChecked = false; - const hasTextInput = - MASK_TYPES.includes(type) || (target as Element).tagName === 'TEXTAREA'; if (type === 'radio' || type === 'checkbox') { isChecked = (target as HTMLInputElement).checked; - } else if (hasTextInput && maskAllInputs) { + } else if ( + maskInputOptions[ + (target as Element).tagName.toLowerCase() as keyof MaskInputOptions + ] || + maskInputOptions[type as keyof MaskInputOptions] + ) { text = '*'.repeat(text.length); } cbWithDedup(target, { text, isChecked }); @@ -262,10 +275,10 @@ function initInputObserver( }); } } - const handlers: Array = [ - 'input', - 'change', - ].map((eventName) => on(eventName, eventHandler)); + const events = sampling.input === 'last' ? ['change'] : ['input', 'change']; + const handlers: Array< + listenerHandler | hookResetter + > = events.map((eventName) => on(eventName, eventHandler)); const propertyDescriptor = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, 'value', @@ -414,20 +427,26 @@ export default function initObservers( o.mutationCb, o.blockClass, o.inlineStylesheet, - o.maskAllInputs, + o.maskInputOptions, ); - const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.mousemoveWait); + const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.sampling); const mouseInteractionHandler = initMouseInteractionObserver( o.mouseInteractionCb, o.blockClass, + o.sampling, + ); + const scrollHandler = initScrollObserver( + o.scrollCb, + o.blockClass, + o.sampling, ); - const scrollHandler = initScrollObserver(o.scrollCb, o.blockClass); const viewportResizeHandler = initViewportResizeObserver(o.viewportResizeCb); const inputHandler = initInputObserver( o.inputCb, o.blockClass, o.ignoreClass, - o.maskAllInputs, + o.maskInputOptions, + o.sampling, ); const mediaInteractionHandler = initMediaInteractionObserver( o.mediaInteractionCb, diff --git a/src/types.ts b/src/types.ts index 455a7032..7a44972f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,9 @@ -import { serializedNodeWithId, idNodeMap, INode } from 'rrweb-snapshot'; +import { + serializedNodeWithId, + idNodeMap, + INode, + MaskInputOptions, +} from 'rrweb-snapshot'; import { PackFn, UnpackFn } from './packer/base'; export enum EventType { @@ -126,6 +131,28 @@ export type eventWithTime = event & { export type blockClass = string | RegExp; +export type SamplingStrategy = Partial<{ + /** + * false means not to record mouse/touch move events + * number is the throttle threshold of recording mouse/touch move + */ + mousemove: boolean | number; + /** + * false means not to record mouse interaction events + * can also specify record some kinds of mouse interactions + */ + mouseInteraction: boolean | Record; + /** + * number is the throttle threshold of recording scroll + */ + scroll: number; + /** + * 'all' will record all the input events + * 'last' will only record the last input value while input a sequence of chars + */ + input: 'all' | 'last'; +}>; + export type recordOptions = { emit?: (e: T, isCheckout?: boolean) => void; checkoutEveryNth?: number; @@ -133,10 +160,13 @@ export type recordOptions = { blockClass?: blockClass; ignoreClass?: string; maskAllInputs?: boolean; + maskInputOptions?: MaskInputOptions; inlineStylesheet?: boolean; hooks?: hooksParam; - mousemoveWait?: number; packFn?: PackFn; + sampling?: SamplingStrategy; + // departed, please use sampling options + mousemoveWait?: number; }; export type observerParam = { @@ -149,10 +179,10 @@ export type observerParam = { mediaInteractionCb: mediaInteractionCallback; blockClass: blockClass; ignoreClass: string; - maskAllInputs: boolean; + maskInputOptions: MaskInputOptions; inlineStylesheet: boolean; styleSheetRuleCb: styleSheetRuleCallback; - mousemoveWait: number; + sampling: SamplingStrategy; }; export type hooksParam = { diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index 534ddf43..60f96b1e 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -2288,6 +2288,694 @@ exports[`mask 1`] = ` \\"id\\": 37 } }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + } +]" +`; + +exports[`maskInputOptions 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\\": \\"form fields\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"form\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"text\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\" + }, + \\"childNodes\\": [], + \\"id\\": 22 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 23 + } + ], + \\"id\\": 20 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"radio\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\" + }, + \\"childNodes\\": [], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + } + ], + \\"id\\": 25 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"checkbox\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 31 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"checkbox\\" + }, + \\"childNodes\\": [], + \\"id\\": 32 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + } + ], + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"textarea\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 36 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"textarea\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"cols\\": \\"30\\", + \\"rows\\": \\"10\\" + }, + \\"childNodes\\": [], + \\"id\\": 37 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + } + ], + \\"id\\": 35 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 39 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"label\\", + \\"attributes\\": { + \\"for\\": \\"select\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"select\\", + \\"attributes\\": { + \\"name\\": \\"\\", + \\"id\\": \\"\\", + \\"value\\": \\"1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 43 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"1\\", + \\"selected\\": true + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 45 + } + ], + \\"id\\": 44 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 46 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"2\\", + \\"id\\": 48 + } + ], + \\"id\\": 47 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 49 + } + ], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 50 + } + ], + \\"id\\": 40 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 51 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 52 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 54 + } + ], + \\"id\\": 53 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\\\n\\", + \\"id\\": 55 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tes\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"test\\", + \\"isChecked\\": false, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 22 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 1, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 27 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 0, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 2, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"on\\", + \\"isChecked\\": true, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 32 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"t\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"te\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"tex\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"text\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"texta\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textar\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textare\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea \\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea t\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea te\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea tes\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"textarea test\\", + \\"isChecked\\": false, + \\"id\\": 37 + } + }, { \\"type\\": 3, \\"data\\": { diff --git a/test/integration.test.ts b/test/integration.test.ts index afe8dba3..d27730d3 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -32,7 +32,8 @@ describe('record integration tests', function (this: ISuite) { console.log(event); window.snapshots.push(event); }, - maskAllInputs: ${options.maskAllInputs} + maskAllInputs: ${options.maskAllInputs}, + maskInputOptions: ${JSON.stringify(options.maskAllInputs)} }); @@ -166,6 +167,28 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots, __filename, 'mask'); }); + it('can use maskInputOptions to configure which type of inputs should be masked', async () => { + const page: puppeteer.Page = await this.browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'form.html', { + maskInputOptions: { + text: false, + textarea: false, + }, + }), + ); + + await page.type('input[type="text"]', 'test'); + await page.click('input[type="radio"]'); + await page.click('input[type="checkbox"]'); + await page.type('textarea', 'textarea test'); + await page.select('select', '1'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots, __filename, 'maskInputOptions'); + }); + it('should not record blocked elements and its child nodes', async () => { const page: puppeteer.Page = await this.browser.newPage(); await page.goto('about:blank'); diff --git a/tslint.json b/tslint.json index ffe2ce35..df183330 100644 --- a/tslint.json +++ b/tslint.json @@ -17,7 +17,8 @@ ], "arrow-parens": false, "only-arrow-functions": false, - "max-line-length": false + "max-line-length": false, + "no-empty": false }, "rulesDirectory": [] }