Skip to content

Commit a2e7ddd

Browse files
committed
apply text mask settings to inputs rrweb-io#1096
1 parent 138ec89 commit a2e7ddd

File tree

8 files changed

+737
-4
lines changed

8 files changed

+737
-4
lines changed

packages/rrweb-snapshot/src/snapshot.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ export function needMaskingText(
319319
? (node as HTMLElement)
320320
: node.parentElement;
321321
if (el === null) return false;
322+
if (maskTextSelector === '*') return true;
322323
if (typeof maskTextClass === 'string') {
323324
if (checkAncestors) {
324325
if (el.closest(`.${maskTextClass}`)) return true;
@@ -501,11 +502,14 @@ function serializeNode(
501502
keepIframeSrcFn,
502503
newlyAddedElement,
503504
rootId,
505+
needsMask,
504506
});
505507
case n.TEXT_NODE:
506508
return serializeTextNode(n as Text, {
507509
needsMask,
508510
maskTextFn,
511+
maskInputOptions,
512+
maskInputFn,
509513
rootId,
510514
});
511515
case n.CDATA_SECTION_NODE:
@@ -536,16 +540,20 @@ function serializeTextNode(
536540
options: {
537541
needsMask: boolean | undefined;
538542
maskTextFn: MaskTextFn | undefined;
543+
maskInputOptions: MaskInputOptions;
544+
maskInputFn: MaskInputFn | undefined;
539545
rootId: number | undefined;
540546
},
541547
): serializedNode {
542-
const { needsMask, maskTextFn, rootId } = options;
548+
const { needsMask, maskTextFn, maskInputOptions, maskInputFn, rootId } =
549+
options;
543550
// The parent node may not be a html element which has a tagName attribute.
544551
// So just let it be undefined which is ok in this use case.
545552
const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName;
546553
let textContent = n.textContent;
547554
const isStyle = parentTagName === 'STYLE' ? true : undefined;
548555
const isScript = parentTagName === 'SCRIPT' ? true : undefined;
556+
const isTextarea = parentTagName === 'TEXTAREA' ? true : undefined;
549557
if (isStyle && textContent) {
550558
try {
551559
// try to read style sheet
@@ -575,6 +583,11 @@ function serializeTextNode(
575583
? maskTextFn(textContent, n.parentElement)
576584
: textContent.replace(/[\S]/g, '*');
577585
}
586+
if (isTextarea && textContent && maskInputOptions.textarea) {
587+
textContent = maskInputFn
588+
? maskInputFn(textContent, n.parentNode as HTMLElement)
589+
: textContent.replace(/[\S]/g, '*');
590+
}
578591

579592
return {
580593
type: NodeType.Text,
@@ -602,6 +615,7 @@ function serializeElementNode(
602615
*/
603616
newlyAddedElement?: boolean;
604617
rootId: number | undefined;
618+
needsMask?: boolean;
605619
},
606620
): serializedNode | false {
607621
const {
@@ -617,6 +631,7 @@ function serializeElementNode(
617631
keepIframeSrcFn,
618632
newlyAddedElement = false,
619633
rootId,
634+
needsMask,
620635
} = options;
621636
const needBlock = _isBlockedElement(n, blockClass, blockSelector);
622637
const tagName = getValidTagName(n);
@@ -674,13 +689,15 @@ function serializeElementNode(
674689
value
675690
) {
676691
const type = getInputType(n);
692+
677693
attributes.value = maskInputValue({
678694
element: n,
679695
type,
680696
tagName,
681697
value,
682698
maskInputOptions,
683699
maskInputFn,
700+
needsMask,
684701
});
685702
} else if (checked) {
686703
attributes.checked = checked;
@@ -1227,7 +1244,7 @@ function snapshot(
12271244
inlineStylesheet?: boolean;
12281245
maskAllInputs?: boolean | MaskInputOptions;
12291246
maskTextFn?: MaskTextFn;
1230-
maskInputFn?: MaskTextFn;
1247+
maskInputFn?: MaskInputFn;
12311248
slimDOM?: 'all' | boolean | SlimDOMOptions;
12321249
dataURLOptions?: DataURLOptions;
12331250
inlineImages?: boolean;

packages/rrweb-snapshot/src/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,20 +248,23 @@ export function maskInputValue({
248248
type,
249249
value,
250250
maskInputFn,
251+
needsMask,
251252
}: {
252253
element: HTMLElement;
253254
maskInputOptions: MaskInputOptions;
254255
tagName: string;
255256
type: string | null;
256257
value: string | null;
257258
maskInputFn?: MaskInputFn;
259+
needsMask?: boolean;
258260
}): string {
259261
let text = value || '';
260262
const actualType = type && toLowerCase(type);
261263

262264
if (
263265
maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] ||
264-
(actualType && maskInputOptions[actualType as keyof MaskInputOptions])
266+
(actualType && maskInputOptions[actualType as keyof MaskInputOptions]) ||
267+
needsMask
265268
) {
266269
if (maskInputFn) {
267270
text = maskInputFn(text, element);

packages/rrweb/src/record/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ function record<T = eventWithTime>(
381381
maskTextSelector,
382382
inlineStylesheet,
383383
maskAllInputs: maskInputOptions,
384+
maskInputFn,
384385
maskTextFn,
385386
slimDOM: slimDOMOptions,
386387
dataURLOptions,

packages/rrweb/src/record/mutation.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,13 +536,21 @@ export default class MutationBuffer {
536536
if (attributeName === 'value') {
537537
const type = getInputType(target);
538538

539+
const needsMask = needMaskingText(
540+
m.target,
541+
this.maskTextClass,
542+
this.maskTextSelector,
543+
true,
544+
);
545+
539546
value = maskInputValue({
540547
element: target,
541548
maskInputOptions: this.maskInputOptions,
542549
tagName: target.tagName,
543550
type,
544551
value,
545552
maskInputFn: this.maskInputFn,
553+
needsMask,
546554
});
547555
}
548556
if (

packages/rrweb/src/record/observer.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
toLowerCase,
77
getNative,
88
nativeSetTimeout,
9+
needMaskingText,
910
} from 'rrweb-snapshot';
1011
import type { FontFaceSet } from 'css-font-loading-module';
1112
import {
@@ -404,6 +405,8 @@ function initInputObserver({
404405
maskInputFn,
405406
sampling,
406407
userTriggeredOnInput,
408+
maskTextClass,
409+
maskTextSelector,
407410
}: observerParam): listenerHandler {
408411
function eventHandler(event: Event) {
409412
let target = getEventTarget(event) as HTMLElement | null;
@@ -436,11 +439,19 @@ function initInputObserver({
436439
let isChecked = false;
437440
const type: Lowercase<string> = getInputType(target) || '';
438441

442+
const needsMask = needMaskingText(
443+
target as Node,
444+
maskTextClass,
445+
maskTextSelector,
446+
true,
447+
);
448+
439449
if (type === 'radio' || type === 'checkbox') {
440450
isChecked = (target as HTMLInputElement).checked;
441451
} else if (
442452
maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] ||
443-
maskInputOptions[type as keyof MaskInputOptions]
453+
maskInputOptions[type as keyof MaskInputOptions] ||
454+
needsMask
444455
) {
445456
text = maskInputValue({
446457
element: target,
@@ -449,6 +460,7 @@ function initInputObserver({
449460
type,
450461
value: text,
451462
maskInputFn,
463+
needsMask,
452464
});
453465
}
454466
cbWithDedup(

0 commit comments

Comments
 (0)