Skip to content

Commit

Permalink
apply text mask settings to inputs rrweb-io#1096
Browse files Browse the repository at this point in the history
  • Loading branch information
mdellanoce committed Oct 27, 2023
1 parent 3bfa800 commit 37c9638
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 3 deletions.
39 changes: 38 additions & 1 deletion packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,11 @@ function serializeNode(
keepIframeSrcFn,
newlyAddedElement,
rootId,
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
});
case n.TEXT_NODE:
return serializeTextNode(n as Text, {
Expand All @@ -579,6 +584,8 @@ function serializeNode(
maskTextSelector,
unmaskTextSelector,
maskTextFn,
maskInputOptions,
maskInputFn,
rootId,
});
case n.CDATA_SECTION_NODE:
Expand Down Expand Up @@ -613,6 +620,8 @@ function serializeTextNode(
maskTextSelector: string | null;
unmaskTextSelector: string | null;
maskTextFn: MaskTextFn | undefined;
maskInputOptions: MaskInputOptions;
maskInputFn: MaskInputFn | undefined;
rootId: number | undefined;
},
): serializedNode {
Expand All @@ -623,6 +632,8 @@ function serializeTextNode(
maskTextSelector,
unmaskTextSelector,
maskTextFn,
maskInputOptions,
maskInputFn,
rootId,
} = options;
// The parent node may not be a html element which has a tagName attribute.
Expand All @@ -631,6 +642,7 @@ function serializeTextNode(
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
Expand Down Expand Up @@ -672,6 +684,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,
Expand Down Expand Up @@ -699,6 +716,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 {
Expand All @@ -714,6 +736,11 @@ function serializeElementNode(
keepIframeSrcFn,
newlyAddedElement = false,
rootId,
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
} = options;
const needBlock = _isBlockedElement(n, blockClass, blockSelector);
const tagName = getValidTagName(n);
Expand Down Expand Up @@ -771,13 +798,23 @@ function serializeElementNode(
value
) {
const type = getInputType(n);
const forceMask = needMaskingText(
n,
maskTextClass,
maskTextSelector,
unmaskTextClass,
unmaskTextSelector,
maskAllText,
);

attributes.value = maskInputValue({
element: n,
type,
tagName,
value,
maskInputOptions,
maskInputFn,
forceMask,
});
} else if (checked) {
attributes.checked = checked;
Expand Down Expand Up @@ -1318,7 +1355,7 @@ function snapshot(
inlineStylesheet?: boolean;
maskAllInputs?: boolean | MaskInputOptions;
maskTextFn?: MaskTextFn;
maskInputFn?: MaskTextFn;
maskInputFn?: MaskInputFn;
slimDOM?: 'all' | boolean | SlimDOMOptions;
dataURLOptions?: DataURLOptions;
inlineImages?: boolean;
Expand Down
5 changes: 4 additions & 1 deletion packages/rrweb-snapshot/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,20 +219,23 @@ export function maskInputValue({
type,
value,
maskInputFn,
forceMask,
}: {
element: HTMLElement;
maskInputOptions: MaskInputOptions;
tagName: string;
type: string | null;
value: string | null;
maskInputFn?: MaskInputFn;
forceMask?: 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]) ||
forceMask
) {
if (maskInputFn) {
text = maskInputFn(text, element);
Expand Down
1 change: 1 addition & 0 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ function record<T = eventWithTime>(
unmaskTextSelector,
inlineStylesheet,
maskAllInputs: maskInputOptions,
maskInputFn,
maskTextFn,
slimDOM: slimDOMOptions,
dataURLOptions,
Expand Down
10 changes: 10 additions & 0 deletions packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,13 +550,23 @@ export default class MutationBuffer {
if (attributeName === 'value') {
const type = getInputType(target);

const forceMask = needMaskingText(
m.target,
this.maskTextClass,
this.maskTextSelector,
this.unmaskTextClass,
this.unmaskTextSelector,
this.maskAllText,
);

value = maskInputValue({
element: target,
maskInputOptions: this.maskInputOptions,
tagName: target.tagName,
type,
value,
maskInputFn: this.maskInputFn,
forceMask,
});
}
if (
Expand Down
19 changes: 18 additions & 1 deletion packages/rrweb/src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Mirror,
getInputType,
toLowerCase,
needMaskingText,
} from 'rrweb-snapshot';
import type { FontFaceSet } from 'css-font-loading-module';
import {
Expand Down Expand Up @@ -428,6 +429,11 @@ function initInputObserver({
maskInputFn,
sampling,
userTriggeredOnInput,
maskAllText,
maskTextClass,
unmaskTextClass,
maskTextSelector,
unmaskTextSelector,
}: observerParam): listenerHandler {
function eventHandler(event: Event) {
let target = getEventTarget(event) as HTMLElement | null;
Expand Down Expand Up @@ -460,11 +466,21 @@ function initInputObserver({
let isChecked = false;
const type: Lowercase<string> = getInputType(target) || '';

const forceMask = needMaskingText(
target as Node,
maskTextClass,
maskTextSelector,
unmaskTextClass,
unmaskTextSelector,
maskAllText,
);

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] ||
forceMask
) {
text = maskInputValue({
element: target,
Expand All @@ -473,6 +489,7 @@ function initInputObserver({
type,
value: text,
maskInputFn,
forceMask,
});
}
cbWithDedup(
Expand Down
23 changes: 23 additions & 0 deletions packages/rrweb/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,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');
Expand Down
1 change: 1 addition & 0 deletions packages/rrweb/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@ export function generateRecordSnippet(options: recordOptions<eventWithTime>) {
maskAllInputs: ${options.maskAllInputs},
maskAllText: ${options.maskAllText},
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
maskInputFn: ${options.maskInputFn},
userTriggeredOnInput: ${options.userTriggeredOnInput},
maskTextFn: ${options.maskTextFn},
maskInputFn: ${options.maskInputFn},
Expand Down

0 comments on commit 37c9638

Please sign in to comment.