Skip to content

Commit e4e7e2e

Browse files
authored
feat(replay): Deprecate privacy options in favor of a new API, remove some recording options (#6645)
* Deprecate `maskTextSelector`, `maskTextClass`, `blockClass`, `blockSelector`, `ignoreClass` in favor of new API: `mask`, `block`, `ignore` which accepts an array of CSS selectors. * Adds new API for unmasking and unblocking elements: `unmask`, `unblock`. This can be used in conjunction with `maskAllText`, `blockAllMedia` to have privacy by default and selectively unmask elements.
1 parent 50817ac commit e4e7e2e

File tree

9 files changed

+343
-45
lines changed

9 files changed

+343
-45
lines changed

packages/integration-tests/utils/helpers.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { EnvelopeItemType, Event, EventEnvelopeHeaders } from '@sentry/type
33

44
const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
55

6-
export const envelopeRequestParser = (request: Request | null): Event => {
6+
export const envelopeParser = (request: Request | null): unknown[] => {
77
// https://develop.sentry.dev/sdk/envelopes/
88
const envelope = request?.postData() || '';
99

@@ -14,7 +14,11 @@ export const envelopeRequestParser = (request: Request | null): Event => {
1414
} catch (error) {
1515
return line;
1616
}
17-
})[2];
17+
});
18+
};
19+
20+
export const envelopeRequestParser = (request: Request | null): Event => {
21+
return envelopeParser(request)[2] as Event;
1822
};
1923

2024
export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => {

packages/replay/README.md

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,20 @@ Sentry.setUser({ email: "[email protected]" });
8383

8484
### Stopping & re-starting replays
8585

86-
You can manually stop/re-start Replay capture via `.stop()` & `.start()`:
86+
Replay recording only starts when it is included in the `integrations` array when calling `Sentry.init` or calling `addIntegration` from the a Sentry client instance. To stop recording you can call the `stop()`.
8787

8888
```js
8989
const replay = new Replay();
9090
Sentry.init({
9191
integrations: [replay]
9292
});
9393

94-
// sometime later
95-
replay.stop();
96-
replay.start();
94+
const client = getClient();
95+
96+
// Add replay integration, will start recoring
97+
client.addIntegration(replay);
98+
99+
replay.stop(); // Stop recording
97100
```
98101

99102
## Loading Replay as a CDN Bundle
@@ -185,19 +188,29 @@ The following options can be configured as options to the integration, in `new R
185188

186189
The following options can be configured as options to the integration, in `new Replay({})`:
187190

188-
| key | type | default | description |
189-
| ---------------- | ------------------------ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
190-
| maskAllText | boolean | `true` | Mask _all_ text content. Will pass text content through `maskTextFn` before sending to server. |
191-
| blockAllMedia | boolean | `true` | Block _all_ media elements (`img, svg, video, object, picture, embed, map, audio`)
192-
| maskTextFn | (text: string) => string | `(text) => '*'.repeat(text.length)` | Function to customize how text content is masked before sending to server. By default, masks text with `*`. |
193-
| maskAllInputs | boolean | `true` | Mask values of `<input>` elements. Passes input values through `maskInputFn` before sending to server. |
194-
| maskInputOptions | Record<string, boolean> | `{ password: true }` | Customize which inputs `type` to mask. <br /> Available `<input>` types: `color, date, datetime-local, email, month, number, range, search, tel, text, time, url, week, textarea, select, password`. |
195-
| maskInputFn | (text: string) => string | `(text) => '*'.repeat(text.length)` | Function to customize how form input values are masked before sending to server. By default, masks values with `*`. |
196-
| blockClass | string \| RegExp | `'sentry-block'` | Redact all elements that match the class name. See [privacy](#blocking) section for an example. |
197-
| blockSelector | string | `'[data-sentry-block]'` | Redact all elements that match the DOM selector. See [privacy](#blocking) section for an example. |
198-
| ignoreClass | string \| RegExp | `'sentry-ignore'` | Ignores all events on the matching input field. See [privacy](#ignoring) section for an example. |
199-
| maskTextClass | string \| RegExp | `'sentry-mask'` | Mask all elements that match the class name. See [privacy](#masking) section for an example. |
200-
| maskTextSelector | string | `undefined` | Mask all elements that match the given DOM selector. See [privacy](#masking) section for an example. |
191+
| key | type | default | description |
192+
| ---------------- | ------------------------ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
193+
| maskAllText | boolean | `true` | Mask _all_ text content. Will pass text content through `maskTextFn` before sending to server. |
194+
| maskAllInputs | boolean | `true` | Mask values of `<input>` elements. Passes input values through `maskInputFn` before sending to server. |
195+
| blockAllMedia | boolean | `true` | Block _all_ media elements (`img, svg, video, object, picture, embed, map, audio`)
196+
| maskTextFn | (text: string) => string | `(text) => '*'.repeat(text.length)` | Function to customize how text content is masked before sending to server. By default, masks text with `*`. |
197+
| block | Array<string> | `.sentry-block, [data-sentry-block]` | Redact any elements that match the DOM selectors. See [privacy](#blocking) section for an example. |
198+
| unblock | Array<string> | `.sentry-unblock, [data-sentry-unblock]`| Do not redact any elements that match the DOM selectors. Useful when using `blockAllMedia`. See [privacy](#blocking) section for an example. |
199+
| mask | Array<string> | `.sentry-mask, [data-sentry-mask]` | Mask all elements that match the given DOM selectors. See [privacy](#masking) section for an example. |
200+
| unmask | Array<string> | `.sentry-unmask, [data-sentry-unmask]` | Unmask all elements that match the given DOM selectors. Useful when using `maskAllText`. See [privacy](#masking) section for an example. |
201+
| ignore | Array<string> | `.sentry-ignore, [data-sentry-ignore]` | Ignores all events on the matching input fields. See [privacy](#ignoring) section for an example. |
202+
203+
#### Deprecated options
204+
In order to streamline our privacy options, the following have been deprecated in favor for the respective options above.
205+
206+
| deprecated key | replaced by | description |
207+
| ---------------- | ----------- | ----------- |
208+
| maskInputOptions | mask | Use CSS selectors in `mask` in order to mask all inputs of a certain type. For example, `input[type="address"]` |
209+
| blockSelector | block | The selector(s) can be moved directly in the `block` array. |
210+
| blockClass | block | Convert the class name to a CSS selector and add to `block` array. For example, `first-name` becomes `.first-name`. Regexes can be moved as-is. |
211+
| maskClass | mask | Convert the class name to a CSS selector and add to `mask` array. For example, `first-name` becomes `.first-name`. Regexes can be moved as-is. |
212+
| maskSelector | mask | The selector(s) can be moved directly in the `mask` array. |
213+
| ignoreClass | ignore | Convert the class name to a CSS selector and add to `ignore` array. For example, `first-name` becomes `.first-name`. Regexes can be moved as-is. |
201214

202215
## Privacy
203216
There are several ways to deal with PII. By default, the integration will mask all text content with `*` and block all media elements (`img, svg, video, object, picture, embed, map, audio`). This can be disabled by setting `maskAllText` to `false`. It is also possible to add the following CSS classes to specific DOM elements to prevent recording its contents: `sentry-block`, `sentry-ignore`, and `sentry-mask`. The following sections will show examples of how content is handled by the differing methods.

packages/replay/src/integration.ts

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { BrowserClientReplayOptions, Integration } from '@sentry/types';
44
import { DEFAULT_FLUSH_MAX_DELAY, DEFAULT_FLUSH_MIN_DELAY, MASK_ALL_TEXT_SELECTOR } from './constants';
55
import { ReplayContainer } from './replay';
66
import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions } from './types';
7+
import { getPrivacyOptions } from './util/getPrivacyOptions';
78
import { isBrowser } from './util/isBrowser';
89

910
const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed,map,audio';
@@ -38,27 +39,57 @@ export class Replay implements Integration {
3839
flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY,
3940
stickySession = true,
4041
useCompression = true,
42+
_experiments = {},
4143
sessionSampleRate,
4244
errorSampleRate,
4345
maskAllText,
44-
maskTextSelector,
4546
maskAllInputs = true,
4647
blockAllMedia = true,
47-
_experiments = {},
48-
blockClass = 'sentry-block',
49-
ignoreClass = 'sentry-ignore',
50-
maskTextClass = 'sentry-mask',
51-
blockSelector = '[data-sentry-block]',
52-
..._recordingOptions
48+
49+
mask = [],
50+
unmask = [],
51+
block = [],
52+
unblock = [],
53+
ignore = [],
54+
maskFn,
55+
56+
// eslint-disable-next-line deprecation/deprecation
57+
blockClass,
58+
// eslint-disable-next-line deprecation/deprecation
59+
blockSelector,
60+
// eslint-disable-next-line deprecation/deprecation
61+
maskTextClass,
62+
// eslint-disable-next-line deprecation/deprecation
63+
maskTextSelector,
64+
// eslint-disable-next-line deprecation/deprecation
65+
ignoreClass,
5366
}: ReplayConfiguration = {}) {
5467
this._recordingOptions = {
5568
maskAllInputs,
56-
blockClass,
57-
ignoreClass,
58-
maskTextClass,
59-
maskTextSelector,
60-
blockSelector,
61-
..._recordingOptions,
69+
maskTextFn: maskFn,
70+
maskInputFn: maskFn,
71+
72+
...getPrivacyOptions({
73+
mask,
74+
unmask,
75+
block,
76+
unblock,
77+
ignore,
78+
blockClass,
79+
blockSelector,
80+
maskTextClass,
81+
maskTextSelector,
82+
ignoreClass,
83+
}),
84+
85+
// Our defaults
86+
slimDOMOptions: 'all',
87+
inlineStylesheet: true,
88+
// Disable inline images as it will increase segment/replay size
89+
inlineImages: false,
90+
// collect fonts, but be aware that `sentry.io` needs to be an allowed
91+
// origin for playback
92+
collectFonts: true,
6293
};
6394

6495
this._options = {

packages/replay/src/types.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,70 @@ export interface ReplayPluginOptions extends SessionOptions {
113113
}>;
114114
}
115115

116+
export interface ReplayIntegrationPrivacyOptions {
117+
/**
118+
* Mask text content for elements that match the CSS selectors in the list.
119+
*/
120+
mask?: string[];
121+
122+
/**
123+
* Unmask text content for elements that match the CSS selectors in the list.
124+
*/
125+
unmask?: string[];
126+
127+
/**
128+
* Block elements that match the CSS selectors in the list. Blocking replaces
129+
* the element with an empty placeholder with the same dimensions.
130+
*/
131+
block?: string[];
132+
133+
/**
134+
* Unblock elements that match the CSS selectors in the list. This is useful when using `blockAllMedia`.
135+
*/
136+
unblock?: string[];
137+
138+
/**
139+
* Ignore input events for elements that match the CSS selectors in the list.
140+
*/
141+
ignore?: string[];
142+
143+
/**
144+
* A callback function to customize how your text is masked.
145+
*/
146+
maskFn?: Pick<RecordingOptions, 'maskTextFn'>;
147+
}
148+
116149
// These are optional for ReplayPluginOptions because the plugin sets default values
117150
type OptionalReplayPluginOptions = Partial<ReplayPluginOptions>;
118151

119-
export interface ReplayConfiguration extends OptionalReplayPluginOptions, RecordingOptions {}
152+
export interface DeprecatedPrivacyOptions {
153+
/**
154+
* @deprecated Use `block` which accepts an array of CSS selectors
155+
*/
156+
blockSelector?: RecordingOptions['blockSelector'];
157+
/**
158+
* @deprecated Use `block` which accepts an array of CSS selectors
159+
*/
160+
blockClass?: RecordingOptions['blockClass'];
161+
/**
162+
* @deprecated Use `mask` which accepts an array of CSS selectors
163+
*/
164+
maskTextClass?: RecordingOptions['maskTextClass'];
165+
/**
166+
* @deprecated Use `mask` which accepts an array of CSS selectors
167+
*/
168+
maskTextSelector?: RecordingOptions['maskTextSelector'];
169+
/**
170+
* @deprecated Use `ignore` which accepts an array of CSS selectors
171+
*/
172+
ignoreClass?: RecordingOptions['ignoreClass'];
173+
}
174+
175+
export interface ReplayConfiguration
176+
extends ReplayIntegrationPrivacyOptions,
177+
OptionalReplayPluginOptions,
178+
DeprecatedPrivacyOptions,
179+
Pick<RecordingOptions, 'maskAllInputs'> {}
120180

121181
interface CommonEventContext {
122182
/**

packages/replay/src/types/rrweb.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ export type recordOptions = {
3434
blockClass?: blockClass;
3535
ignoreClass?: string;
3636
maskTextClass?: maskTextClass;
37+
maskTextSelector?: string;
3738
blockSelector?: string;
3839
} & Record<string, unknown>;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { DeprecatedPrivacyOptions, ReplayIntegrationPrivacyOptions } from '../types';
2+
3+
type GetPrivacyOptions = Required<Omit<ReplayIntegrationPrivacyOptions, 'maskFn'>> & DeprecatedPrivacyOptions;
4+
interface GetPrivacyReturn {
5+
maskTextSelector: string;
6+
unmaskTextSelector: string;
7+
maskInputSelector: string;
8+
unmaskInputSelector: string;
9+
blockSelector: string;
10+
unblockSelector: string;
11+
ignoreSelector: string;
12+
13+
blockClass?: RegExp;
14+
maskTextClass?: RegExp;
15+
}
16+
17+
function getOption(
18+
selectors: string[],
19+
defaultSelectors: string[],
20+
deprecatedClassOption?: string | RegExp,
21+
deprecatedSelectorOption?: string,
22+
): string {
23+
const deprecatedSelectors = typeof deprecatedSelectorOption === 'string' ? deprecatedSelectorOption.split(',') : [];
24+
25+
const allSelectors = [
26+
...selectors,
27+
// @deprecated
28+
...deprecatedSelectors,
29+
30+
// sentry defaults
31+
...defaultSelectors,
32+
];
33+
34+
// @deprecated
35+
if (typeof deprecatedClassOption !== 'undefined') {
36+
// NOTE: No support for RegExp
37+
if (typeof deprecatedClassOption === 'string') {
38+
allSelectors.push(`.${deprecatedClassOption}`);
39+
}
40+
41+
// eslint-disable-next-line no-console
42+
console.warn(
43+
'[Replay] You are using a deprecated configuration item for privacy. Read the documentation on how to use the new privacy configuration.',
44+
);
45+
}
46+
47+
return allSelectors.join(',');
48+
}
49+
50+
/**
51+
* Returns privacy related configuration for use in rrweb
52+
*/
53+
export function getPrivacyOptions({
54+
mask,
55+
unmask,
56+
block,
57+
unblock,
58+
ignore,
59+
60+
// eslint-disable-next-line deprecation/deprecation
61+
blockClass,
62+
// eslint-disable-next-line deprecation/deprecation
63+
blockSelector,
64+
// eslint-disable-next-line deprecation/deprecation
65+
maskTextClass,
66+
// eslint-disable-next-line deprecation/deprecation
67+
maskTextSelector,
68+
// eslint-disable-next-line deprecation/deprecation
69+
ignoreClass,
70+
}: GetPrivacyOptions): GetPrivacyReturn {
71+
const maskSelector = getOption(mask, ['.sentry-mask', '[data-sentry-mask]'], maskTextClass, maskTextSelector);
72+
const unmaskSelector = getOption(unmask, ['.sentry-unmask', '[data-sentry-unmask]']);
73+
74+
const options: GetPrivacyReturn = {
75+
// We are making the decision to make text and input selectors the same
76+
maskTextSelector: maskSelector,
77+
unmaskTextSelector: unmaskSelector,
78+
maskInputSelector: maskSelector,
79+
unmaskInputSelector: unmaskSelector,
80+
81+
blockSelector: getOption(block, ['.sentry-block', '[data-sentry-block]'], blockClass, blockSelector),
82+
unblockSelector: getOption(unblock, ['.sentry-unblock', '[data-sentry-unblock]']),
83+
ignoreSelector: getOption(ignore, ['.sentry-ignore', '[data-sentry-ignore]'], ignoreClass),
84+
};
85+
86+
if (blockClass instanceof RegExp) {
87+
options.blockClass = blockClass;
88+
}
89+
90+
if (maskTextClass instanceof RegExp) {
91+
options.maskTextClass = maskTextClass;
92+
}
93+
94+
return options;
95+
}

0 commit comments

Comments
 (0)