-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(replay): Deprecate privacy options in favor of a new API, remove some recording options #6645
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -83,17 +83,20 @@ Sentry.setUser({ email: "[email protected]" }); | |
|
||
### Stopping & re-starting replays | ||
|
||
You can manually stop/re-start Replay capture via `.stop()` & `.start()`: | ||
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()`. | ||
|
||
```js | ||
const replay = new Replay(); | ||
Sentry.init({ | ||
integrations: [replay] | ||
}); | ||
|
||
// sometime later | ||
replay.stop(); | ||
replay.start(); | ||
const client = getClient(); | ||
|
||
// Add replay integration, will start recoring | ||
client.addIntegration(replay); | ||
|
||
replay.stop(); // Stop recording | ||
``` | ||
|
||
## Loading Replay as a CDN Bundle | ||
|
@@ -185,19 +188,29 @@ The following options can be configured as options to the integration, in `new R | |
|
||
The following options can be configured as options to the integration, in `new Replay({})`: | ||
|
||
| key | type | default | description | | ||
| ---------------- | ------------------------ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| maskAllText | boolean | `true` | Mask _all_ text content. Will pass text content through `maskTextFn` before sending to server. | | ||
| blockAllMedia | boolean | `true` | Block _all_ media elements (`img, svg, video, object, picture, embed, map, audio`) | ||
| 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 `*`. | | ||
| maskAllInputs | boolean | `true` | Mask values of `<input>` elements. Passes input values through `maskInputFn` before sending to server. | | ||
| 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`. | | ||
| 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 `*`. | | ||
| blockClass | string \| RegExp | `'sentry-block'` | Redact all elements that match the class name. See [privacy](#blocking) section for an example. | | ||
| blockSelector | string | `'[data-sentry-block]'` | Redact all elements that match the DOM selector. See [privacy](#blocking) section for an example. | | ||
| ignoreClass | string \| RegExp | `'sentry-ignore'` | Ignores all events on the matching input field. See [privacy](#ignoring) section for an example. | | ||
| maskTextClass | string \| RegExp | `'sentry-mask'` | Mask all elements that match the class name. See [privacy](#masking) section for an example. | | ||
| maskTextSelector | string | `undefined` | Mask all elements that match the given DOM selector. See [privacy](#masking) section for an example. | | ||
| key | type | default | description | | ||
| ---------------- | ------------------------ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| maskAllText | boolean | `true` | Mask _all_ text content. Will pass text content through `maskTextFn` before sending to server. | | ||
| maskAllInputs | boolean | `true` | Mask values of `<input>` elements. Passes input values through `maskInputFn` before sending to server. | | ||
| blockAllMedia | boolean | `true` | Block _all_ media elements (`img, svg, video, object, picture, embed, map, audio`) | ||
| 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 `*`. | | ||
| block | Array<string> | `.sentry-block, [data-sentry-block]` | Redact any elements that match the DOM selectors. See [privacy](#blocking) section for an example. | | ||
| 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. | | ||
| mask | Array<string> | `.sentry-mask, [data-sentry-mask]` | Mask all elements that match the given DOM selectors. See [privacy](#masking) section for an example. | | ||
| 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. | | ||
| ignore | Array<string> | `.sentry-ignore, [data-sentry-ignore]` | Ignores all events on the matching input fields. See [privacy](#ignoring) section for an example. | | ||
|
||
#### Deprecated options | ||
In order to streamline our privacy options, the following have been deprecated in favor for the respective options above. | ||
|
||
| deprecated key | replaced by | description | | ||
| ---------------- | ----------- | ----------- | | ||
| maskInputOptions | mask | Use CSS selectors in `mask` in order to mask all inputs of a certain type. For example, `input[type="address"]` | | ||
| blockSelector | block | The selector(s) can be moved directly in the `block` array. | | ||
| 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. | | ||
| 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. | | ||
| maskSelector | mask | The selector(s) can be moved directly in the `mask` array. | | ||
| 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. | | ||
|
||
## Privacy | ||
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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import type { BrowserClientReplayOptions, Integration } from '@sentry/types'; | |
import { DEFAULT_FLUSH_MAX_DELAY, DEFAULT_FLUSH_MIN_DELAY, MASK_ALL_TEXT_SELECTOR } from './constants'; | ||
import { ReplayContainer } from './replay'; | ||
import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions } from './types'; | ||
import { getPrivacyOptions } from './util/getPrivacyOptions'; | ||
import { isBrowser } from './util/isBrowser'; | ||
|
||
const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed,map,audio'; | ||
|
@@ -38,27 +39,57 @@ export class Replay implements Integration { | |
flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY, | ||
stickySession = true, | ||
useCompression = true, | ||
_experiments = {}, | ||
sessionSampleRate, | ||
errorSampleRate, | ||
maskAllText, | ||
maskTextSelector, | ||
maskAllInputs = true, | ||
blockAllMedia = true, | ||
_experiments = {}, | ||
blockClass = 'sentry-block', | ||
ignoreClass = 'sentry-ignore', | ||
maskTextClass = 'sentry-mask', | ||
blockSelector = '[data-sentry-block]', | ||
..._recordingOptions | ||
|
||
mask = [], | ||
unmask = [], | ||
block = [], | ||
unblock = [], | ||
ignore = [], | ||
maskFn, | ||
|
||
// eslint-disable-next-line deprecation/deprecation | ||
blockClass, | ||
// eslint-disable-next-line deprecation/deprecation | ||
blockSelector, | ||
// eslint-disable-next-line deprecation/deprecation | ||
maskTextClass, | ||
// eslint-disable-next-line deprecation/deprecation | ||
maskTextSelector, | ||
// eslint-disable-next-line deprecation/deprecation | ||
ignoreClass, | ||
}: ReplayConfiguration = {}) { | ||
this._recordingOptions = { | ||
maskAllInputs, | ||
blockClass, | ||
ignoreClass, | ||
maskTextClass, | ||
maskTextSelector, | ||
blockSelector, | ||
..._recordingOptions, | ||
maskTextFn: maskFn, | ||
maskInputFn: maskFn, | ||
|
||
...getPrivacyOptions({ | ||
mask, | ||
unmask, | ||
block, | ||
unblock, | ||
ignore, | ||
blockClass, | ||
blockSelector, | ||
maskTextClass, | ||
maskTextSelector, | ||
ignoreClass, | ||
}), | ||
|
||
// Our defaults | ||
slimDOMOptions: 'all', | ||
inlineStylesheet: true, | ||
// Disable inline images as it will increase segment/replay size | ||
inlineImages: false, | ||
// collect fonts, but be aware that `sentry.io` needs to be an allowed | ||
// origin for playback | ||
collectFonts: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may get some CORS errors on playback, but it should be fine as it will fallback to system fonts. |
||
}; | ||
|
||
this._options = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -113,10 +113,70 @@ export interface ReplayPluginOptions extends SessionOptions { | |
}>; | ||
} | ||
|
||
export interface ReplayIntegrationPrivacyOptions { | ||
/** | ||
* Mask text content for elements that match the CSS selectors in the list. | ||
*/ | ||
mask?: string[]; | ||
|
||
/** | ||
* Unmask text content for elements that match the CSS selectors in the list. | ||
*/ | ||
unmask?: string[]; | ||
|
||
/** | ||
* Block elements that match the CSS selectors in the list. Blocking replaces | ||
* the element with an empty placeholder with the same dimensions. | ||
*/ | ||
block?: string[]; | ||
|
||
/** | ||
* Unblock elements that match the CSS selectors in the list. This is useful when using `blockAllMedia`. | ||
*/ | ||
unblock?: string[]; | ||
|
||
/** | ||
* Ignore input events for elements that match the CSS selectors in the list. | ||
*/ | ||
ignore?: string[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we go with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mydea hmm my worry is that people will confuse it for DOM events e.g. clicks? |
||
|
||
/** | ||
* A callback function to customize how your text is masked. | ||
*/ | ||
maskFn?: Pick<RecordingOptions, 'maskTextFn'>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just thinking, do we need to expose this? Or is it OK to just not allow to configure this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking we should keep it since we are being so aggressive about privacy first |
||
} | ||
|
||
// These are optional for ReplayPluginOptions because the plugin sets default values | ||
type OptionalReplayPluginOptions = Partial<ReplayPluginOptions>; | ||
|
||
export interface ReplayConfiguration extends OptionalReplayPluginOptions, RecordingOptions {} | ||
export interface DeprecatedPrivacyOptions { | ||
/** | ||
* @deprecated Use `block` which accepts an array of CSS selectors | ||
*/ | ||
blockSelector?: RecordingOptions['blockSelector']; | ||
/** | ||
* @deprecated Use `block` which accepts an array of CSS selectors | ||
*/ | ||
blockClass?: RecordingOptions['blockClass']; | ||
/** | ||
* @deprecated Use `mask` which accepts an array of CSS selectors | ||
*/ | ||
maskTextClass?: RecordingOptions['maskTextClass']; | ||
/** | ||
* @deprecated Use `mask` which accepts an array of CSS selectors | ||
*/ | ||
maskTextSelector?: RecordingOptions['maskTextSelector']; | ||
/** | ||
* @deprecated Use `ignore` which accepts an array of CSS selectors | ||
*/ | ||
ignoreClass?: RecordingOptions['ignoreClass']; | ||
} | ||
|
||
export interface ReplayConfiguration | ||
extends ReplayIntegrationPrivacyOptions, | ||
OptionalReplayPluginOptions, | ||
DeprecatedPrivacyOptions, | ||
Pick<RecordingOptions, 'maskAllInputs'> {} | ||
|
||
interface CommonEventContext { | ||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import type { DeprecatedPrivacyOptions, ReplayIntegrationPrivacyOptions } from '../types'; | ||
|
||
type GetPrivacyOptions = Required<Omit<ReplayIntegrationPrivacyOptions, 'maskFn'>> & DeprecatedPrivacyOptions; | ||
interface GetPrivacyReturn { | ||
maskTextSelector: string; | ||
unmaskTextSelector: string; | ||
maskInputSelector: string; | ||
unmaskInputSelector: string; | ||
blockSelector: string; | ||
unblockSelector: string; | ||
ignoreSelector: string; | ||
|
||
blockClass?: RegExp; | ||
maskTextClass?: RegExp; | ||
} | ||
|
||
function getOption( | ||
selectors: string[], | ||
defaultSelectors: string[], | ||
deprecatedClassOption?: string | RegExp, | ||
deprecatedSelectorOption?: string, | ||
): string { | ||
const deprecatedSelectors = typeof deprecatedSelectorOption === 'string' ? deprecatedSelectorOption.split(',') : []; | ||
|
||
const allSelectors = [ | ||
...selectors, | ||
// @deprecated | ||
...deprecatedSelectors, | ||
|
||
// sentry defaults | ||
...defaultSelectors, | ||
]; | ||
|
||
// @deprecated | ||
if (typeof deprecatedClassOption !== 'undefined') { | ||
// NOTE: No support for RegExp | ||
if (typeof deprecatedClassOption === 'string') { | ||
allSelectors.push(`.${deprecatedClassOption}`); | ||
} | ||
|
||
// eslint-disable-next-line no-console | ||
console.warn( | ||
'[Replay] You are using a deprecated configuration item for privacy. Read the documentation on how to use the new privacy configuration.', | ||
); | ||
} | ||
|
||
return allSelectors.join(','); | ||
} | ||
|
||
/** | ||
* Returns privacy related configuration for use in rrweb | ||
*/ | ||
export function getPrivacyOptions({ | ||
mask, | ||
unmask, | ||
block, | ||
unblock, | ||
ignore, | ||
|
||
// eslint-disable-next-line deprecation/deprecation | ||
blockClass, | ||
// eslint-disable-next-line deprecation/deprecation | ||
blockSelector, | ||
// eslint-disable-next-line deprecation/deprecation | ||
maskTextClass, | ||
// eslint-disable-next-line deprecation/deprecation | ||
maskTextSelector, | ||
// eslint-disable-next-line deprecation/deprecation | ||
ignoreClass, | ||
}: GetPrivacyOptions): GetPrivacyReturn { | ||
const maskSelector = getOption(mask, ['.sentry-mask', '[data-sentry-mask]'], maskTextClass, maskTextSelector); | ||
const unmaskSelector = getOption(unmask, ['.sentry-unmask', '[data-sentry-unmask]']); | ||
|
||
const options: GetPrivacyReturn = { | ||
// We are making the decision to make text and input selectors the same | ||
maskTextSelector: maskSelector, | ||
unmaskTextSelector: unmaskSelector, | ||
maskInputSelector: maskSelector, | ||
unmaskInputSelector: unmaskSelector, | ||
|
||
blockSelector: getOption(block, ['.sentry-block', '[data-sentry-block]'], blockClass, blockSelector), | ||
unblockSelector: getOption(unblock, ['.sentry-unblock', '[data-sentry-unblock]']), | ||
ignoreSelector: getOption(ignore, ['.sentry-ignore', '[data-sentry-ignore]'], ignoreClass), | ||
}; | ||
|
||
if (blockClass instanceof RegExp) { | ||
options.blockClass = blockClass; | ||
} | ||
|
||
if (maskTextClass instanceof RegExp) { | ||
options.maskTextClass = maskTextClass; | ||
} | ||
|
||
return options; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to be clear (we should put this in the changelog), this means passing through "random" rrweb option is not possible anymore in this release!