Skip to content

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

Merged
merged 3 commits into from
Jan 30, 2023
Merged
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
8 changes: 6 additions & 2 deletions packages/integration-tests/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { EnvelopeItemType, Event, EventEnvelopeHeaders } from '@sentry/type

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

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

Expand All @@ -14,7 +14,11 @@ export const envelopeRequestParser = (request: Request | null): Event => {
} catch (error) {
return line;
}
})[2];
});
};

export const envelopeRequestParser = (request: Request | null): Event => {
return envelopeParser(request)[2] as Event;
};

export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => {
Expand Down
47 changes: 30 additions & 17 deletions packages/replay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
57 changes: 44 additions & 13 deletions packages/replay/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Copy link
Member

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!

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,
Copy link
Member Author

Choose a reason for hiding this comment

The 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 = {
Expand Down
62 changes: 61 additions & 1 deletion packages/replay/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we go with ignoreEvents here?

Copy link
Member Author

Choose a reason for hiding this comment

The 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'>;
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 {
/**
Expand Down
1 change: 1 addition & 0 deletions packages/replay/src/types/rrweb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ export type recordOptions = {
blockClass?: blockClass;
ignoreClass?: string;
maskTextClass?: maskTextClass;
maskTextSelector?: string;
blockSelector?: string;
} & Record<string, unknown>;
95 changes: 95 additions & 0 deletions packages/replay/src/util/getPrivacyOptions.ts
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;
}
Loading