From 6c1232ccf4d5d1f980c3c1cba5863dc2fd3d9a90 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 6 Feb 2023 22:07:01 -0500 Subject: [PATCH 1/4] feat: Fix masking on textarea If textarea has a child string content, it will get duplicated as a `value` attribute. Masking by text vs input can cause these two values to diverge (i.e. only one is masked). On playback, we remove duplicate textContent for textareas, which means we could show double textarea values: one masked, one unmasked. --- packages/rrweb-snapshot/src/snapshot.ts | 22 +- packages/rrweb-snapshot/src/utils.ts | 1 + .../__snapshots__/integration.test.ts.snap | 409 +++++++++++++++++- packages/rrweb/test/html/mask-text.html | 1 + packages/rrweb/test/integration.test.ts | 14 + 5 files changed, 443 insertions(+), 4 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 7e4efe99db..2ac49d469f 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -263,7 +263,6 @@ export function transformAttribute( } else if (maskAllText && ['placeholder', 'title', 'aria-label'].indexOf(name) > -1) { return maskTextFn ? maskTextFn(value) : defaultMaskFn(value); } else { - console.log({maskAllText}) return value; } } @@ -332,6 +331,7 @@ export function needMaskingText( } // Can skip class/selector evaluations if `maskAllText` is true + // unless if (maskAllText) { return true; } @@ -688,6 +688,7 @@ function serializeNode( let textContent = (n as Text).textContent; const isStyle = parentTagName === 'STYLE' ? true : undefined; const isScript = parentTagName === 'SCRIPT' ? true : undefined; + if (isStyle && textContent) { try { // try to read style sheet @@ -709,10 +710,26 @@ function serializeNode( } textContent = absoluteToStylesheet(textContent, getHref()); } + if (isScript) { textContent = 'SCRIPT_PLACEHOLDER'; } - if ( + + if (parentTagName === 'TEXTAREA' && textContent) { + // Ensure that textContent === attribute.value + // (masking options can make them different) + // replay will remove duplicate textContent. + textContent = maskInputValue({ + input: n.parentNode as HTMLElement, + maskInputSelector, + unmaskInputSelector, + maskInputOptions, + tagName: parentTagName, + type: null, + value: textContent, + maskInputFn, + }); + } else if ( !isStyle && !isScript && needMaskingText( @@ -728,6 +745,7 @@ function serializeNode( ? maskTextFn(textContent) : defaultMaskFn(textContent); } + return { type: NodeType.Text, textContent: textContent || '', diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 15adc27590..4f9f6b6e1c 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -28,6 +28,7 @@ export function maskInputValue({ value: string | null; maskInputFn?: MaskInputFn; }): string { + console.log('maskInputValue', tagName) let text = value || ''; if (unmaskInputSelector && input.matches(unmaskInputSelector)) { diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 182480bfee..9262e9ab66 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -3869,9 +3869,29 @@ exports[`record integration tests should mask all text (except unmaskTextSelecto }, { "type": 3, - "textContent": "\\n \\n ", + "textContent": "\\n ", "id": 48 }, + { + "type": 2, + "tagName": "textarea", + "attributes": { + "value": "mask10" + }, + "childNodes": [ + { + "type": 3, + "textContent": "mask10", + "id": 50 + } + ], + "id": 49 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 51 + }, { "type": 2, "tagName": "script", @@ -3880,6 +3900,373 @@ exports[`record integration tests should mask all text (except unmaskTextSelecto { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", + "id": 53 + } + ], + "id": 52 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 54 + } + ], + "id": 44 + } + ], + "id": 16 + } + ], + "id": 3 + } + ], + "id": 1 + }, + "initialOffset": { + "left": 0, + "top": 0 + } + } + } +]" +`; + +exports[`record integration tests should mask only inputs 1`] = ` +"[ + { + "type": 0, + "data": {} + }, + { + "type": 1, + "data": {} + }, + { + "type": 4, + "data": { + "href": "about:blank", + "width": 1920, + "height": 1080 + } + }, + { + "type": 2, + "data": { + "node": { + "type": 0, + "childNodes": [ + { + "type": 1, + "name": "html", + "publicId": "", + "systemId": "", + "id": 2 + }, + { + "type": 2, + "tagName": "html", + "attributes": { + "lang": "en" + }, + "childNodes": [ + { + "type": 2, + "tagName": "head", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 5 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "charset": "UTF-8" + }, + "childNodes": [], + "id": 6 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 7 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "name": "viewport", + "content": "width=device-width, initial-scale=1.0" + }, + "childNodes": [], + "id": 8 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 9 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "http-equiv": "X-UA-Compatible", + "content": "ie=edge" + }, + "childNodes": [], + "id": 10 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 11 + }, + { + "type": 2, + "tagName": "title", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "Mask text", + "id": 13 + } + ], + "id": 12 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 14 + } + ], + "id": 4 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 15 + }, + { + "type": 2, + "tagName": "body", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 17 + }, + { + "type": 2, + "tagName": "p", + "attributes": { + "class": "rr-mask" + }, + "childNodes": [ + { + "type": 3, + "textContent": "*****", + "id": 19 + } + ], + "id": 18 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 20 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "class": "rr-mask" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 22 + }, + { + "type": 2, + "tagName": "span", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "*****", + "id": 24 + } + ], + "id": 23 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 25 + } + ], + "id": 21 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 26 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "data-masking": "true" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 28 + }, + { + "type": 2, + "tagName": "div", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 30 + }, + { + "type": 2, + "tagName": "div", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "mask3", + "id": 32 + } + ], + "id": 31 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 33 + } + ], + "id": 29 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 34 + } + ], + "id": 27 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 35 + }, + { + "type": 2, + "tagName": "div", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n mask4\\n ", + "id": 37 + } + ], + "id": 36 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 38 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "class": "rr-unmask" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n mask5\\n ", + "id": 40 + } + ], + "id": 39 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 41 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "placeholder": "mask6" + }, + "childNodes": [], + "id": 42 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 43 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "title": "mask7" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 45 + }, + { + "type": 2, + "tagName": "button", + "attributes": { + "aria-label": "mask8" + }, + "childNodes": [ + { + "type": 3, + "textContent": "mask9", + "id": 47 + } + ], + "id": 46 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 48 + }, + { + "type": 2, + "tagName": "textarea", + "attributes": { + "value": "******" + }, + "childNodes": [ + { + "type": 3, + "textContent": "******", "id": 50 } ], @@ -3887,8 +4274,26 @@ exports[`record integration tests should mask all text (except unmaskTextSelecto }, { "type": 3, - "textContent": "\\n \\n \\n\\n", + "textContent": "\\n \\n ", "id": 51 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 53 + } + ], + "id": 52 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 54 } ], "id": 44 diff --git a/packages/rrweb/test/html/mask-text.html b/packages/rrweb/test/html/mask-text.html index 502d529729..7610310985 100644 --- a/packages/rrweb/test/html/mask-text.html +++ b/packages/rrweb/test/html/mask-text.html @@ -25,5 +25,6 @@
+ diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index b418b40617..04d7516ed2 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -590,6 +590,20 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it.only('should mask only inputs', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'mask-text.html', { + maskAllInputs: true, + maskAllText: false, + }), + ); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + it('should mask all text (except unmaskTextSelector), using maskAllText ', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); From d5608ff4e165de3b3d462549743ba99d8df50c6e Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 6 Feb 2023 22:07:56 -0500 Subject: [PATCH 2/4] extra comment --- packages/rrweb-snapshot/src/snapshot.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 2ac49d469f..78a1affc59 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -331,7 +331,6 @@ export function needMaskingText( } // Can skip class/selector evaluations if `maskAllText` is true - // unless if (maskAllText) { return true; } From b6d71a7f75614c8398964a3c02ddd9224443f446 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 7 Feb 2023 21:18:31 -0500 Subject: [PATCH 3/4] remove debug stuff --- packages/rrweb-snapshot/src/utils.ts | 1 - packages/rrweb/test/integration.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 4f9f6b6e1c..15adc27590 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -28,7 +28,6 @@ export function maskInputValue({ value: string | null; maskInputFn?: MaskInputFn; }): string { - console.log('maskInputValue', tagName) let text = value || ''; if (unmaskInputSelector && input.matches(unmaskInputSelector)) { diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 04d7516ed2..1bccbabba5 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -590,7 +590,7 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); - it.only('should mask only inputs', async () => { + it('should mask only inputs', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); await page.setContent( From c8eabcad5d01a23f0b27588800d02531273a7a51 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 7 Feb 2023 21:26:45 -0500 Subject: [PATCH 4/4] missed snapshots --- .../__snapshots__/integration.test.ts.snap | 56 ++++++++++++++++--- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 9262e9ab66..a475d13218 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -4639,9 +4639,29 @@ exports[`record integration tests should mask texts 1`] = ` }, { "type": 3, - "textContent": "\\n \\n ", + "textContent": "\\n ", "id": 48 }, + { + "type": 2, + "tagName": "textarea", + "attributes": { + "value": "mask10" + }, + "childNodes": [ + { + "type": 3, + "textContent": "mask10", + "id": 50 + } + ], + "id": 49 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 51 + }, { "type": 2, "tagName": "script", @@ -4650,15 +4670,15 @@ exports[`record integration tests should mask texts 1`] = ` { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 50 + "id": 53 } ], - "id": 49 + "id": 52 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 51 + "id": 54 } ], "id": 44 @@ -5004,9 +5024,29 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` }, { "type": 3, - "textContent": "\\n \\n ", + "textContent": "\\n ", "id": 48 }, + { + "type": 2, + "tagName": "textarea", + "attributes": { + "value": "mask10" + }, + "childNodes": [ + { + "type": 3, + "textContent": "mask10", + "id": 50 + } + ], + "id": 49 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 51 + }, { "type": 2, "tagName": "script", @@ -5015,15 +5055,15 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 50 + "id": 53 } ], - "id": 49 + "id": 52 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 51 + "id": 54 } ], "id": 44