diff --git a/guide.md b/guide.md
index d7807bf00d..036af9aa61 100644
--- a/guide.md
+++ b/guide.md
@@ -145,8 +145,11 @@ The parameter of `rrweb.record` accepts the following options.
| ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter |
| ignoreSelector | null | Use a string to configure which selector should be ignored, refer to the [privacy](#privacy) chapter |
| ignoreCSSAttributes | null | array of CSS attributes that should be ignored |
+| maskAllText | false | mask all text content as \* |
| maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter |
+| unmaskTextClass | 'rr-unmask' | Use a string or RegExp to configure which elements should be unmasked, refer to the [privacy](#privacy) chapter |
| maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter |
+| unmaskTextSelector | null | Use a string to configure which selector should be unmasked, refer to the [privacy](#privacy) chapter |
| maskAllInputs | false | mask all input content as \* |
| maskInputOptions | { password: true } | mask some kinds of input \*
refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
| maskInputFn | - | customize mask input content recording logic |
@@ -173,6 +176,7 @@ You may find some contents on the webpage which are not willing to be recorded,
- An element with the class name `.rr-block` will not be recorded. Instead, it will replay as a placeholder with the same dimension.
- An element with the class name `.rr-ignore` will not record its input events.
- All text of elements with the class name `.rr-mask` and their children will be masked.
+- All text of elements with the class name `.rr-unmask` and their children will be unmasked, unless any child is marked with `.rr-mask`.
- `input[type="password"]` will be masked by default.
- Mask options to mask the content in input elements.
diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts
index 0963e6cfef..e3b3f87313 100644
--- a/packages/rrweb-snapshot/src/snapshot.ts
+++ b/packages/rrweb-snapshot/src/snapshot.ts
@@ -290,26 +290,89 @@ export function classMatchesRegex(
regex: RegExp,
checkAncestors: boolean,
): boolean {
- if (!node) return false;
+ return distanceToClassRegexMatch(node, regex, checkAncestors) >= 0;
+}
+
+function distanceToClassRegexMatch(
+ node: Node | null,
+ regex: RegExp,
+ checkAncestors: boolean,
+ distance = 0,
+): number {
+ if (!node) return -1;
if (node.nodeType !== node.ELEMENT_NODE) {
- if (!checkAncestors) return false;
- return classMatchesRegex(node.parentNode, regex, checkAncestors);
+ if (!checkAncestors) return -1;
+ return distanceToClassRegexMatch(node.parentNode, regex, checkAncestors);
}
for (let eIndex = (node as HTMLElement).classList.length; eIndex--; ) {
const className = (node as HTMLElement).classList[eIndex];
if (regex.test(className)) {
- return true;
+ return distance;
}
}
- if (!checkAncestors) return false;
- return classMatchesRegex(node.parentNode, regex, checkAncestors);
+ if (!checkAncestors) return -1;
+ return distanceToClassRegexMatch(
+ node.parentNode,
+ regex,
+ checkAncestors,
+ distance + 1,
+ );
+}
+
+function distanceToSelectorMatch(el: HTMLElement, selector: string): number {
+ if (!el) return -1;
+ if (el.matches(selector)) return 0;
+ const closestParent = el.closest(selector);
+ if (closestParent) {
+ let current = el;
+ let distance = 0;
+ while (current && current !== closestParent) {
+ current = current.parentNode as HTMLElement;
+ if (!current) {
+ return -1;
+ }
+ distance++;
+ }
+ return distance;
+ }
+ return -1;
+}
+
+function distanceToMatch(
+ el: HTMLElement,
+ className: string | RegExp,
+ selector: string | null,
+): number {
+ let classDistance = -1;
+ let selectorDistance = -1;
+
+ if (typeof className === 'string') {
+ classDistance = distanceToSelectorMatch(el, `.${className}`);
+ } else {
+ classDistance = distanceToClassRegexMatch(el, className, true);
+ }
+
+ if (selector) {
+ selectorDistance = distanceToSelectorMatch(el, selector);
+ }
+
+ return selectorDistance >= 0
+ ? classDistance >= 0
+ ? Math.min(classDistance, selectorDistance)
+ : selectorDistance
+ : classDistance >= 0
+ ? classDistance
+ : -1;
}
export function needMaskingText(
node: Node,
maskTextClass: string | RegExp,
maskTextSelector: string | null,
+ unmaskTextClass: string | RegExp,
+ unmaskTextSelector: string | null,
+ maskAllText: boolean,
): boolean {
try {
const el: HTMLElement | null =
@@ -318,21 +381,25 @@ export function needMaskingText(
: node.parentElement;
if (el === null) return false;
- if (typeof maskTextClass === 'string') {
- if (el.classList.contains(maskTextClass)) return true;
- if (el.closest(`.${maskTextClass}`)) return true;
- } else {
- if (classMatchesRegex(el, maskTextClass, true)) return true;
- }
+ const maskDistance = distanceToMatch(el, maskTextClass, maskTextSelector);
+ const unmaskDistance = distanceToMatch(
+ el,
+ unmaskTextClass,
+ unmaskTextSelector,
+ );
- if (maskTextSelector) {
- if (el.matches(maskTextSelector)) return true;
- if (el.closest(maskTextSelector)) return true;
- }
+ return maskDistance >= 0
+ ? unmaskDistance >= 0
+ ? maskDistance <= unmaskDistance
+ : true
+ : unmaskDistance >= 0
+ ? false
+ : !!maskAllText;
} catch (e) {
//
}
- return false;
+
+ return !!maskAllText;
}
// https://stackoverflow.com/a/36155560
@@ -426,8 +493,11 @@ function serializeNode(
mirror: Mirror;
blockClass: string | RegExp;
blockSelector: string | null;
+ maskAllText: boolean;
maskTextClass: string | RegExp;
+ unmaskTextClass: string | RegExp;
maskTextSelector: string | null;
+ unmaskTextSelector: string | null;
inlineStylesheet: boolean;
maskInputOptions: MaskInputOptions;
maskTextFn: MaskTextFn | undefined;
@@ -447,8 +517,11 @@ function serializeNode(
mirror,
blockClass,
blockSelector,
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
inlineStylesheet,
maskInputOptions = {},
maskTextFn,
@@ -500,8 +573,11 @@ function serializeNode(
});
case n.TEXT_NODE:
return serializeTextNode(n as Text, {
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
maskTextFn,
rootId,
});
@@ -531,13 +607,24 @@ function getRootId(doc: Document, mirror: Mirror): number | undefined {
function serializeTextNode(
n: Text,
options: {
+ maskAllText: boolean;
maskTextClass: string | RegExp;
+ unmaskTextClass: string | RegExp;
maskTextSelector: string | null;
+ unmaskTextSelector: string | null;
maskTextFn: MaskTextFn | undefined;
rootId: number | undefined;
},
): serializedNode {
- const { maskTextClass, maskTextSelector, maskTextFn, rootId } = options;
+ const {
+ maskAllText,
+ maskTextClass,
+ unmaskTextClass,
+ maskTextSelector,
+ unmaskTextSelector,
+ maskTextFn,
+ 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;
@@ -572,7 +659,14 @@ function serializeTextNode(
!isStyle &&
!isScript &&
textContent &&
- needMaskingText(n, maskTextClass, maskTextSelector)
+ needMaskingText(
+ n,
+ maskTextClass,
+ maskTextSelector,
+ unmaskTextClass,
+ unmaskTextSelector,
+ maskAllText,
+ )
) {
textContent = maskTextFn
? maskTextFn(textContent, n.parentElement)
@@ -922,11 +1016,14 @@ export function serializeNodeWithId(
blockClass: string | RegExp;
blockSelector: string | null;
maskTextClass: string | RegExp;
+ unmaskTextClass: string | RegExp;
maskTextSelector: string | null;
+ unmaskTextSelector: string | null;
skipChild: boolean;
inlineStylesheet: boolean;
newlyAddedElement?: boolean;
maskInputOptions?: MaskInputOptions;
+ maskAllText: boolean;
maskTextFn: MaskTextFn | undefined;
maskInputFn: MaskInputFn | undefined;
slimDOMOptions: SlimDOMOptions;
@@ -953,8 +1050,11 @@ export function serializeNodeWithId(
mirror,
blockClass,
blockSelector,
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
skipChild = false,
inlineStylesheet = true,
maskInputOptions = {},
@@ -978,8 +1078,11 @@ export function serializeNodeWithId(
mirror,
blockClass,
blockSelector,
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
inlineStylesheet,
maskInputOptions,
maskTextFn,
@@ -1050,8 +1153,11 @@ export function serializeNodeWithId(
mirror,
blockClass,
blockSelector,
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
skipChild,
inlineStylesheet,
maskInputOptions,
@@ -1110,8 +1216,11 @@ export function serializeNodeWithId(
mirror,
blockClass,
blockSelector,
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
skipChild: false,
inlineStylesheet,
maskInputOptions,
@@ -1157,8 +1266,11 @@ export function serializeNodeWithId(
mirror,
blockClass,
blockSelector,
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
skipChild: false,
inlineStylesheet,
maskInputOptions,
@@ -1198,8 +1310,11 @@ function snapshot(
mirror?: Mirror;
blockClass?: string | RegExp;
blockSelector?: string | null;
+ maskAllText?: boolean;
maskTextClass?: string | RegExp;
+ unmaskTextClass?: string | RegExp;
maskTextSelector?: string | null;
+ unmaskTextSelector?: string | null;
inlineStylesheet?: boolean;
maskAllInputs?: boolean | MaskInputOptions;
maskTextFn?: MaskTextFn;
@@ -1227,8 +1342,11 @@ function snapshot(
mirror = new Mirror(),
blockClass = 'rr-block',
blockSelector = null,
+ maskAllText = false,
maskTextClass = 'rr-mask',
+ unmaskTextClass = 'rr-unmask',
maskTextSelector = null,
+ unmaskTextSelector = null,
inlineStylesheet = true,
inlineImages = false,
recordCanvas = false,
@@ -1293,8 +1411,11 @@ function snapshot(
mirror,
blockClass,
blockSelector,
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
skipChild: false,
inlineStylesheet,
maskInputOptions,
diff --git a/packages/rrweb-snapshot/test/snapshot.test.ts b/packages/rrweb-snapshot/test/snapshot.test.ts
index 75d635e0c0..7785b5bb06 100644
--- a/packages/rrweb-snapshot/test/snapshot.test.ts
+++ b/packages/rrweb-snapshot/test/snapshot.test.ts
@@ -6,6 +6,7 @@ import {
absoluteToStylesheet,
serializeNodeWithId,
_isBlockedElement,
+ needMaskingText,
} from '../src/snapshot';
import { serializedNodeWithId } from '../src/types';
import { Mirror } from '../src/utils';
@@ -143,8 +144,11 @@ describe('style elements', () => {
mirror: new Mirror(),
blockClass: 'blockblock',
blockSelector: null,
+ maskAllText: false,
maskTextClass: 'maskmask',
+ unmaskTextClass: 'unmaskmask',
maskTextSelector: null,
+ unmaskTextSelector: null,
skipChild: false,
inlineStylesheet: true,
maskTextFn: undefined,
@@ -188,8 +192,11 @@ describe('scrollTop/scrollLeft', () => {
mirror: new Mirror(),
blockClass: 'blockblock',
blockSelector: null,
+ maskAllText: false,
maskTextClass: 'maskmask',
+ unmaskTextClass: 'unmaskmask',
maskTextSelector: null,
+ unmaskTextSelector: null,
skipChild: false,
inlineStylesheet: true,
maskTextFn: undefined,
@@ -218,3 +225,222 @@ describe('scrollTop/scrollLeft', () => {
});
});
});
+
+describe('needMaskingText', () => {
+ const render = (html: string): HTMLDivElement => {
+ document.write(html);
+ return document.querySelector('div')!;
+ };
+
+ it('should not mask by default', () => {
+ const el = render(`
Lorem ipsum
`);
+ expect(
+ needMaskingText(el, 'maskmask', null, 'unmaskmask', null, false),
+ ).toEqual(false);
+ });
+
+ it('should mask if the masking class is matched', () => {
+ const el = render(`Lorem ipsum
`);
+ expect(
+ needMaskingText(el, 'maskmask', null, 'unmaskmask', null, false),
+ ).toEqual(true);
+ expect(
+ needMaskingText(el, /^maskmask$/, null, /^unmaskmask$/, null, false),
+ ).toEqual(true);
+ });
+
+ it('should mask if the masking class is matched on an ancestor', () => {
+ const el = render(
+ `Lorem ipsum
`,
+ );
+ expect(
+ needMaskingText(
+ el.children[0],
+ 'maskmask',
+ null,
+ 'unmaskmask',
+ null,
+ false,
+ ),
+ ).toEqual(true);
+ expect(
+ needMaskingText(
+ el.children[0],
+ /^maskmask$/,
+ null,
+ /^unmaskmask$/,
+ null,
+ false,
+ ),
+ ).toEqual(true);
+ });
+
+ it('should mask if the masking selector is matched', () => {
+ const el = render(`Lorem ipsum
`);
+ expect(
+ needMaskingText(el, 'maskmask', '.foo', 'unmaskmask', null, false),
+ ).toEqual(true);
+ });
+
+ it('should mask if the masking selector is matched on an ancestor', () => {
+ const el = render(`Lorem ipsum
`);
+ expect(
+ needMaskingText(
+ el.children[0],
+ 'maskmask',
+ '.foo',
+ 'unmaskmask',
+ null,
+ false,
+ ),
+ ).toEqual(true);
+ });
+
+ it('should mask by default', () => {
+ const el = render(`Lorem ipsum
`);
+ expect(
+ needMaskingText(el, 'maskmask', null, 'unmaskmask', null, true),
+ ).toEqual(true);
+ });
+
+ it('should not mask if the un-masking class is matched', () => {
+ const el = render(`Lorem ipsum
`);
+ expect(
+ needMaskingText(el, 'maskmask', null, 'unmaskmask', null, true),
+ ).toEqual(false);
+ expect(
+ needMaskingText(el, /^maskmask$/, null, /^unmaskmask$/, null, true),
+ ).toEqual(false);
+ });
+
+ it('should not mask if the un-masking class is matched on an ancestor', () => {
+ const el = render(
+ `Lorem ipsum
`,
+ );
+ expect(
+ needMaskingText(
+ el.children[0],
+ 'maskmask',
+ null,
+ 'unmaskmask',
+ null,
+ true,
+ ),
+ ).toEqual(false);
+ expect(
+ needMaskingText(
+ el.children[0],
+ /^maskmask$/,
+ null,
+ /^unmaskmask$/,
+ null,
+ true,
+ ),
+ ).toEqual(false);
+ });
+
+ it('should mask if the masking class is more specific than the unmasking class', () => {
+ const el = render(
+ `Lorem ipsum`,
+ );
+ expect(
+ needMaskingText(
+ el.children[0].children[0],
+ 'maskmask',
+ null,
+ 'unmaskmask',
+ null,
+ false,
+ ),
+ ).toEqual(true);
+ expect(
+ needMaskingText(
+ el.children[0].children[0],
+ /^maskmask$/,
+ null,
+ /^unmaskmask$/,
+ null,
+ false,
+ ),
+ ).toEqual(true);
+ });
+
+ it('should not mask if the unmasking class is more specific than the masking class', () => {
+ const el = render(
+ `
Lorem ipsum`,
+ );
+ expect(
+ needMaskingText(
+ el.children[0].children[0],
+ 'maskmask',
+ null,
+ 'unmaskmask',
+ null,
+ false,
+ ),
+ ).toEqual(false);
+ expect(
+ needMaskingText(
+ el.children[0].children[0],
+ /^maskmask$/,
+ null,
+ /^unmaskmask$/,
+ null,
+ false,
+ ),
+ ).toEqual(false);
+ });
+
+ it('should not mask if the unmasking selector is matched', () => {
+ const el = render(`
Lorem ipsum
`);
+ expect(
+ needMaskingText(el, 'maskmask', null, 'unmaskmask', '.foo', true),
+ ).toEqual(false);
+ });
+
+ it('should not mask if the unmasking selector is matched on an ancestor', () => {
+ const el = render(`
Lorem ipsum
`);
+ expect(
+ needMaskingText(
+ el.children[0],
+ 'maskmask',
+ null,
+ 'unmaskmask',
+ '.foo',
+ true,
+ ),
+ ).toEqual(false);
+ });
+
+ it('should mask if the masking selector is more specific than the unmasking selector', () => {
+ const el = render(
+ `
Lorem ipsum`,
+ );
+ expect(
+ needMaskingText(
+ el.children[0].children[0],
+ 'maskmask',
+ '.bar',
+ 'unmaskmask',
+ '.foo',
+ false,
+ ),
+ ).toEqual(true);
+ });
+
+ it('should not mask if the unmasking selector is more specific than the masking selector', () => {
+ const el = render(
+ `
Lorem ipsum`,
+ );
+ expect(
+ needMaskingText(
+ el.children[0].children[0],
+ 'maskmask',
+ '.bar',
+ 'unmaskmask',
+ '.foo',
+ false,
+ ),
+ ).toEqual(false);
+ });
+});
diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts
index 7308ac6d04..c10438c5f3 100644
--- a/packages/rrweb/src/record/index.ts
+++ b/packages/rrweb/src/record/index.ts
@@ -65,8 +65,11 @@ function record
(
blockSelector = null,
ignoreClass = 'rr-ignore',
ignoreSelector = null,
+ maskAllText = false,
maskTextClass = 'rr-mask',
+ unmaskTextClass = 'rr-unmask',
maskTextSelector = null,
+ unmaskTextSelector = null,
inlineStylesheet = true,
maskAllInputs,
maskInputOptions: _maskInputOptions,
@@ -325,8 +328,11 @@ function record(
bypassOptions: {
blockClass,
blockSelector,
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
inlineStylesheet,
maskInputOptions,
dataURLOptions,
@@ -371,8 +377,11 @@ function record(
mirror,
blockClass,
blockSelector,
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
inlineStylesheet,
maskAllInputs: maskInputOptions,
maskTextFn,
@@ -528,8 +537,11 @@ function record(
blockClass,
ignoreClass,
ignoreSelector,
+ maskAllText,
maskTextClass,
+ unmaskTextClass,
maskTextSelector,
+ unmaskTextSelector,
maskInputOptions,
inlineStylesheet,
sampling,
diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts
index 80943d96ab..c1aa909e04 100644
--- a/packages/rrweb/src/record/mutation.ts
+++ b/packages/rrweb/src/record/mutation.ts
@@ -171,8 +171,11 @@ export default class MutationBuffer {
private mutationCb: observerParam['mutationCb'];
private blockClass: observerParam['blockClass'];
private blockSelector: observerParam['blockSelector'];
+ private maskAllText: observerParam['maskAllText'];
private maskTextClass: observerParam['maskTextClass'];
+ private unmaskTextClass: observerParam['unmaskTextClass'];
private maskTextSelector: observerParam['maskTextSelector'];
+ private unmaskTextSelector: observerParam['unmaskTextSelector'];
private inlineStylesheet: observerParam['inlineStylesheet'];
private maskInputOptions: observerParam['maskInputOptions'];
private maskTextFn: observerParam['maskTextFn'];
@@ -196,8 +199,11 @@ export default class MutationBuffer {
'mutationCb',
'blockClass',
'blockSelector',
+ 'maskAllText',
'maskTextClass',
+ 'unmaskTextClass',
'maskTextSelector',
+ 'unmaskTextSelector',
'inlineStylesheet',
'maskInputOptions',
'maskTextFn',
@@ -298,8 +304,11 @@ export default class MutationBuffer {
mirror: this.mirror,
blockClass: this.blockClass,
blockSelector: this.blockSelector,
+ maskAllText: this.maskAllText,
maskTextClass: this.maskTextClass,
+ unmaskTextClass: this.unmaskTextClass,
maskTextSelector: this.maskTextSelector,
+ unmaskTextSelector: this.unmaskTextSelector,
skipChild: true,
newlyAddedElement: true,
inlineStylesheet: this.inlineStylesheet,
@@ -520,6 +529,9 @@ export default class MutationBuffer {
m.target,
this.maskTextClass,
this.maskTextSelector,
+ this.unmaskTextClass,
+ this.unmaskTextSelector,
+ this.maskAllText,
) && value
? this.maskTextFn
? this.maskTextFn(value, closestElementOfNode(m.target))
diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts
index 1ceb44222b..cd23e3205f 100644
--- a/packages/rrweb/src/types.ts
+++ b/packages/rrweb/src/types.ts
@@ -25,6 +25,7 @@ import type {
KeepIframeSrcFn,
listenerHandler,
maskTextClass,
+ unmaskTextClass,
mediaInteractionCallback,
mouseInteractionCallBack,
mousemoveCallBack,
@@ -47,8 +48,11 @@ export type recordOptions = {
blockSelector?: string;
ignoreClass?: string;
ignoreSelector?: string;
+ maskAllText?: boolean;
maskTextClass?: maskTextClass;
+ unmaskTextClass?: unmaskTextClass;
maskTextSelector?: string;
+ unmaskTextSelector?: string;
maskAllInputs?: boolean;
maskInputOptions?: MaskInputOptions;
maskInputFn?: MaskInputFn;
@@ -87,8 +91,11 @@ export type observerParam = {
blockSelector: string | null;
ignoreClass: string;
ignoreSelector: string | null;
+ maskAllText: boolean;
maskTextClass: maskTextClass;
+ unmaskTextClass: unmaskTextClass;
maskTextSelector: string | null;
+ unmaskTextSelector: string | null;
maskInputOptions: MaskInputOptions;
maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
@@ -130,8 +137,11 @@ export type MutationBufferParam = Pick<
| 'mutationCb'
| 'blockClass'
| 'blockSelector'
+ | 'maskAllText'
| 'maskTextClass'
+ | 'unmaskTextClass'
| 'maskTextSelector'
+ | 'unmaskTextSelector'
| 'inlineStylesheet'
| 'maskInputOptions'
| 'maskTextFn'
diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
index e320254111..5b656bfa89 100644
--- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap
+++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
@@ -3574,6 +3574,349 @@ exports[`record integration tests can record style changes compactly and preserv
]"
`;
+exports[`record integration tests can selectively unmask parts of the page 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\\": \\"****** ****\\",
+ \\"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\\": \\"div\\",
+ \\"attributes\\": {
+ \\"class\\": \\"rr-unmask\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"*******\\\\n \\",
+ \\"id\\": 19
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"div\\",
+ \\"attributes\\": {
+ \\"class\\": \\"rr-mask\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 21
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"span\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"*****\\",
+ \\"id\\": 23
+ }
+ ],
+ \\"id\\": 22
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 24
+ }
+ ],
+ \\"id\\": 20
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 25
+ }
+ ],
+ \\"id\\": 18
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 26
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"div\\",
+ \\"attributes\\": {
+ \\"data-masking\\": \\"false\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 28
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"div\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 30
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"div\\",
+ \\"attributes\\": {
+ \\"data-masking\\": \\"true\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"*****\\",
+ \\"id\\": 32
+ }
+ ],
+ \\"id\\": 31
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 33
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"div\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"unmask2\\",
+ \\"id\\": 35
+ }
+ ],
+ \\"id\\": 34
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 36
+ }
+ ],
+ \\"id\\": 29
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 37
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"ul\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"id\\": 38
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 39
+ }
+ ],
+ \\"id\\": 27
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\",
+ \\"id\\": 40
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 42
+ }
+ ],
+ \\"id\\": 41
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
+ \\"id\\": 43
+ }
+ ],
+ \\"id\\": 16
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 38,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"li\\",
+ \\"attributes\\": {
+ \\"class\\": \\"rr-mask\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 44
+ }
+ },
+ {
+ \\"parentId\\": 44,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 3,
+ \\"textContent\\": \\"*** **** ****\\",
+ \\"id\\": 45
+ }
+ }
+ ]
+ }
+ }
+]"
+`;
+
exports[`record integration tests can use maskInputOptions to configure which type of inputs should be masked 1`] = `
"[
{
@@ -4057,34 +4400,864 @@ exports[`record integration tests can use maskInputOptions to configure which ty
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
- \\"text\\": \\"t\\",
- \\"isChecked\\": false,
- \\"id\\": 22
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"te\\",
- \\"isChecked\\": false,
- \\"id\\": 22
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"tes\\",
- \\"isChecked\\": false,
- \\"id\\": 22
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"test\\",
+ \\"text\\": \\"t\\",
+ \\"isChecked\\": false,
+ \\"id\\": 22
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"te\\",
+ \\"isChecked\\": false,
+ \\"id\\": 22
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"tes\\",
+ \\"isChecked\\": false,
+ \\"id\\": 22
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"test\\",
+ \\"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\\": \\"t\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"te\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"tex\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"text\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"texta\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"textar\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"textare\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"textarea\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"textarea \\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"textarea t\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"textarea te\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"textarea tes\\",
+ \\"isChecked\\": false,
+ \\"id\\": 42
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"textarea test\\",
+ \\"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\\": \\"**\\",
+ \\"isChecked\\": false,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"***\\",
+ \\"isChecked\\": false,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"****\\",
+ \\"isChecked\\": false,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"*****\\",
+ \\"isChecked\\": false,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"******\\",
+ \\"isChecked\\": false,
+ \\"id\\": 59
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 5,
+ \\"text\\": \\"*******\\",
+ \\"isChecked\\": false,
+ \\"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 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
}
@@ -4218,115 +5391,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
- \\"text\\": \\"t\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"te\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"tex\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"text\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"texta\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textar\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textare\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textarea\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textarea \\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textarea t\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textarea te\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textarea tes\\",
- \\"isChecked\\": false,
- \\"id\\": 42
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"textarea test\\",
+ \\"text\\": \\"**********\\",
\\"isChecked\\": false,
\\"id\\": 42
}
@@ -4351,70 +5416,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
- \\"text\\": \\"*\\",
- \\"isChecked\\": false,
- \\"id\\": 59
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"**\\",
- \\"isChecked\\": false,
- \\"id\\": 59
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"***\\",
- \\"isChecked\\": false,
- \\"id\\": 59
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"****\\",
- \\"isChecked\\": false,
- \\"id\\": 59
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"*****\\",
- \\"isChecked\\": false,
- \\"id\\": 59
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"******\\",
- \\"isChecked\\": false,
- \\"id\\": 59
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"*******\\",
- \\"isChecked\\": false,
- \\"id\\": 59
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 5,
- \\"text\\": \\"********\\",
+ \\"text\\": \\"**********\\",
\\"isChecked\\": false,
\\"id\\": 59
}
@@ -9233,6 +10235,15 @@ exports[`record integration tests should not record input values if dynamically
\\"id\\": 21
}
},
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 3,
+ \\"id\\": 21,
+ \\"x\\": 6,
+ \\"y\\": 0
+ }
+ },
{
\\"type\\": 3,
\\"data\\": {
diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap
index cb40f3328d..27bcc46b5e 100644
--- a/packages/rrweb/test/__snapshots__/record.test.ts.snap
+++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap
@@ -3168,6 +3168,16 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti
\\"textContent\\": \\"\\\\n \\",
\\"rootId\\": 19,
\\"id\\": 32
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"link\\",
+ \\"attributes\\": {
+ \\"_cssText\\": \\"body { color: pink; }\\"
+ },
+ \\"childNodes\\": [],
+ \\"rootId\\": 19,
+ \\"id\\": 33
}
],
\\"rootId\\": 19,
@@ -3177,7 +3187,7 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"rootId\\": 19,
- \\"id\\": 33
+ \\"id\\": 34
},
{
\\"type\\": 2,
@@ -3188,11 +3198,11 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti
\\"type\\": 3,
\\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\",
\\"rootId\\": 19,
- \\"id\\": 35
+ \\"id\\": 36
}
],
\\"rootId\\": 19,
- \\"id\\": 34
+ \\"id\\": 35
}
],
\\"rootId\\": 19,
@@ -3208,49 +3218,6 @@ exports[`record loading stylesheets captures stylesheets in iframes that are sti
\\"attributes\\": [],
\\"isAttachIframe\\": true
}
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 0,
- \\"texts\\": [],
- \\"attributes\\": [],
- \\"removes\\": [],
- \\"adds\\": [
- {
- \\"parentId\\": 22,
- \\"nextId\\": null,
- \\"node\\": {
- \\"type\\": 2,
- \\"tagName\\": \\"link\\",
- \\"attributes\\": {
- \\"rel\\": \\"stylesheet\\",
- \\"href\\": \\"http://localhost:3030/html/assets/style.css\\"
- },
- \\"childNodes\\": [],
- \\"rootId\\": 19,
- \\"id\\": 36
- }
- }
- ]
- }
- },
- {
- \\"type\\": 3,
- \\"data\\": {
- \\"source\\": 0,
- \\"adds\\": [],
- \\"removes\\": [],
- \\"texts\\": [],
- \\"attributes\\": [
- {
- \\"id\\": 36,
- \\"attributes\\": {
- \\"_cssText\\": \\"body { color: pink; }\\"
- }
- }
- ]
- }
}
]"
`;
diff --git a/packages/rrweb/test/html/unmask-text.html b/packages/rrweb/test/html/unmask-text.html
new file mode 100644
index 0000000000..a71dc04205
--- /dev/null
+++ b/packages/rrweb/test/html/unmask-text.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Unmask text
+
+
+
+
+
+
diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts
index c627be84cb..48d051dcc5 100644
--- a/packages/rrweb/test/integration.test.ts
+++ b/packages/rrweb/test/integration.test.ts
@@ -1213,6 +1213,31 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots);
});
+ it('can selectively unmask parts of the page', async () => {
+ const page: puppeteer.Page = await browser.newPage();
+ await page.goto('about:blank');
+ await page.setContent(
+ getHtml.call(this, 'unmask-text.html', {
+ maskAllText: true,
+ maskTextSelector: '[data-masking="true"]',
+ unmaskTextSelector: '[data-masking="false"]',
+ }),
+ );
+
+ await page.evaluate(() => {
+ const li = document.createElement('li');
+ const ul = document.querySelector('ul') as HTMLUListElement;
+ li.className = 'rr-mask';
+ ul.appendChild(li);
+ li.innerText = 'new list item';
+ });
+
+ const snapshots = (await page.evaluate(
+ 'window.snapshots',
+ )) as eventWithTime[];
+ assertSnapshot(snapshots);
+ });
+
it('should record after DOMContentLoaded event', 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 5a90f62031..a32932c308 100644
--- a/packages/rrweb/test/utils.ts
+++ b/packages/rrweb/test/utils.ts
@@ -690,7 +690,9 @@ export function generateRecordSnippet(options: recordOptions) {
},
ignoreSelector: ${JSON.stringify(options.ignoreSelector)},
maskTextSelector: ${JSON.stringify(options.maskTextSelector)},
+ unmaskTextSelector: ${JSON.stringify(options.unmaskTextSelector)},
maskAllInputs: ${options.maskAllInputs},
+ maskAllText: ${options.maskAllText},
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
userTriggeredOnInput: ${options.userTriggeredOnInput},
maskTextFn: ${options.maskTextFn},
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index e6f6f15cd3..eb9b9b618a 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -179,6 +179,7 @@ export type canvasEventWithTime = eventWithTime & {
export type blockClass = string | RegExp;
export type maskTextClass = string | RegExp;
+export type unmaskTextClass = string | RegExp;
export type SamplingStrategy = Partial<{
/**