From e812910acf153c7a087c992cc7305fd30e3ca398 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 22 Feb 2023 17:31:51 -0500 Subject: [PATCH 1/5] test: Fix masking inputs on change when `maskAllInputs:false` 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/src/record/observer.ts | 8 +- .../__snapshots__/integration.test.ts.snap | 861 ++++++++++++++++++ packages/rrweb/test/html/form-masked.html | 38 + packages/rrweb/test/integration.test.ts | 19 + 4 files changed, 920 insertions(+), 6 deletions(-) create mode 100644 packages/rrweb/test/html/form-masked.html diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 24df529cfd..98983af9b1 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -365,16 +365,12 @@ 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] - ) { + } else { text = maskInputValue({ input: target as HTMLElement, maskInputOptions, diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 031226dddf..c6b0f88708 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -8235,6 +8235,867 @@ exports[`record integration tests should not record input values if maskAllInput ]" `; +exports[`record integration tests should not record input values on selectively masked elements when maskAllInputs is disabled 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", + "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 + } + ], + "id": 18 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 62 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 64 + } + ], + "id": 63 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 65 + } + ], + "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 + } + } +]" +`; + exports[`record integration tests should only record unblocked elements 1`] = ` "[ { diff --git a/packages/rrweb/test/html/form-masked.html b/packages/rrweb/test/html/form-masked.html new file mode 100644 index 0000000000..142c3f82b4 --- /dev/null +++ b/packages/rrweb/test/html/form-masked.html @@ -0,0 +1,38 @@ + + + + + + + form fields + + + +
+ + + + + + + +
+ + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index fa6a00f5e5..1d7965e849 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -49,6 +49,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}, @@ -239,6 +240,24 @@ describe('record integration tests', function (this: ISuite) { 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'); + 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'); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + it('can use maskInputOptions to configure which type of inputs should be masked', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); From 2ba61fabdfd207d1f129d481400d215a51acbfc6 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 23 Feb 2023 11:16:02 -0500 Subject: [PATCH 2/5] refactor utility functions and check if there are masking options involved before attempting to mask --- packages/rrweb-snapshot/src/snapshot.ts | 29 +- packages/rrweb-snapshot/src/utils.ts | 62 ++- packages/rrweb/src/record/observer.ts | 11 +- .../__snapshots__/integration.test.ts.snap | 465 +++++++++++++++++- packages/rrweb/test/html/form-masked.html | 3 + packages/rrweb/test/html/form.html | 6 + packages/rrweb/test/integration.test.ts | 34 +- 7 files changed, 555 insertions(+), 55 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 0f2af24876..19375d6275 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -260,7 +260,10 @@ export function transformAttribute( return absoluteToStylesheet(value, getHref()); } else if (tagName === 'object' && name === 'data' && value) { return absoluteToDoc(doc, value); - } else if (maskAllText && ['placeholder', 'title', 'aria-label'].indexOf(name) > -1) { + } else if ( + maskAllText && + ['placeholder', 'title', 'aria-label'].indexOf(name) > -1 + ) { return maskTextFn ? maskTextFn(value) : defaultMaskFn(value); } else { return value; @@ -501,7 +504,14 @@ function serializeNode( let attributes: attributes = {}; for (const { name, value } of Array.from((n as HTMLElement).attributes)) { if (!skipAttribute(tagName, name, value)) { - attributes[name] = transformAttribute(doc, tagName, name, value, maskAllText, maskTextFn); + attributes[name] = transformAttribute( + doc, + tagName, + name, + value, + maskAllText, + maskTextFn, + ); } } // remote css @@ -793,7 +803,8 @@ function slimDOMExcluded( (sn.tagName === 'script' || // (module)preload link (sn.tagName === 'link' && - (sn.attributes.rel === 'preload' || sn.attributes.rel === 'modulepreload') && + (sn.attributes.rel === 'preload' || + sn.attributes.rel === 'modulepreload') && sn.attributes.as === 'script') || // prefetch link (sn.tagName === 'link' && @@ -1247,6 +1258,12 @@ export function cleanupSnapshot() { export default snapshot; /** We want to skip `autoplay` attribute, as this has weird results when replaying. */ -function skipAttribute(tagName: string, attributeName: string, value?: unknown) { - return (tagName === 'video' || tagName === 'audio') && attributeName === 'autoplay'; -} \ No newline at end of file +function skipAttribute( + tagName: string, + attributeName: string, + value?: unknown, +) { + return ( + (tagName === 'video' || tagName === 'audio') && attributeName === 'autoplay' + ); +} diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 15adc27590..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,8 +74,7 @@ export function maskInputValue({ } if ( - maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || - maskInputOptions[type as keyof MaskInputOptions] || + isInputTypeMasked({ maskInputOptions, tagName, type }) || (maskInputSelector && input.matches(maskInputSelector)) ) { if (maskInputFn) { diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 98983af9b1..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'; @@ -370,7 +370,14 @@ function initInputObserver({ let isChecked = false; if (type === 'radio' || type === 'checkbox') { isChecked = (target as HTMLInputElement).checked; - } else { + } else if ( + hasInputMaskOptions({ + maskInputOptions, + maskInputSelector, + tagName: (target as HTMLElement).tagName, + type, + }) + ) { text = maskInputValue({ input: target as HTMLElement, maskInputOptions, diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index c6b0f88708..31dcb1a410 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 @@ -7807,8 +7945,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 @@ -7816,7 +8023,7 @@ exports[`record integration tests should not record input values if maskAllInput { "type": 3, "textContent": "\\n \\n ", - "id": 62 + "id": 72 }, { "type": 2, @@ -7826,15 +8033,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 @@ -8231,6 +8438,58 @@ 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 + } } ]" `; @@ -8668,8 +8927,43 @@ exports[`record integration tests should not record input values on selectively }, { "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", + "class": "rr-mask" + }, + "childNodes": [], + "id": 64 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 65 + } + ], + "id": 62 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 66 } ], "id": 18 @@ -8677,7 +8971,7 @@ exports[`record integration tests should not record input values on selectively { "type": 3, "textContent": "\\n \\n ", - "id": 62 + "id": 67 }, { "type": 2, @@ -8687,15 +8981,15 @@ exports[`record integration tests should not record input values on selectively { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 64 + "id": 69 } ], - "id": 63 + "id": 68 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 65 + "id": 70 } ], "id": 16 @@ -9092,6 +9386,58 @@ exports[`record integration tests should not record input values on selectively "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 + } } ]" `; @@ -11474,8 +11820,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 @@ -11483,7 +11898,7 @@ exports[`record integration tests should record input userTriggered values if us { "type": 3, "textContent": "\\n \\n ", - "id": 62 + "id": 72 }, { "type": 2, @@ -11493,15 +11908,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 diff --git a/packages/rrweb/test/html/form-masked.html b/packages/rrweb/test/html/form-masked.html index 142c3f82b4..bdd57ef0b1 100644 --- a/packages/rrweb/test/html/form-masked.html +++ b/packages/rrweb/test/html/form-masked.html @@ -33,6 +33,9 @@ + 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 1d7965e849..3e9cb90d49 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -226,7 +226,10 @@ 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'); @@ -235,6 +238,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); @@ -244,7 +248,10 @@ 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-masked.html', { maskAllInputs: false, maskInputSelector: '.rr-mask' }), + getHtml.call(this, 'form-masked.html', { + maskAllInputs: false, + maskInputSelector: '.rr-mask', + }), ); await page.type('input[type="text"]', 'test'); @@ -253,6 +260,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); @@ -353,9 +361,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'); @@ -391,10 +401,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); @@ -451,7 +463,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) => { From f433d2abf95b2d61744c9bada417bc7703d3d973 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 23 Feb 2023 12:57:17 -0500 Subject: [PATCH 3/5] undo prettier --- packages/rrweb-snapshot/src/snapshot.ts | 29 +++++-------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 19375d6275..0f2af24876 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -260,10 +260,7 @@ export function transformAttribute( return absoluteToStylesheet(value, getHref()); } else if (tagName === 'object' && name === 'data' && value) { return absoluteToDoc(doc, value); - } else if ( - maskAllText && - ['placeholder', 'title', 'aria-label'].indexOf(name) > -1 - ) { + } else if (maskAllText && ['placeholder', 'title', 'aria-label'].indexOf(name) > -1) { return maskTextFn ? maskTextFn(value) : defaultMaskFn(value); } else { return value; @@ -504,14 +501,7 @@ function serializeNode( let attributes: attributes = {}; for (const { name, value } of Array.from((n as HTMLElement).attributes)) { if (!skipAttribute(tagName, name, value)) { - attributes[name] = transformAttribute( - doc, - tagName, - name, - value, - maskAllText, - maskTextFn, - ); + attributes[name] = transformAttribute(doc, tagName, name, value, maskAllText, maskTextFn); } } // remote css @@ -803,8 +793,7 @@ function slimDOMExcluded( (sn.tagName === 'script' || // (module)preload link (sn.tagName === 'link' && - (sn.attributes.rel === 'preload' || - sn.attributes.rel === 'modulepreload') && + (sn.attributes.rel === 'preload' || sn.attributes.rel === 'modulepreload') && sn.attributes.as === 'script') || // prefetch link (sn.tagName === 'link' && @@ -1258,12 +1247,6 @@ export function cleanupSnapshot() { export default snapshot; /** We want to skip `autoplay` attribute, as this has weird results when replaying. */ -function skipAttribute( - tagName: string, - attributeName: string, - value?: unknown, -) { - return ( - (tagName === 'video' || tagName === 'audio') && attributeName === 'autoplay' - ); -} +function skipAttribute(tagName: string, attributeName: string, value?: unknown) { + return (tagName === 'video' || tagName === 'audio') && attributeName === 'autoplay'; +} \ No newline at end of file From 136c45d67fd1de7a3bfb8a719bb73d1b7db9edd0 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 24 Feb 2023 10:56:03 -0500 Subject: [PATCH 4/5] update snapshots, masking fix from a different PR --- .../rrweb/test/__snapshots__/integration.test.ts.snap | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index b0b8252ffa..86f4466ac9 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -13230,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 @@ -13243,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 } @@ -13260,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 } @@ -13269,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 } @@ -13278,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 } From 7033081af4c0a078dda66dd98b974831869ef09e Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 24 Feb 2023 10:56:30 -0500 Subject: [PATCH 5/5] update types --- packages/rrweb-snapshot/typings/utils.d.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) 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 {};