From bbf38bf1837f02dc7f7c7e3bf1b63df7804ede25 Mon Sep 17 00:00:00 2001 From: Michael Dellanoce Date: Thu, 12 Jan 2023 12:54:17 -0500 Subject: [PATCH] apply text mask settings to inputs #1096 --- packages/rrweb-snapshot/src/snapshot.ts | 22 +- packages/rrweb-snapshot/src/utils.ts | 5 +- packages/rrweb/src/record/index.ts | 1 + packages/rrweb/src/record/mutation.ts | 8 + packages/rrweb/src/record/observer.ts | 14 +- .../__snapshots__/integration.test.ts.snap | 668 ++++++++++++++++++ packages/rrweb/test/integration.test.ts | 23 + packages/rrweb/test/utils.ts | 1 + 8 files changed, 738 insertions(+), 4 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index abb872b006..509912cad7 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -322,6 +322,7 @@ export function needMaskingText( ? (node as HTMLElement) : node.parentElement; if (el === null) return false; + if (maskTextSelector === '*') return true; if (typeof maskTextClass === 'string') { if (checkAncestors) { if (el.closest(`.${maskTextClass}`)) return true; @@ -504,11 +505,14 @@ function serializeNode( keepIframeSrcFn, newlyAddedElement, rootId, + needsMask, }); case n.TEXT_NODE: return serializeTextNode(n as Text, { needsMask, maskTextFn, + maskInputOptions, + maskInputFn, rootId, }); case n.CDATA_SECTION_NODE: @@ -539,16 +543,20 @@ function serializeTextNode( options: { needsMask: boolean | undefined; maskTextFn: MaskTextFn | undefined; + maskInputOptions: MaskInputOptions; + maskInputFn: MaskInputFn | undefined; rootId: number | undefined; }, ): serializedNode { - const { needsMask, maskTextFn, rootId } = options; + const { needsMask, maskTextFn, maskInputOptions, maskInputFn, rootId } = + options; // The parent node may not be a html element which has a tagName attribute. // So just let it be undefined which is ok in this use case. const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName; let textContent = n.textContent; const isStyle = parentTagName === 'STYLE' ? true : undefined; const isScript = parentTagName === 'SCRIPT' ? true : undefined; + const isTextarea = parentTagName === 'TEXTAREA' ? true : undefined; if (isStyle && textContent) { try { // try to read style sheet @@ -578,6 +586,11 @@ function serializeTextNode( ? maskTextFn(textContent, n.parentElement) : textContent.replace(/[\S]/g, '*'); } + if (isTextarea && textContent && maskInputOptions.textarea) { + textContent = maskInputFn + ? maskInputFn(textContent, n.parentNode as HTMLElement) + : textContent.replace(/[\S]/g, '*'); + } return { type: NodeType.Text, @@ -605,6 +618,7 @@ function serializeElementNode( */ newlyAddedElement?: boolean; rootId: number | undefined; + needsMask?: boolean; }, ): serializedNode | false { const { @@ -620,6 +634,7 @@ function serializeElementNode( keepIframeSrcFn, newlyAddedElement = false, rootId, + needsMask, } = options; const needBlock = _isBlockedElement(n, blockClass, blockSelector); const tagName = getValidTagName(n); @@ -676,6 +691,8 @@ function serializeElementNode( attributes.type !== 'button' && value ) { + const type = getInputType(n); + attributes.value = maskInputValue({ element: n, type: getInputType(n), @@ -683,6 +700,7 @@ function serializeElementNode( value, maskInputOptions, maskInputFn, + needsMask, }); } else if (checked) { attributes.checked = checked; @@ -1247,7 +1265,7 @@ function snapshot( inlineStylesheet?: boolean; maskAllInputs?: boolean | MaskInputOptions; maskTextFn?: MaskTextFn; - maskInputFn?: MaskTextFn; + maskInputFn?: MaskInputFn; slimDOM?: 'all' | boolean | SlimDOMOptions; dataURLOptions?: DataURLOptions; inlineImages?: boolean; diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 25c8f9acbd..8671abdaaa 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -248,6 +248,7 @@ export function maskInputValue({ type, value, maskInputFn, + needsMask, }: { element: HTMLElement; maskInputOptions: MaskInputOptions; @@ -255,13 +256,15 @@ export function maskInputValue({ type: string | null; value: string | null; maskInputFn?: MaskInputFn; + needsMask?: boolean; }): string { let text = value || ''; const actualType = type && toLowerCase(type); if ( maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || - (actualType && maskInputOptions[actualType as keyof MaskInputOptions]) + (actualType && maskInputOptions[actualType as keyof MaskInputOptions]) || + needsMask ) { if (maskInputFn) { text = maskInputFn(text, element); diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index f0c5c4a969..00b43fd67e 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -368,6 +368,7 @@ function record( maskTextSelector, 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 5c126555e8..d6524e91a8 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -568,6 +568,13 @@ export default class MutationBuffer { if (attributeName === 'value') { const type = getInputType(target); + const needsMask = needMaskingText( + m.target, + this.maskTextClass, + this.maskTextSelector, + true, + ); + value = maskInputValue({ element: target, maskInputOptions: this.maskInputOptions, @@ -575,6 +582,7 @@ export default class MutationBuffer { type, value, maskInputFn: this.maskInputFn, + needsMask, }); } if ( diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 5c8bbb069c..1162317a4c 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -6,6 +6,7 @@ import { toLowerCase, getNative, nativeSetTimeout, + needMaskingText, } from 'rrweb-snapshot'; import type { FontFaceSet } from 'css-font-loading-module'; import { @@ -404,6 +405,8 @@ function initInputObserver({ maskInputFn, sampling, userTriggeredOnInput, + maskTextClass, + maskTextSelector, }: observerParam): listenerHandler { function eventHandler(event: Event) { let target = getEventTarget(event) as HTMLElement | null; @@ -436,11 +439,19 @@ function initInputObserver({ let isChecked = false; const type: Lowercase = getInputType(target) || ''; + const needsMask = needMaskingText( + target as Node, + maskTextClass, + maskTextSelector, + true, + ); + if (type === 'radio' || type === 'checkbox') { isChecked = (target as HTMLInputElement).checked; } else if ( maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || - maskInputOptions[type as keyof MaskInputOptions] + maskInputOptions[type as keyof MaskInputOptions] || + needsMask ) { text = maskInputValue({ element: target, @@ -449,6 +460,7 @@ function initInputObserver({ type, value: text, maskInputFn, + needsMask, }); } cbWithDedup( diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index ae1aa7bf54..7b230846d1 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -4960,6 +4960,665 @@ exports[`record integration tests can use maskInputOptions to configure which ty ]" `; +exports[`record integration tests can use maskTextSelector to configure which inputs should be masked 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\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"radio\\", + \\"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\\", + \\"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\\" + }, + \\"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\\", + \\"data-unmask-example\\": \\"true\\" + }, + \\"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\\": \\"\\", + \\"value\\": \\"1\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 48 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"1\\", + \\"selected\\": true + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"1\\", + \\"id\\": 50 + } + ], + \\"id\\": 49 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 51 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"option\\", + \\"attributes\\": { + \\"value\\": \\"2\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"2\\", + \\"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\\" + }, + \\"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\\": 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, + \\"pointerType\\": 0 + } + }, + { + \\"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, + \\"pointerType\\": 0 + } + }, + { + \\"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\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**********\\", + \\"isChecked\\": false, + \\"id\\": 42 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 6, + \\"id\\": 42 + } + }, + { + \\"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\\": \\"1\\", + \\"isChecked\\": false, + \\"id\\": 47 + } + } +]" +`; + exports[`record integration tests handles null attribute values 1`] = ` "[ { @@ -9785,6 +10444,15 @@ exports[`record integration tests should not record input values if dynamically \\"id\\": 21 } }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 3, + \\"id\\": 21, + \\"x\\": 20, + \\"y\\": 0 + } + }, { \\"type\\": 3, \\"data\\": { diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 616b6e91cb..59656f8c4f 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -425,6 +425,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 password 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 30a36abfcb..eccb2b5f32 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -694,6 +694,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}, maskTextClass: ${options.maskTextClass}, maskTextFn: ${options.maskTextFn},