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.
This commit is contained in:
yz-yu
2020-07-18 15:05:19 +08:00
committed by GitHub
parent 286b520907
commit 7de7eb5e54
8 changed files with 888 additions and 93 deletions

View File

@@ -62,7 +62,7 @@
"@xstate/fsm": "^1.4.0", "@xstate/fsm": "^1.4.0",
"mitt": "^1.1.3", "mitt": "^1.1.3",
"pako": "^1.0.11", "pako": "^1.0.11",
"rrweb-snapshot": "^0.7.27", "rrweb-snapshot": "^0.8.0",
"smoothscroll-polyfill": "^0.4.3" "smoothscroll-polyfill": "^0.4.3"
} }
} }

View File

@@ -1,4 +1,4 @@
import { snapshot } from 'rrweb-snapshot'; import { snapshot, MaskInputOptions } from 'rrweb-snapshot';
import initObservers from './observer'; import initObservers from './observer';
import { import {
mirror, mirror,
@@ -35,15 +35,44 @@ function record<T = eventWithTime>(
blockClass = 'rr-block', blockClass = 'rr-block',
ignoreClass = 'rr-ignore', ignoreClass = 'rr-ignore',
inlineStylesheet = true, inlineStylesheet = true,
maskAllInputs = false, maskAllInputs,
maskInputOptions: _maskInputOptions,
hooks, hooks,
mousemoveWait = 50,
packFn, packFn,
sampling = {},
mousemoveWait,
} = options; } = options;
// runtime checks for user options // runtime checks for user options
if (!emit) { if (!emit) {
throw new Error('emit function is required'); 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(); polyfill();
@@ -83,7 +112,7 @@ function record<T = eventWithTime>(
document, document,
blockClass, blockClass,
inlineStylesheet, inlineStylesheet,
maskAllInputs, maskInputOptions,
); );
if (!node) { if (!node) {
@@ -217,9 +246,9 @@ function record<T = eventWithTime>(
), ),
blockClass, blockClass,
ignoreClass, ignoreClass,
maskAllInputs, maskInputOptions,
inlineStylesheet, inlineStylesheet,
mousemoveWait, sampling,
}, },
hooks, hooks,
), ),

View File

@@ -1,4 +1,9 @@
import { INode, serializeNodeWithId, transformAttribute } from 'rrweb-snapshot'; import {
INode,
serializeNodeWithId,
transformAttribute,
MaskInputOptions,
} from 'rrweb-snapshot';
import { import {
mutationRecord, mutationRecord,
blockClass, blockClass,
@@ -50,17 +55,17 @@ export default class MutationBuffer {
private emissionCallback: mutationCallBack; private emissionCallback: mutationCallBack;
private blockClass: blockClass; private blockClass: blockClass;
private inlineStylesheet: boolean; private inlineStylesheet: boolean;
private maskAllInputs: boolean; private maskInputOptions: MaskInputOptions;
constructor( constructor(
cb: mutationCallBack, cb: mutationCallBack,
blockClass: blockClass, blockClass: blockClass,
inlineStylesheet: boolean, inlineStylesheet: boolean,
maskAllInputs: boolean, maskInputOptions: MaskInputOptions,
) { ) {
this.blockClass = blockClass; this.blockClass = blockClass;
this.inlineStylesheet = inlineStylesheet; this.inlineStylesheet = inlineStylesheet;
this.maskAllInputs = maskAllInputs; this.maskInputOptions = maskInputOptions;
this.emissionCallback = cb; this.emissionCallback = cb;
} }
@@ -89,7 +94,7 @@ export default class MutationBuffer {
this.blockClass, this.blockClass,
true, true,
this.inlineStylesheet, this.inlineStylesheet,
this.maskAllInputs, this.maskInputOptions,
)!, )!,
}); });
}; };
@@ -130,6 +135,47 @@ export default class MutationBuffer {
this.emit(); 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<Node>();
this.movedSet = new Set<Node>();
this.droppedSet = new Set<Node>();
this.movedMap = {};
};
private processMutation = (m: mutationRecord) => { private processMutation = (m: mutationRecord) => {
switch (m.type) { switch (m.type) {
case 'characterData': { case 'characterData': {
@@ -231,47 +277,6 @@ export default class MutationBuffer {
} }
n.childNodes.forEach((childN) => this.genAdds(childN)); 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<Node>();
this.movedSet = new Set<Node>();
this.droppedSet = new Set<Node>();
this.movedMap = {};
};
} }
/** /**

View File

@@ -1,4 +1,4 @@
import { INode } from 'rrweb-snapshot'; import { INode, MaskInputOptions } from 'rrweb-snapshot';
import { import {
mirror, mirror,
throttle, throttle,
@@ -29,6 +29,7 @@ import {
Arguments, Arguments,
mediaInteractionCallback, mediaInteractionCallback,
MediaInteractions, MediaInteractions,
SamplingStrategy,
} from '../types'; } from '../types';
import MutationBuffer from './mutation'; import MutationBuffer from './mutation';
@@ -36,14 +37,14 @@ function initMutationObserver(
cb: mutationCallBack, cb: mutationCallBack,
blockClass: blockClass, blockClass: blockClass,
inlineStylesheet: boolean, inlineStylesheet: boolean,
maskAllInputs: boolean, maskInputOptions: MaskInputOptions,
): MutationObserver { ): MutationObserver {
// see mutation.ts for details // see mutation.ts for details
const mutationBuffer = new MutationBuffer( const mutationBuffer = new MutationBuffer(
cb, cb,
blockClass, blockClass,
inlineStylesheet, inlineStylesheet,
maskAllInputs, maskInputOptions,
); );
const observer = new MutationObserver(mutationBuffer.processMutations); const observer = new MutationObserver(mutationBuffer.processMutations);
observer.observe(document, { observer.observe(document, {
@@ -59,8 +60,15 @@ function initMutationObserver(
function initMoveObserver( function initMoveObserver(
cb: mousemoveCallBack, cb: mousemoveCallBack,
mousemoveWait: number, sampling: SamplingStrategy,
): listenerHandler { ): listenerHandler {
if (sampling.mousemove === false) {
return () => {};
}
const threshold =
typeof sampling.mousemove === 'number' ? sampling.mousemove : 50;
let positions: mousePosition[] = []; let positions: mousePosition[] = [];
let timeBaseline: number | null; let timeBaseline: number | null;
const wrappedCb = throttle((isTouch: boolean) => { const wrappedCb = throttle((isTouch: boolean) => {
@@ -92,7 +100,7 @@ function initMoveObserver(
}); });
wrappedCb(isTouchEvent(evt)); wrappedCb(isTouchEvent(evt));
}, },
mousemoveWait, threshold,
{ {
trailing: false, trailing: false,
}, },
@@ -109,7 +117,17 @@ function initMoveObserver(
function initMouseInteractionObserver( function initMouseInteractionObserver(
cb: mouseInteractionCallBack, cb: mouseInteractionCallBack,
blockClass: blockClass, blockClass: blockClass,
sampling: SamplingStrategy,
): listenerHandler { ): listenerHandler {
if (sampling.mouseInteraction === false) {
return () => {};
}
const disableMap: Record<string, boolean | undefined> =
sampling.mouseInteraction === true ||
sampling.mouseInteraction === undefined
? {}
: sampling.mouseInteraction;
const handlers: listenerHandler[] = []; const handlers: listenerHandler[] = [];
const getHandler = (eventKey: keyof typeof MouseInteractions) => { const getHandler = (eventKey: keyof typeof MouseInteractions) => {
return (event: MouseEvent | TouchEvent) => { return (event: MouseEvent | TouchEvent) => {
@@ -129,7 +147,12 @@ function initMouseInteractionObserver(
}; };
}; };
Object.keys(MouseInteractions) 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) => { .forEach((eventKey: keyof typeof MouseInteractions) => {
const eventName = eventKey.toLowerCase(); const eventName = eventKey.toLowerCase();
const handler = getHandler(eventKey); const handler = getHandler(eventKey);
@@ -143,6 +166,7 @@ function initMouseInteractionObserver(
function initScrollObserver( function initScrollObserver(
cb: scrollCallback, cb: scrollCallback,
blockClass: blockClass, blockClass: blockClass,
sampling: SamplingStrategy,
): listenerHandler { ): listenerHandler {
const updatePosition = throttle<UIEvent>((evt) => { const updatePosition = throttle<UIEvent>((evt) => {
if (!evt.target || isBlocked(evt.target as Node, blockClass)) { if (!evt.target || isBlocked(evt.target as Node, blockClass)) {
@@ -163,7 +187,7 @@ function initScrollObserver(
y: (evt.target as HTMLElement).scrollTop, y: (evt.target as HTMLElement).scrollTop,
}); });
} }
}, 100); }, sampling.scroll || 100);
return on('scroll', updatePosition); return on('scroll', updatePosition);
} }
@@ -182,27 +206,13 @@ function initViewportResizeObserver(
} }
export const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; 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<EventTarget, inputValue> = new WeakMap(); const lastInputValueMap: WeakMap<EventTarget, inputValue> = new WeakMap();
function initInputObserver( function initInputObserver(
cb: inputCallback, cb: inputCallback,
blockClass: blockClass, blockClass: blockClass,
ignoreClass: string, ignoreClass: string,
maskAllInputs: boolean, maskInputOptions: MaskInputOptions,
sampling: SamplingStrategy,
): listenerHandler { ): listenerHandler {
function eventHandler(event: Event) { function eventHandler(event: Event) {
const { target } = event; const { target } = event;
@@ -223,11 +233,14 @@ function initInputObserver(
} }
let text = (target as HTMLInputElement).value; let text = (target as HTMLInputElement).value;
let isChecked = false; let isChecked = false;
const hasTextInput =
MASK_TYPES.includes(type) || (target as Element).tagName === 'TEXTAREA';
if (type === 'radio' || type === 'checkbox') { if (type === 'radio' || type === 'checkbox') {
isChecked = (target as HTMLInputElement).checked; 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); text = '*'.repeat(text.length);
} }
cbWithDedup(target, { text, isChecked }); cbWithDedup(target, { text, isChecked });
@@ -262,10 +275,10 @@ function initInputObserver(
}); });
} }
} }
const handlers: Array<listenerHandler | hookResetter> = [ const events = sampling.input === 'last' ? ['change'] : ['input', 'change'];
'input', const handlers: Array<
'change', listenerHandler | hookResetter
].map((eventName) => on(eventName, eventHandler)); > = events.map((eventName) => on(eventName, eventHandler));
const propertyDescriptor = Object.getOwnPropertyDescriptor( const propertyDescriptor = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype, HTMLInputElement.prototype,
'value', 'value',
@@ -414,20 +427,26 @@ export default function initObservers(
o.mutationCb, o.mutationCb,
o.blockClass, o.blockClass,
o.inlineStylesheet, o.inlineStylesheet,
o.maskAllInputs, o.maskInputOptions,
); );
const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.mousemoveWait); const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.sampling);
const mouseInteractionHandler = initMouseInteractionObserver( const mouseInteractionHandler = initMouseInteractionObserver(
o.mouseInteractionCb, o.mouseInteractionCb,
o.blockClass, 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 viewportResizeHandler = initViewportResizeObserver(o.viewportResizeCb);
const inputHandler = initInputObserver( const inputHandler = initInputObserver(
o.inputCb, o.inputCb,
o.blockClass, o.blockClass,
o.ignoreClass, o.ignoreClass,
o.maskAllInputs, o.maskInputOptions,
o.sampling,
); );
const mediaInteractionHandler = initMediaInteractionObserver( const mediaInteractionHandler = initMediaInteractionObserver(
o.mediaInteractionCb, o.mediaInteractionCb,

View File

@@ -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'; import { PackFn, UnpackFn } from './packer/base';
export enum EventType { export enum EventType {
@@ -126,6 +131,28 @@ export type eventWithTime = event & {
export type blockClass = string | RegExp; 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<string, boolean | undefined>;
/**
* 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<T> = { export type recordOptions<T> = {
emit?: (e: T, isCheckout?: boolean) => void; emit?: (e: T, isCheckout?: boolean) => void;
checkoutEveryNth?: number; checkoutEveryNth?: number;
@@ -133,10 +160,13 @@ export type recordOptions<T> = {
blockClass?: blockClass; blockClass?: blockClass;
ignoreClass?: string; ignoreClass?: string;
maskAllInputs?: boolean; maskAllInputs?: boolean;
maskInputOptions?: MaskInputOptions;
inlineStylesheet?: boolean; inlineStylesheet?: boolean;
hooks?: hooksParam; hooks?: hooksParam;
mousemoveWait?: number;
packFn?: PackFn; packFn?: PackFn;
sampling?: SamplingStrategy;
// departed, please use sampling options
mousemoveWait?: number;
}; };
export type observerParam = { export type observerParam = {
@@ -149,10 +179,10 @@ export type observerParam = {
mediaInteractionCb: mediaInteractionCallback; mediaInteractionCb: mediaInteractionCallback;
blockClass: blockClass; blockClass: blockClass;
ignoreClass: string; ignoreClass: string;
maskAllInputs: boolean; maskInputOptions: MaskInputOptions;
inlineStylesheet: boolean; inlineStylesheet: boolean;
styleSheetRuleCb: styleSheetRuleCallback; styleSheetRuleCb: styleSheetRuleCallback;
mousemoveWait: number; sampling: SamplingStrategy;
}; };
export type hooksParam = { export type hooksParam = {

View File

@@ -2288,6 +2288,694 @@ exports[`mask 1`] = `
\\"id\\": 37 \\"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, \\"type\\": 3,
\\"data\\": { \\"data\\": {

View File

@@ -32,7 +32,8 @@ describe('record integration tests', function (this: ISuite) {
console.log(event); console.log(event);
window.snapshots.push(event); window.snapshots.push(event);
}, },
maskAllInputs: ${options.maskAllInputs} maskAllInputs: ${options.maskAllInputs},
maskInputOptions: ${JSON.stringify(options.maskAllInputs)}
}); });
</script> </script>
</body> </body>
@@ -166,6 +167,28 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots, __filename, 'mask'); 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 () => { it('should not record blocked elements and its child nodes', async () => {
const page: puppeteer.Page = await this.browser.newPage(); const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank'); await page.goto('about:blank');

View File

@@ -17,7 +17,8 @@
], ],
"arrow-parens": false, "arrow-parens": false,
"only-arrow-functions": false, "only-arrow-functions": false,
"max-line-length": false "max-line-length": false,
"no-empty": false
}, },
"rulesDirectory": [] "rulesDirectory": []
} }