diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 1c91318e16..5a2107e7d7 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -566,6 +566,11 @@ function serializeNode( keepIframeSrcFn, newlyAddedElement, rootId, + maskAllText, + maskTextClass, + unmaskTextClass, + maskTextSelector, + unmaskTextSelector, }); case n.TEXT_NODE: return serializeTextNode(n as Text, { @@ -695,6 +700,11 @@ function serializeElementNode( */ newlyAddedElement?: boolean; rootId: number | undefined; + maskAllText: boolean; + maskTextClass: string | RegExp; + unmaskTextClass: string | RegExp; + maskTextSelector: string | null; + unmaskTextSelector: string | null; }, ): serializedNode | false { const { @@ -710,6 +720,11 @@ function serializeElementNode( keepIframeSrcFn, newlyAddedElement = false, rootId, + maskAllText, + maskTextClass, + unmaskTextClass, + maskTextSelector, + unmaskTextSelector, } = options; const needBlock = _isBlockedElement(n, blockClass, blockSelector); const tagName = getValidTagName(n); @@ -766,12 +781,21 @@ function serializeElementNode( attributes.type !== 'button' && value ) { + const forceMask = needMaskingText( + n, + maskTextClass, + maskTextSelector, + unmaskTextClass, + unmaskTextSelector, + maskAllText, + ); attributes.value = maskInputValue({ type: attributes.type, tagName, value, maskInputOptions, maskInputFn, + forceMask, }); } else if (checked) { attributes.checked = checked; diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 1c8c757363..0115fb47f4 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -159,17 +159,20 @@ export function maskInputValue({ type, value, maskInputFn, + forceMask, }: { maskInputOptions: MaskInputOptions; tagName: string; type: string | number | boolean | null; value: string | null; maskInputFn?: MaskInputFn; + forceMask?: boolean; }): string { let text = value || ''; if ( maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || - maskInputOptions[type as keyof MaskInputOptions] + maskInputOptions[type as keyof MaskInputOptions] || + forceMask ) { if (maskInputFn) { text = maskInputFn(text); diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 30e74e9f3f..e8a81e6c33 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -371,6 +371,7 @@ function record( unmaskTextSelector, inlineStylesheet, maskAllInputs: maskInputOptions, + maskInputFn, maskTextFn, slimDOM: slimDOMOptions, dataURLOptions, diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 9b4df22d97..6b30acee3c 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -500,12 +500,21 @@ export default class MutationBuffer { const target = m.target as HTMLElement; let value = (m.target as HTMLElement).getAttribute(m.attributeName!); if (m.attributeName === 'value') { + const forceMask = needMaskingText( + m.target, + this.maskTextClass, + this.maskTextSelector, + this.unmaskTextClass, + this.unmaskTextSelector, + this.maskAllText, + ); value = maskInputValue({ maskInputOptions: this.maskInputOptions, tagName: (m.target as HTMLElement).tagName, type: (m.target as HTMLElement).getAttribute('type'), value, maskInputFn: this.maskInputFn, + forceMask, }); } if ( diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 2360078445..c9f575c140 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -1,4 +1,9 @@ -import { MaskInputOptions, maskInputValue, Mirror } from 'rrweb-snapshot'; +import { + MaskInputOptions, + maskInputValue, + Mirror, + needMaskingText, +} from 'rrweb-snapshot'; import type { FontFaceSet } from 'css-font-loading-module'; import { throttle, @@ -336,6 +341,11 @@ function initInputObserver({ maskInputFn, sampling, userTriggeredOnInput, + maskAllText, + maskTextClass, + unmaskTextClass, + maskTextSelector, + unmaskTextSelector, }: observerParam): listenerHandler { function eventHandler(event: Event) { let target = getEventTarget(event); @@ -360,13 +370,22 @@ function initInputObserver({ } let text = (target as HTMLInputElement).value; let isChecked = false; + const forceMask = needMaskingText( + target as Node, + maskTextClass, + maskTextSelector, + unmaskTextClass, + unmaskTextSelector, + maskAllText, + ); 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] + maskInputOptions[type as keyof MaskInputOptions] || + forceMask ) { text = maskInputValue({ maskInputOptions, @@ -374,6 +393,7 @@ function initInputObserver({ type, value: text, maskInputFn, + forceMask, }); } cbWithDedup( diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index df9fe7733c..207dfb76d4 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -281,6 +281,29 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('can use maskTextSelector to configure which inputs should be masked', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'form.html', { + maskTextSelector: 'input[type="text"],textarea', + maskInputFn: () => '*'.repeat(10), + }), + ); + + 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.type('input[type="password"]', 'password'); + await page.select('select', '1'); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + it('should mask value attribute with maskInputOptions', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank'); diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 12fa37517c..d0da66306a 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -597,6 +597,7 @@ export function generateRecordSnippet(options: recordOptions) { maskTextSelector: ${JSON.stringify(options.maskTextSelector)}, maskAllInputs: ${options.maskAllInputs}, maskInputOptions: ${JSON.stringify(options.maskAllInputs)}, + maskInputFn: ${options.maskInputFn}, userTriggeredOnInput: ${options.userTriggeredOnInput}, maskTextFn: ${options.maskTextFn}, recordCanvas: ${options.recordCanvas},