Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow for masking of attributes #1257

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -148,6 +148,7 @@ The parameter of `rrweb.record` accepts the following options.
| maskTextSelector | null | Use a string to configure which selector should be masked, 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
24 changes: 24 additions & 0 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
KeepIframeSrcFn,
ICanvas,
serializedElementNodeWithId,
MaskAttributeFn,
} from './types';
import {
Mirror,
Expand Down Expand Up @@ -229,6 +230,8 @@
tagName: Lowercase<string>,
name: Lowercase<string>,
value: string | null,
element: HTMLElement,
maskAttributeFn: MaskAttributeFn | undefined,
): string | null {
if (!value) {
return value;
Expand Down Expand Up @@ -257,13 +260,18 @@
return absoluteToDoc(doc, value);
}

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

return value;
}

export function ignoreAttribute(
tagName: string,
name: string,
_value: unknown,

Check warning on line 274 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L274

[@typescript-eslint/no-unused-vars] '_value' is defined but never used.
): boolean {
return (tagName === 'video' || tagName === 'audio') && name === 'autoplay';
}
Expand Down Expand Up @@ -396,7 +404,7 @@
iframeEl.addEventListener('load', listener);
}

function isStylesheetLoaded(link: HTMLLinkElement) {

Check warning on line 407 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L407

[@typescript-eslint/no-unused-vars] 'isStylesheetLoaded' is defined but never used.
if (!link.getAttribute('href')) return true; // nothing to load
return link.sheet !== null;
}
Expand Down Expand Up @@ -437,6 +445,7 @@
mirror: Mirror;
blockClass: string | RegExp;
blockSelector: string | null;
maskAttributeFn: MaskAttributeFn | undefined;
maskTextClass: string | RegExp;
maskTextSelector: string | null;
inlineStylesheet: boolean;
Expand All @@ -458,6 +467,7 @@
mirror,
blockClass,
blockSelector,
maskAttributeFn,
maskTextClass,
maskTextSelector,
inlineStylesheet,
Expand Down Expand Up @@ -500,6 +510,7 @@
blockClass,
blockSelector,
inlineStylesheet,
maskAttributeFn,
maskInputOptions,
maskInputFn,
dataURLOptions,
Expand Down Expand Up @@ -565,7 +576,7 @@
// So we'll be conservative and keep textContent as-is.
} else if ((n.parentNode as HTMLStyleElement).sheet?.cssRules) {
textContent = stringifyStyleSheet(
(n.parentNode as HTMLStyleElement).sheet!,

Check warning on line 579 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L579

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
);
}
} catch (err) {
Expand Down Expand Up @@ -605,6 +616,7 @@
blockClass: string | RegExp;
blockSelector: string | null;
inlineStylesheet: boolean;
maskAttributeFn: MaskAttributeFn | undefined;
maskInputOptions: MaskInputOptions;
maskInputFn: MaskInputFn | undefined;
dataURLOptions?: DataURLOptions;
Expand All @@ -624,6 +636,7 @@
blockSelector,
inlineStylesheet,
maskInputOptions = {},
maskAttributeFn,
maskInputFn,
dataURLOptions = {},
inlineImages,
Expand All @@ -644,6 +657,8 @@
tagName,
toLowerCase(attr.name),
attr.value,
n,
maskAttributeFn,
);
}
}
Expand All @@ -659,7 +674,7 @@
if (cssText) {
delete attributes.rel;
delete attributes.href;
attributes._cssText = absoluteToStylesheet(cssText, stylesheet!.href!);

Check warning on line 677 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L677

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.

Check warning on line 677 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L677

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}
}
// dynamic stylesheet
Expand Down Expand Up @@ -753,10 +768,10 @@
const recordInlineImage = () => {
image.removeEventListener('load', recordInlineImage);
try {
canvasService!.width = image.naturalWidth;

Check warning on line 771 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L771

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
canvasService!.height = image.naturalHeight;

Check warning on line 772 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L772

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
canvasCtx!.drawImage(image, 0, 0);

Check warning on line 773 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L773

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
attributes.rr_dataURL = canvasService!.toDataURL(

Check warning on line 774 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L774

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
dataURLOptions.type,
dataURLOptions.quality,
);
Expand Down Expand Up @@ -938,6 +953,7 @@
inlineStylesheet: boolean;
newlyAddedElement?: boolean;
maskInputOptions?: MaskInputOptions;
maskAttributeFn: MaskAttributeFn | undefined;
maskTextFn: MaskTextFn | undefined;
maskInputFn: MaskInputFn | undefined;
slimDOMOptions: SlimDOMOptions;
Expand Down Expand Up @@ -969,6 +985,7 @@
skipChild = false,
inlineStylesheet = true,
maskInputOptions = {},
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
Expand All @@ -993,6 +1010,7 @@
maskTextSelector,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
dataURLOptions,
Expand Down Expand Up @@ -1066,6 +1084,7 @@
skipChild,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
Expand Down Expand Up @@ -1126,6 +1145,7 @@
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
Expand Down Expand Up @@ -1173,6 +1193,7 @@
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOMOptions,
Expand Down Expand Up @@ -1213,6 +1234,7 @@
maskTextSelector?: string | null;
inlineStylesheet?: boolean;
maskAllInputs?: boolean | MaskInputOptions;
maskAttributeFn?: MaskAttributeFn;
maskTextFn?: MaskTextFn;
maskInputFn?: MaskTextFn;
slimDOM?: 'all' | boolean | SlimDOMOptions;
Expand Down Expand Up @@ -1244,6 +1266,7 @@
inlineImages = false,
recordCanvas = false,
maskAllInputs = false,
maskAttributeFn,
maskTextFn,
maskInputFn,
slimDOM = false,
Expand Down Expand Up @@ -1309,6 +1332,7 @@
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 @@ -155,6 +155,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
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,12 @@ exports[`integration tests [html file]: picture-in-frame.html 1`] = `
</body></html>"
`;
exports[`integration tests [html file]: picture-with-inline-onload.html 1`] = `
"<html xmlns=\\"http://www.w3.org/1999/xhtml\\"><head></head><body>
<img src=\\"http://localhost:3030/images/robot.png\\" alt=\\"This is a robot\\" style=\\"opacity: 1;\\" _onload=\\"this.style.opacity=1\\" />
</body></html>"
`;

exports[`integration tests [html file]: preload.html 1`] = `
"<!DOCTYPE html><html lang=\\"en\\"><head>
<meta charset=\\"UTF-8\\" />
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,
} from '../src/snapshot';
import { serializedNodeWithId } from '../src/types';
Expand Down Expand Up @@ -110,6 +111,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 @@ -147,6 +192,7 @@ describe('style elements', () => {
maskTextSelector: null,
skipChild: false,
inlineStylesheet: true,
maskAttributeFn: undefined,
maskTextFn: undefined,
maskInputFn: undefined,
slimDOMOptions: {},
Expand Down Expand Up @@ -192,6 +238,7 @@ describe('scrollTop/scrollLeft', () => {
maskTextSelector: 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 @@ -70,6 +70,7 @@
maskAllInputs,
maskInputOptions: _maskInputOptions,
slimDOMOptions: _slimDOMOptions,
maskAttributeFn,
maskInputFn,
maskTextFn,
hooks,
Expand Down Expand Up @@ -328,6 +329,7 @@
inlineStylesheet,
maskInputOptions,
dataURLOptions,
maskAttributeFn,
maskTextFn,
maskInputFn,
recordCanvas,
Expand Down Expand Up @@ -370,6 +372,7 @@
maskTextSelector,
inlineStylesheet,
maskAllInputs: maskInputOptions,
maskAttributeFn,
maskTextFn,
slimDOM: slimDOMOptions,
dataURLOptions,
Expand Down Expand Up @@ -532,6 +535,7 @@
userTriggeredOnInput,
collectFonts,
doc,
maskAttributeFn,
maskInputFn,
maskTextFn,
keepIframeSrcFn,
Expand All @@ -549,7 +553,7 @@
plugins
?.filter((p) => p.observer)
?.map((p) => ({
observer: p.observer!,

Check warning on line 556 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/index.ts#L556

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
options: p.options,
callback: (payload: object) =>
wrappedEmit(
Expand All @@ -569,7 +573,7 @@

iframeManager.addLoadListener((iframeEl) => {
try {
handlers.push(observe(iframeEl.contentDocument!));

Check warning on line 576 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/index.ts#L576

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
} catch (error) {
// TODO: handle internal error
console.warn(error);
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 @@ -175,6 +175,7 @@
private maskTextSelector: observerParam['maskTextSelector'];
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 All @@ -200,6 +201,7 @@
'maskTextSelector',
'inlineStylesheet',
'maskInputOptions',
'maskAttributeFn',
'maskTextFn',
'maskInputFn',
'keepIframeSrcFn',
Expand Down Expand Up @@ -303,6 +305,7 @@
newlyAddedElement: true,
inlineStylesheet: this.inlineStylesheet,
maskInputOptions: this.maskInputOptions,
maskAttributeFn: this.maskAttributeFn,
maskTextFn: this.maskTextFn,
maskInputFn: this.maskInputFn,
slimDOMOptions: this.slimDOMOptions,
Expand Down Expand Up @@ -340,13 +343,13 @@
};

while (this.mapRemoves.length) {
this.mirror.removeNodeFromMap(this.mapRemoves.shift()!);

Check warning on line 346 in packages/rrweb/src/record/mutation.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/mutation.ts#L346

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}

for (const n of this.movedSet) {
if (
isParentRemoved(this.removes, n, this.mirror) &&
!this.movedSet.has(n.parentNode!)

Check warning on line 352 in packages/rrweb/src/record/mutation.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/mutation.ts#L352

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
) {
continue;
}
Expand Down Expand Up @@ -601,6 +604,8 @@
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 @@ -50,6 +51,7 @@ export type recordOptions<T> = {
maskTextSelector?: string;
maskAllInputs?: boolean;
maskInputOptions?: MaskInputOptions;
maskAttributeFn?: MaskAttributeFn;
maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
slimDOMOptions?: SlimDOMOptions | 'all' | true;
Expand Down Expand Up @@ -87,6 +89,7 @@ export type observerParam = {
maskTextClass: maskTextClass;
maskTextSelector: string | null;
maskInputOptions: MaskInputOptions;
maskAttributeFn?: MaskAttributeFn;
maskInputFn?: MaskInputFn;
maskTextFn?: MaskTextFn;
keepIframeSrcFn: KeepIframeSrcFn;
Expand Down Expand Up @@ -130,6 +133,7 @@ export type MutationBufferParam = Pick<
| 'maskTextSelector'
| 'inlineStylesheet'
| 'maskInputOptions'
| 'maskAttributeFn'
| 'maskTextFn'
| 'maskInputFn'
| 'keepIframeSrcFn'
Expand Down
Loading