Skip to content

Commit

Permalink
feat: Allow for masking of attributes via maskAttributesFn
Browse files Browse the repository at this point in the history
This currently adds a new API, `maskAttributesFn: (key: string, value: string, element: HTMLElement) => string`, that is used as a callback in `transformAttribute`. I prefer this API as it gives more flexibility for users (though it may need to pass the el node for most flexibility), but it is inconsistent with `maskTextFn` and `maskInputFn`.

other options:

* Rename this to something else (open to ideas)
* Change this to pass value, and dom element (similar to MaskInputFn) to customize masking instead of decision maker of when to mask and introduce a simpler declarative API for what attributes to mask
* ???
  • Loading branch information
billyvg committed Jul 18, 2023
1 parent 759ab0c commit 9d17a71
Show file tree
Hide file tree
Showing 13 changed files with 897 additions and 62 deletions.
6 changes: 6 additions & 0 deletions .changeset/twenty-tables-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'rrweb-snapshot': patch
'rrweb': patch
---

Add `maskAttributesFn` to be called when transforming an attribute. This is typically used to determine if an attribute should be masked or not.
1 change: 1 addition & 0 deletions guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ The parameter of `rrweb.record` accepts the following options.
| 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 \*<br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
| maskAttributeFn | - | callback before transforming attribute. can be used to mask specific attributes |
| maskInputFn | - | customize mask input content recording logic |
| maskTextFn | - | customize mask text content recording logic |
| slimDOMOptions | {} | remove unnecessary parts of the DOM <br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |
Expand Down
38 changes: 24 additions & 14 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
KeepIframeSrcFn,
ICanvas,
serializedElementNodeWithId,
MaskAttributeFn,
} from './types';
import {
Mirror,
Expand Down Expand Up @@ -231,6 +232,8 @@ export function transformAttribute(
tagName: Lowercase<string>,
name: Lowercase<string>,
value: string | null,
element: HTMLElement,
maskAttributeFn: MaskAttributeFn | undefined,
): string | null {
if (!value) {
return value;
Expand Down Expand Up @@ -259,6 +262,11 @@ export function transformAttribute(
return absoluteToDoc(doc, value);
}

// Custom attribute masking
if (typeof maskAttributeFn === 'function') {
return maskAttributeFn(name, value, element);
}

return value;
}

Expand Down Expand Up @@ -517,6 +525,7 @@ function serializeNode(
blockClass: string | RegExp;
blockSelector: string | null;
maskAllText: boolean;
maskAttributeFn: MaskAttributeFn | undefined;
maskTextClass: string | RegExp;
unmaskTextClass: string | RegExp | null;
maskTextSelector: string | null;
Expand All @@ -541,6 +550,7 @@ function serializeNode(
blockClass,
blockSelector,
maskAllText,
maskAttributeFn,
maskTextClass,
unmaskTextClass,
maskTextSelector,
Expand Down Expand Up @@ -585,6 +595,7 @@ function serializeNode(
blockClass,
blockSelector,
inlineStylesheet,
maskAttributeFn,
maskInputOptions,
maskInputFn,
dataURLOptions,
Expand Down Expand Up @@ -649,7 +660,6 @@ function serializeTextNode(
},
): serializedNode {
const {
<<<<<<< HEAD
maskAllText,
maskTextClass,
unmaskTextClass,
Expand All @@ -658,13 +668,6 @@ function serializeTextNode(
maskTextFn,
maskInputOptions,
maskInputFn,
=======
maskTextClass,
maskTextSelector,
maskTextFn,
maskInputFn,
maskInputOptions,
>>>>>>> b310e6f (feat: Better masking of option/radio/checkbox values)
rootId,
} = options;
// The parent node may not be a html element which has a tagName attribute.
Expand Down Expand Up @@ -748,6 +751,7 @@ function serializeElementNode(
blockClass: string | RegExp;
blockSelector: string | null;
inlineStylesheet: boolean;
maskAttributeFn: MaskAttributeFn | undefined;
maskInputOptions: MaskInputOptions;
maskInputFn: MaskInputFn | undefined;
dataURLOptions?: DataURLOptions;
Expand All @@ -772,6 +776,7 @@ function serializeElementNode(
blockSelector,
inlineStylesheet,
maskInputOptions = {},
maskAttributeFn,
maskInputFn,
dataURLOptions = {},
inlineImages,
Expand All @@ -797,6 +802,8 @@ function serializeElementNode(
tagName,
toLowerCase(attr.name),
attr.value,
n,
maskAttributeFn,
);
}
}
Expand Down Expand Up @@ -845,10 +852,7 @@ function serializeElementNode(
const type = getInputType(el);
const value = getInputValue(el, toUpperCase(tagName), type);
const checked = (n as HTMLInputElement).checked;
<<<<<<< HEAD
if (
attributes.type !== 'radio' &&
attributes.type !== 'checkbox' &&
attributes.type !== 'submit' &&
attributes.type !== 'button' &&
value
Expand All @@ -863,9 +867,6 @@ function serializeElementNode(
maskAllText,
);

=======
if (type !== 'submit' && type !== 'button' && value) {
>>>>>>> b310e6f (feat: Better masking of option/radio/checkbox values)
attributes.value = maskInputValue({
element: el,
type,
Expand Down Expand Up @@ -1129,6 +1130,7 @@ export function serializeNodeWithId(
newlyAddedElement?: boolean;
maskInputOptions?: MaskInputOptions;
maskAllText: boolean;
maskAttributeFn: MaskAttributeFn | undefined;
maskTextFn: MaskTextFn | undefined;
maskInputFn: MaskInputFn | undefined;
slimDOMOptions: SlimDOMOptions;
Expand Down Expand Up @@ -1163,6 +1165,7 @@ export function serializeNodeWithId(
skipChild = false,
inlineStylesheet = true,
maskInputOptions = {},
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
Expand Down Expand Up @@ -1190,6 +1193,7 @@ export function serializeNodeWithId(
unmaskTextSelector,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
dataURLOptions,
Expand Down Expand Up @@ -1266,6 +1270,7 @@ export function serializeNodeWithId(
skipChild,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
Expand Down Expand Up @@ -1329,6 +1334,7 @@ export function serializeNodeWithId(
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
Expand Down Expand Up @@ -1379,6 +1385,7 @@ export function serializeNodeWithId(
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
Expand Down Expand Up @@ -1422,6 +1429,7 @@ function snapshot(
unmaskTextSelector?: string | null;
inlineStylesheet?: boolean;
maskAllInputs?: boolean | MaskInputOptions;
maskAttributeFn?: MaskAttributeFn;
maskTextFn?: MaskTextFn;
maskInputFn?: MaskInputFn;
slimDOM?: 'all' | boolean | SlimDOMOptions;
Expand Down Expand Up @@ -1456,6 +1464,7 @@ function snapshot(
inlineImages = false,
recordCanvas = false,
maskAllInputs = false,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOM = false,
Expand Down Expand Up @@ -1524,6 +1533,7 @@ function snapshot(
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
Expand Down
5 changes: 5 additions & 0 deletions packages/rrweb-snapshot/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ export type DataURLOptions = Partial<{

export type MaskTextFn = (text: string) => string;
export type MaskInputFn = (text: string, element: HTMLElement) => string;
export type MaskAttributeFn = (
attributeName: string,
attributeValue: string,
element: HTMLElement,
) => string;

export type KeepIframeSrcFn = (src: string) => boolean;

Expand Down
47 changes: 47 additions & 0 deletions packages/rrweb-snapshot/test/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { JSDOM } from 'jsdom';
import {
absoluteToStylesheet,
serializeNodeWithId,
transformAttribute,
_isBlockedElement,
needMaskingText,
} from '../src/snapshot';
Expand Down Expand Up @@ -111,6 +112,50 @@ describe('absolute url to stylesheet', () => {
});
});

describe('transformAttribute()', () => {
it('handles empty attribute value', () => {
expect(
transformAttribute(
document,
'a',
'data-loading',
null,
document.createElement('span'),
undefined,
),
).toBe(null);
expect(
transformAttribute(
document,
'a',
'data-loading',
'',
document.createElement('span'),
undefined,
),
).toBe('');
});

it('handles custom masking function', () => {
const maskAttributeFn = jest
.fn()
.mockImplementation((_key, value): string => {
return value.split('').reverse().join('');
}) as any;
expect(
transformAttribute(
document,
'a',
'data-loading',
'foo',
document.createElement('span'),
maskAttributeFn,
),
).toBe('oof');
expect(maskAttributeFn).toHaveBeenCalledTimes(1);
});
});

describe('isBlockedElement()', () => {
const subject = (html: string, opt: any = {}) =>
_isBlockedElement(render(html), 'rr-block', opt.blockSelector);
Expand Down Expand Up @@ -151,6 +196,7 @@ describe('style elements', () => {
unmaskTextSelector: null,
skipChild: false,
inlineStylesheet: true,
maskAttributeFn: undefined,
maskTextFn: undefined,
maskInputFn: undefined,
slimDOMOptions: {},
Expand Down Expand Up @@ -199,6 +245,7 @@ describe('scrollTop/scrollLeft', () => {
unmaskTextSelector: null,
skipChild: false,
inlineStylesheet: true,
maskAttributeFn: undefined,
maskTextFn: undefined,
maskInputFn: undefined,
slimDOMOptions: {},
Expand Down
4 changes: 4 additions & 0 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ function record<T = eventWithTime>(
maskAllInputs,
maskInputOptions: _maskInputOptions,
slimDOMOptions: _slimDOMOptions,
maskAttributeFn,
maskInputFn,
maskTextFn,
hooks,
Expand Down Expand Up @@ -336,6 +337,7 @@ function record<T = eventWithTime>(
inlineStylesheet,
maskInputOptions,
dataURLOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
recordCanvas,
Expand Down Expand Up @@ -381,6 +383,7 @@ function record<T = eventWithTime>(
unmaskTextSelector,
inlineStylesheet,
maskAllInputs: maskInputOptions,
maskAttributeFn,
maskInputFn,
maskTextFn,
slimDOM: slimDOMOptions,
Expand Down Expand Up @@ -558,6 +561,7 @@ function record<T = eventWithTime>(
userTriggeredOnInput,
collectFonts,
doc,
maskAttributeFn,
maskInputFn,
maskTextFn,
keepIframeSrcFn,
Expand Down
5 changes: 5 additions & 0 deletions packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export default class MutationBuffer {
private unmaskTextSelector: observerParam['unmaskTextSelector'];
private inlineStylesheet: observerParam['inlineStylesheet'];
private maskInputOptions: observerParam['maskInputOptions'];
private maskAttributeFn: observerParam['maskAttributeFn'];
private maskTextFn: observerParam['maskTextFn'];
private maskInputFn: observerParam['maskInputFn'];
private keepIframeSrcFn: observerParam['keepIframeSrcFn'];
Expand Down Expand Up @@ -207,6 +208,7 @@ export default class MutationBuffer {
'unmaskTextSelector',
'inlineStylesheet',
'maskInputOptions',
'maskAttributeFn',
'maskTextFn',
'maskInputFn',
'keepIframeSrcFn',
Expand Down Expand Up @@ -313,6 +315,7 @@ export default class MutationBuffer {
newlyAddedElement: true,
inlineStylesheet: this.inlineStylesheet,
maskInputOptions: this.maskInputOptions,
maskAttributeFn: this.maskAttributeFn,
maskTextFn: this.maskTextFn,
maskInputFn: this.maskInputFn,
slimDOMOptions: this.slimDOMOptions,
Expand Down Expand Up @@ -627,6 +630,8 @@ export default class MutationBuffer {
toLowerCase(target.tagName),
toLowerCase(attributeName),
value,
target,
this.maskAttributeFn,
);
}
break;
Expand Down
4 changes: 4 additions & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
MaskInputFn,
MaskTextFn,
DataURLOptions,
MaskAttributeFn,
} from 'rrweb-snapshot';
import type { PackFn, UnpackFn } from './packer/base';
import type { IframeManager } from './record/iframe-manager';
Expand Down Expand Up @@ -55,6 +56,7 @@ export type recordOptions<T> = {
unmaskTextSelector?: string;
maskAllInputs?: boolean;
maskInputOptions?: MaskInputOptions;
maskAttributeFn?: MaskAttributeFn;
maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
slimDOMOptions?: SlimDOMOptions | 'all' | true;
Expand Down Expand Up @@ -95,6 +97,7 @@ export type observerParam = {
maskTextSelector: string | null;
unmaskTextSelector: string | null;
maskInputOptions: MaskInputOptions;
maskAttributeFn?: MaskAttributeFn;
maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
keepIframeSrcFn: KeepIframeSrcFn;
Expand Down Expand Up @@ -142,6 +145,7 @@ export type MutationBufferParam = Pick<
| 'unmaskTextSelector'
| 'inlineStylesheet'
| 'maskInputOptions'
| 'maskAttributeFn'
| 'maskTextFn'
| 'maskInputFn'
| 'keepIframeSrcFn'
Expand Down
Loading

0 comments on commit 9d17a71

Please sign in to comment.