From 6db7ad8d93a359dc3611c5ecfde3a3930b59af4e Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 24 Feb 2023 11:06:23 -0500 Subject: [PATCH] fix: masking inputs on change when `maskAllInputs:false` (#61) Since `maskInputSelector` is a new configuration item, we were not handling the case for input change when `maskAllInputs:false`. Before, input masking was only done via `maskInputOptions` and `maskAllInputs`. --- packages/rrweb-snapshot/src/utils.ts | 63 +- packages/rrweb-snapshot/typings/utils.d.ts | 17 +- packages/rrweb/src/record/observer.ts | 13 +- .../__snapshots__/integration.test.ts.snap | 1342 ++++++++++++++++- packages/rrweb/test/html/form-masked.html | 41 + packages/rrweb/test/html/form.html | 6 + packages/rrweb/test/integration.test.ts | 51 +- 7 files changed, 1468 insertions(+), 65 deletions(-) create mode 100644 packages/rrweb/test/html/form-masked.html diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index adbd8797b9..4a0be19701 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -9,6 +9,54 @@ export function isShadowRoot(n: Node): n is ShadowRoot { return Boolean(host && host.shadowRoot && host.shadowRoot === n); } +interface IsInputTypeMasked { + maskInputOptions: MaskInputOptions; + tagName: string; + type: string | number | boolean | null; +} + +/** + * Check `maskInputOptions` if the element, based on tag name and `type` attribute, should be masked. + * If `` has no `type`, default to using `type="text"`. + */ +function isInputTypeMasked({ + maskInputOptions, + tagName, + type, +}: IsInputTypeMasked) { + return ( + maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || + maskInputOptions[type as keyof MaskInputOptions] || + // Default to "text" option for inputs without a "type" attribute defined + (tagName === 'input' && !type && maskInputOptions['text']) + ); +} + +interface HasInputMaskOptions extends IsInputTypeMasked { + maskInputSelector: string | null; +} + +/** + * Determine if there are masking options configured and if `maskInputValue` needs to be called + */ +export function hasInputMaskOptions({ + tagName, + type, + maskInputOptions, + maskInputSelector, +}: HasInputMaskOptions) { + return ( + maskInputSelector || isInputTypeMasked({ maskInputOptions, tagName, type }) + ); +} + +interface MaskInputValue extends HasInputMaskOptions { + input: HTMLElement; + unmaskInputSelector: string | null; + value: string | null; + maskInputFn?: MaskInputFn; +} + export function maskInputValue({ input, maskInputSelector, @@ -18,16 +66,7 @@ export function maskInputValue({ type, value, maskInputFn, -}: { - input: HTMLElement; - maskInputSelector: string | null; - unmaskInputSelector: string | null; - maskInputOptions: MaskInputOptions; - tagName: string; - type: string | number | boolean | null; - value: string | null; - maskInputFn?: MaskInputFn; -}): string { +}: MaskInputValue): string { let text = value || ''; if (unmaskInputSelector && input.matches(unmaskInputSelector)) { @@ -35,9 +74,7 @@ export function maskInputValue({ } if ( - maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || - maskInputOptions[type as keyof MaskInputOptions] || - (tagName === 'input' && !type && maskInputOptions['text']) || // For inputs without a "type" attribute defined + isInputTypeMasked({ maskInputOptions, tagName, type }) || (maskInputSelector && input.matches(maskInputSelector)) ) { if (maskInputFn) { diff --git a/packages/rrweb-snapshot/typings/utils.d.ts b/packages/rrweb-snapshot/typings/utils.d.ts index 708fb96fa7..53b974b31a 100644 --- a/packages/rrweb-snapshot/typings/utils.d.ts +++ b/packages/rrweb-snapshot/typings/utils.d.ts @@ -1,14 +1,21 @@ import { INode, MaskInputFn, MaskInputOptions } from './types'; export declare function isElement(n: Node | INode): n is Element; export declare function isShadowRoot(n: Node): n is ShadowRoot; -export declare function maskInputValue({ input, maskInputSelector, unmaskInputSelector, maskInputOptions, tagName, type, value, maskInputFn, }: { - input: HTMLElement; - maskInputSelector: string | null; - unmaskInputSelector: string | null; +interface IsInputTypeMasked { maskInputOptions: MaskInputOptions; tagName: string; type: string | number | boolean | null; +} +interface HasInputMaskOptions extends IsInputTypeMasked { + maskInputSelector: string | null; +} +export declare function hasInputMaskOptions({ tagName, type, maskInputOptions, maskInputSelector, }: HasInputMaskOptions): string | boolean | undefined; +interface MaskInputValue extends HasInputMaskOptions { + input: HTMLElement; + unmaskInputSelector: string | null; value: string | null; maskInputFn?: MaskInputFn; -}): string; +} +export declare function maskInputValue({ input, maskInputSelector, unmaskInputSelector, maskInputOptions, tagName, type, value, maskInputFn, }: MaskInputValue): string; export declare function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean; +export {}; diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 24df529cfd..2158a6a9da 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -1,6 +1,6 @@ import { INode, - MaskInputOptions, + hasInputMaskOptions, maskInputValue, } from '@sentry-internal/rrweb-snapshot'; import { FontFaceSet } from 'css-font-loading-module'; @@ -365,15 +365,18 @@ function initInputObserver({ ) { return; } + let text = (target as HTMLInputElement).value; let isChecked = false; if (type === 'radio' || type === 'checkbox') { isChecked = (target as HTMLInputElement).checked; } else if ( - maskInputOptions[ - (target as Element).tagName.toLowerCase() as keyof MaskInputOptions - ] || - maskInputOptions[type as keyof MaskInputOptions] + hasInputMaskOptions({ + maskInputOptions, + maskInputSelector, + tagName: (target as HTMLElement).tagName, + type, + }) ) { text = maskInputValue({ input: target as HTMLElement, diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index e2da998fbd..86f4466ac9 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -1340,8 +1340,77 @@ exports[`record integration tests can record form interactions 1`] = ` }, { "type": 3, - "textContent": "\\n ", + "textContent": "\\n ", "id": 61 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "empty" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 63 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "id": "empty" + }, + "childNodes": [], + "id": 64 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 65 + } + ], + "id": 62 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 66 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "unmask" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 68 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "text", + "class": "rr-unmask" + }, + "childNodes": [], + "id": 69 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 70 + } + ], + "id": 67 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 71 } ], "id": 18 @@ -1349,7 +1418,7 @@ exports[`record integration tests can record form interactions 1`] = ` { "type": 3, "textContent": "\\n \\n ", - "id": 62 + "id": 72 }, { "type": 2, @@ -1359,15 +1428,15 @@ exports[`record integration tests can record form interactions 1`] = ` { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 64 + "id": 74 } ], - "id": 63 + "id": 73 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 65 + "id": 75 } ], "id": 16 @@ -3118,8 +3187,77 @@ exports[`record integration tests can use maskInputOptions to configure which ty }, { "type": 3, - "textContent": "\\n ", + "textContent": "\\n ", "id": 61 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "empty" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 63 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "id": "empty" + }, + "childNodes": [], + "id": 64 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 65 + } + ], + "id": 62 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 66 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "unmask" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 68 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "text", + "class": "rr-unmask" + }, + "childNodes": [], + "id": 69 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 70 + } + ], + "id": 67 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 71 } ], "id": 18 @@ -3127,7 +3265,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty { "type": 3, "textContent": "\\n \\n ", - "id": 62 + "id": 72 }, { "type": 2, @@ -3137,15 +3275,15 @@ exports[`record integration tests can use maskInputOptions to configure which ty { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 64 + "id": 74 } ], - "id": 63 + "id": 73 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 65 + "id": 75 } ], "id": 16 @@ -8039,8 +8177,77 @@ exports[`record integration tests should not record input values if maskAllInput }, { "type": 3, - "textContent": "\\n ", + "textContent": "\\n ", "id": 61 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "empty" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 63 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "id": "empty" + }, + "childNodes": [], + "id": 64 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 65 + } + ], + "id": 62 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 66 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "unmask" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 68 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "text", + "class": "rr-unmask" + }, + "childNodes": [], + "id": 69 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 70 + } + ], + "id": 67 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 71 } ], "id": 18 @@ -8048,7 +8255,7 @@ exports[`record integration tests should not record input values if maskAllInput { "type": 3, "textContent": "\\n \\n ", - "id": 62 + "id": 72 }, { "type": 2, @@ -8058,15 +8265,15 @@ exports[`record integration tests should not record input values if maskAllInput { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 64 + "id": 74 } ], - "id": 63 + "id": 73 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 65 + "id": 75 } ], "id": 16 @@ -8463,11 +8670,63 @@ exports[`record integration tests should not record input values if maskAllInput "isChecked": false, "id": 47 } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 6, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 64 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*", + "isChecked": false, + "id": 64 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "**", + "isChecked": false, + "id": 64 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "***", + "isChecked": false, + "id": 64 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "****", + "isChecked": false, + "id": 64 + } } ]" `; -exports[`record integration tests should not record textarea values if dynamically added and maskAllInputs is true 1`] = ` +exports[`record integration tests should not record input values on selectively masked elements when maskAllInputs is disabled 1`] = ` "[ { "type": 0, @@ -8546,13 +8805,961 @@ exports[`record integration tests should not record textarea values if dynamical }, { "type": 2, - "tagName": "title", - "attributes": {}, - "childNodes": [ - { - "type": 3, - "textContent": "Empty", - "id": 11 + "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", + "class": "rr-mask" + }, + "childNodes": [], + "id": 22 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 23 + } + ], + "id": 20 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 24 + }, + { + "type": 2, + "tagName": "label", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 26 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "radio", + "class": "rr-mask", + "name": "toggle", + "value": "on" + }, + "childNodes": [], + "id": 27 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 28 + } + ], + "id": 25 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 29 + }, + { + "type": 2, + "tagName": "label", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 31 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "radio", + "class": "rr-mask", + "name": "toggle", + "value": "off", + "checked": true + }, + "childNodes": [], + "id": 32 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 33 + } + ], + "id": 30 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 34 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "checkbox" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 36 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "checkbox", + "class": "rr-mask" + }, + "childNodes": [], + "id": 37 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 38 + } + ], + "id": 35 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 39 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "textarea" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 41 + }, + { + "type": 2, + "tagName": "textarea", + "attributes": { + "name": "", + "id": "", + "cols": "30", + "rows": "10", + "class": "rr-mask" + }, + "childNodes": [], + "id": 42 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 43 + } + ], + "id": 40 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 44 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "select" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 46 + }, + { + "type": 2, + "tagName": "select", + "attributes": { + "name": "", + "id": "", + "class": "rr-mask", + "value": "*" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 48 + }, + { + "type": 2, + "tagName": "option", + "attributes": { + "value": "1", + "selected": true + }, + "childNodes": [ + { + "type": 3, + "textContent": "*", + "id": 50 + } + ], + "id": 49 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 51 + }, + { + "type": 2, + "tagName": "option", + "attributes": { + "value": "2" + }, + "childNodes": [ + { + "type": 3, + "textContent": "*", + "id": 53 + } + ], + "id": 52 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 54 + } + ], + "id": 47 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 55 + } + ], + "id": 45 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 56 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "password" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 58 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "password", + "class": "rr-mask" + }, + "childNodes": [], + "id": 59 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 60 + } + ], + "id": 57 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 61 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "empty" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 63 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "id": "empty", + "class": "rr-mask" + }, + "childNodes": [], + "id": 64 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 65 + } + ], + "id": 62 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 66 + } + ], + "id": 18 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 67 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 69 + } + ], + "id": 68 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 70 + } + ], + "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": "*", + "isChecked": false, + "id": 22 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "**", + "isChecked": false, + "id": 22 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "***", + "isChecked": false, + "id": 22 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "****", + "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": 5, + "text": "off", + "isChecked": false, + "id": 32 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 1, + "id": 37 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 6, + "id": 27 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 37 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 0, + "id": 37 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 2, + "id": 37 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "on", + "isChecked": true, + "id": 37 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 6, + "id": 37 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 59 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*", + "isChecked": false, + "id": 59 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "**", + "isChecked": false, + "id": 59 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "***", + "isChecked": false, + "id": 59 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "****", + "isChecked": false, + "id": 59 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*****", + "isChecked": false, + "id": 59 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "******", + "isChecked": false, + "id": 59 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*******", + "isChecked": false, + "id": 59 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "********", + "isChecked": false, + "id": 59 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 6, + "id": 59 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "**", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "***", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "****", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*****", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "******", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*******", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "********", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*********", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "**********", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "***********", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "************", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*************", + "isChecked": false, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*", + "isChecked": false, + "id": 47 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 6, + "id": 42 + } + }, + { + "type": 3, + "data": { + "source": 2, + "type": 5, + "id": 64 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "*", + "isChecked": false, + "id": 64 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "**", + "isChecked": false, + "id": 64 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "***", + "isChecked": false, + "id": 64 + } + }, + { + "type": 3, + "data": { + "source": 5, + "text": "****", + "isChecked": false, + "id": 64 + } + } +]" +`; + +exports[`record integration tests should not record textarea values if dynamically added and maskAllInputs is true 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": "title", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "Empty", + "id": 11 } ], "id": 10 @@ -11088,8 +12295,77 @@ exports[`record integration tests should record input userTriggered values if us }, { "type": 3, - "textContent": "\\n ", + "textContent": "\\n ", "id": 61 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "empty" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 63 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "id": "empty" + }, + "childNodes": [], + "id": 64 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 65 + } + ], + "id": 62 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 66 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "unmask" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 68 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "text", + "class": "rr-unmask" + }, + "childNodes": [], + "id": 69 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 70 + } + ], + "id": 67 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 71 } ], "id": 18 @@ -11097,7 +12373,7 @@ exports[`record integration tests should record input userTriggered values if us { "type": 3, "textContent": "\\n \\n ", - "id": 62 + "id": 72 }, { "type": 2, @@ -11107,15 +12383,15 @@ exports[`record integration tests should record input userTriggered values if us { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 64 + "id": 74 } ], - "id": 63 + "id": 73 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 65 + "id": 75 } ], "id": 16 @@ -11954,7 +13230,7 @@ exports[`record integration tests should record input values if dynamically adde "attributes": { "id": "input-masked", "class": "rr-mask", - "value": "input should be masked" + "value": "**********************" }, "childNodes": [], "id": 21 @@ -11967,7 +13243,7 @@ exports[`record integration tests should record input values if dynamically adde "type": 3, "data": { "source": 5, - "text": "input should be masked", + "text": "**********************", "isChecked": false, "id": 21 } @@ -11984,7 +13260,7 @@ exports[`record integration tests should record input values if dynamically adde "type": 3, "data": { "source": 5, - "text": "input should be maskedm", + "text": "***********************", "isChecked": false, "id": 21 } @@ -11993,7 +13269,7 @@ exports[`record integration tests should record input values if dynamically adde "type": 3, "data": { "source": 5, - "text": "input should be maskedmo", + "text": "************************", "isChecked": false, "id": 21 } @@ -12002,7 +13278,7 @@ exports[`record integration tests should record input values if dynamically adde "type": 3, "data": { "source": 5, - "text": "input should be maskedmoo", + "text": "*************************", "isChecked": false, "id": 21 } diff --git a/packages/rrweb/test/html/form-masked.html b/packages/rrweb/test/html/form-masked.html new file mode 100644 index 0000000000..bdd57ef0b1 --- /dev/null +++ b/packages/rrweb/test/html/form-masked.html @@ -0,0 +1,41 @@ + + + + + + + form fields + + + +
+ + + + + + + + +
+ + diff --git a/packages/rrweb/test/html/form.html b/packages/rrweb/test/html/form.html index a89f11ff74..b8d8e36444 100644 --- a/packages/rrweb/test/html/form.html +++ b/packages/rrweb/test/html/form.html @@ -33,6 +33,12 @@ + + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index cd3530261d..8283fa482f 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -56,6 +56,7 @@ describe('record integration tests', function (this: ISuite) { blockSelector: ${JSON.stringify(options.blockSelector)}, maskAllInputs: ${options.maskAllInputs}, maskInputOptions: ${JSON.stringify(options.maskAllInputs)}, + maskInputSelector: ${JSON.stringify(options.maskInputSelector)}, userTriggeredOnInput: ${options.userTriggeredOnInput}, maskAllText: ${options.maskAllText}, maskTextFn: ${options.maskTextFn}, @@ -233,7 +234,32 @@ describe('record integration tests', function (this: ISuite) { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); await page.setContent( - getHtml.call(this, 'form.html', { maskAllInputs: true }), + getHtml.call(this, 'form.html', { + maskAllInputs: true, + unmaskTextSelector: '.rr-unmask', + }), + ); + + await page.type('input[type="text"]', 'test'); + await page.click('input[type="radio"]'); + await page.click('input[type="checkbox"]'); + await page.type('input[type="password"]', 'password'); + await page.type('textarea', 'textarea test'); + await page.select('select', '1'); + await page.type('#empty', 'test'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + + it('should not record input values on selectively masked elements when maskAllInputs is disabled', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'form-masked.html', { + maskAllInputs: false, + maskInputSelector: '.rr-mask', + }), ); await page.type('input[type="text"]', 'test'); @@ -242,6 +268,7 @@ describe('record integration tests', function (this: ISuite) { await page.type('input[type="password"]', 'password'); await page.type('textarea', 'textarea test'); await page.select('select', '1'); + await page.type('#empty', 'test'); const snapshots = await page.evaluate('window.snapshots'); assertSnapshot(snapshots); @@ -478,9 +505,11 @@ describe('record integration tests', function (this: ISuite) { it('should not record blocked elements from blockSelector, when dynamically added', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); - await page.setContent(getHtml.call(this, 'block.html', { - blockSelector: 'video' - })); + await page.setContent( + getHtml.call(this, 'block.html', { + blockSelector: 'video', + }), + ); await page.evaluate(() => { const el2 = document.createElement('video'); @@ -516,10 +545,12 @@ describe('record integration tests', function (this: ISuite) { it('should only record unblocked elements', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); - await page.setContent(getHtml.call(this, 'block.html', { - blockSelector: 'img,svg', - unblockSelector: '.rr-unblock', - })); + await page.setContent( + getHtml.call(this, 'block.html', { + blockSelector: 'img,svg', + unblockSelector: '.rr-unblock', + }), + ); const snapshots = await page.evaluate('window.snapshots'); assertSnapshot(snapshots); @@ -576,7 +607,9 @@ describe('record integration tests', function (this: ISuite) { }), ); await waitForRAF(page); - const snapshots = await page.evaluate('window.snapshots') as eventWithTime[]; + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; for (const event of snapshots) { if (event.type === EventType.FullSnapshot) { visitSnapshot(event.data.node, (n) => {