Skip to content

feat(replay): Streamline replay options #6641

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

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 1 addition & 1 deletion packages/replay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ The following options can be configured as options to the integration, in `new R
| 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. |
| ignoreClass | string | `'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. |

Expand Down
34 changes: 23 additions & 11 deletions packages/replay/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Integration } from '@sentry/types';

import { DEFAULT_ERROR_SAMPLE_RATE, DEFAULT_SESSION_SAMPLE_RATE, MASK_ALL_TEXT_SELECTOR } from './constants';
import { ReplayContainer } from './replay';
import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions } from './types';
import type { RecordingOptions, ReplayConfiguration, ReplayOptions } from './types';
import { isBrowser } from './util/isBrowser';

const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed,map,audio';
Expand All @@ -27,7 +27,7 @@ export class Replay implements Integration {
*/
readonly recordingOptions: RecordingOptions;

readonly options: ReplayPluginOptions;
readonly options: ReplayOptions;

protected get _isInitialized(): boolean {
return _initialized;
Expand All @@ -48,15 +48,16 @@ export class Replay implements Integration {
sessionSampleRate,
errorSampleRate,
maskAllText,
maskTextSelector,
maskAllInputs = true,
maskTextClass = 'sentry-mask',
maskTextSelector,
blockAllMedia = true,
_experiments = {},
blockClass = 'sentry-block',
ignoreClass = 'sentry-ignore',
maskTextClass = 'sentry-mask',
blockSelector = '[data-sentry-block]',
...recordingOptions
ignoreClass = 'sentry-ignore',
_experiments = {},
recordingOptions = {},
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer if we pulled out the rrweb options that we want to allow. I still think having recordingOptions is a bit arbitrary when we have options like the above that could be considered recording options as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that's basically what we are doing here anyhow :) All the options we really support are on the top level, and recordingOptions is really more of an escape hatch to allow passing arbitrary other options (for which we do not provide any guarantees). That was also the original idea for calling this option rrweb, as that is really rrweb-specific stuff that we do not explicitly support.

Copy link
Member

@billyvg billyvg Jan 4, 2023

Choose a reason for hiding this comment

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

Yeah my suggestion is to kill the escape hatch is we are going to change the API anyway. It should either be a first class option or not supported at all. I'm gonna go through the rrweb options and make a list of what I think we should support. Thoughts on that?

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 also generally fine with that, although we may get into the situation where one customer wants to do some kind of weird thing that rrweb supports but we do not want to support. But I'm also happy with just saying "no, sorry, can't do that" in such a case!

...rest
}: ReplayConfiguration = {}) {
this.recordingOptions = {
maskAllInputs,
Expand All @@ -76,11 +77,21 @@ export class Replay implements Integration {
sessionSampleRate: DEFAULT_SESSION_SAMPLE_RATE,
errorSampleRate: DEFAULT_ERROR_SAMPLE_RATE,
useCompression,
maskAllText: typeof maskAllText === 'boolean' ? maskAllText : !maskTextSelector,
blockAllMedia,
_experiments,
};

if (Object.keys(rest).length > 0) {
// eslint-disable-next-line
console.warn(`[Replay] You are passing unknown option(s) to the Replay integration: ${Object.keys(rest).join(',')}
This is deprecated and will not be supported in future versions.
Instead, put recording-specific configuration into \`recordingOptions\`, e.g. \`recordingOptions: { inlineStylesheet: false }\`.`);

this.recordingOptions = {
...this.recordingOptions,
...rest,
};
}

if (typeof sessionSampleRate === 'number') {
// eslint-disable-next-line
console.warn(
Expand All @@ -105,14 +116,15 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
this.options.errorSampleRate = errorSampleRate;
}

if (this.options.maskAllText) {
// We want to default `maskAllText` to true, unless `maskTextSelector` has been set
if (maskAllText || (!maskTextSelector && maskAllText !== false)) {
// `maskAllText` is a more user friendly option to configure
// `maskTextSelector`. This means that all nodes will have their text
// content masked.
this.recordingOptions.maskTextSelector = MASK_ALL_TEXT_SELECTOR;
}

if (this.options.blockAllMedia) {
if (blockAllMedia) {
// `blockAllMedia` is a more user friendly option to configure blocking
// embedded media elements
this.recordingOptions.blockSelector = !this.recordingOptions.blockSelector
Expand Down
8 changes: 4 additions & 4 deletions packages/replay/src/replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import type {
RecordingEvent,
RecordingOptions,
ReplayContainer as ReplayContainerInterface,
ReplayPluginOptions,
ReplayOptions,
ReplayRecordingMode,
SendReplay,
Session,
Expand Down Expand Up @@ -79,7 +79,7 @@ export class ReplayContainer implements ReplayContainerInterface {
*/
private readonly _recordingOptions: RecordingOptions;

private readonly _options: ReplayPluginOptions;
private readonly _options: ReplayOptions;

private _performanceObserver: PerformanceObserver | null = null;

Expand Down Expand Up @@ -126,7 +126,7 @@ export class ReplayContainer implements ReplayContainerInterface {
initialUrl: '',
};

constructor({ options, recordingOptions }: { options: ReplayPluginOptions; recordingOptions: RecordingOptions }) {
constructor({ options, recordingOptions }: { options: ReplayOptions; recordingOptions: RecordingOptions }) {
this._recordingOptions = recordingOptions;
this._options = options;

Expand All @@ -151,7 +151,7 @@ export class ReplayContainer implements ReplayContainerInterface {
}

/** Get the replay integration options. */
public getOptions(): ReplayPluginOptions {
public getOptions(): ReplayOptions {
return this._options;
}

Expand Down
73 changes: 59 additions & 14 deletions packages/replay/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ export interface SessionOptions extends SampleRates {
stickySession: boolean;
}

export interface ReplayPluginOptions extends SessionOptions {
/**
* These are the options that are available on the replay container's `getOptions`
*/
export interface ReplayOptions extends SessionOptions {
/**
* The amount of time to wait before sending a replay
*/
Expand All @@ -100,33 +103,75 @@ export interface ReplayPluginOptions extends SessionOptions {
*/
useCompression: boolean;

/**
* _experiments allows users to enable experimental or internal features.
* We don't consider such features as part of the public API and hence we don't guarantee semver for them.
* Experimental features can be added, changed or removed at any time.
*
* Default: undefined
*/
_experiments?: Partial<{
captureExceptions: boolean;
traceInternals: boolean;
}>;
}

/**
* These are the options to be passed at replay initialization.
*/
interface ReplayPluginOptions extends ReplayOptions {
/**
* Mask all text in recordings. All text will be replaced with asterisks by default.
*
* (default is true, unless `maskTextClass` is set)
*/
maskAllText: boolean;

/**
* Mask the content of the given class names.
*/
maskTextClass?: string | RegExp;

/**
* Mask the content of the given selector.
*/
maskTextSelector?: string;

/**
* If all inputs should be masked.
*
* (default is true)
*/
maskAllInputs: boolean;

/**
* Block all media (e.g. images, svg, video) in recordings.
*/
blockAllMedia: boolean;

/**
* _experiments allows users to enable experimental or internal features.
* We don't consider such features as part of the public API and hence we don't guarantee semver for them.
* Experimental features can be added, changed or removed at any time.
*
* Default: undefined
* Redact the given class names.
*/
_experiments?: Partial<{
captureExceptions: boolean;
traceInternals: boolean;
}>;
blockClass?: string | RegExp;

/**
* Redact the given selector.
*/
blockSelector?: string;

/**
* Ignore all events on elements matching the class names.
*/
ignoreClass?: string;

/**
* Additional config passed through to rrweb.
*/
recordingOptions?: RecordingOptions;
}

// These are optional for ReplayPluginOptions because the plugin sets default values
type OptionalReplayPluginOptions = Partial<ReplayPluginOptions>;

export interface ReplayConfiguration extends OptionalReplayPluginOptions, RecordingOptions {}
export type ReplayConfiguration = Partial<ReplayPluginOptions>;

interface CommonEventContext {
/**
Expand Down Expand Up @@ -235,5 +280,5 @@ export interface ReplayContainer {
flushImmediate(): void;
triggerUserActivity(): void;
addUpdate(cb: AddUpdateCallback): void;
getOptions(): ReplayPluginOptions;
getOptions(): ReplayOptions;
}
7 changes: 2 additions & 5 deletions packages/replay/src/types/rrweb.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */

type blockClass = string | RegExp;
type maskTextClass = string | RegExp;

enum EventType {
DomContentLoaded = 0,
Load = 1,
Expand Down Expand Up @@ -31,8 +28,8 @@ export type eventWithTime = {
*/
export type recordOptions = {
maskAllInputs?: boolean;
blockClass?: blockClass;
blockClass?: string | RegExp;
ignoreClass?: string;
maskTextClass?: maskTextClass;
maskTextClass?: string | RegExp;
blockSelector?: string;
} & Record<string, unknown>;
45 changes: 45 additions & 0 deletions packages/replay/test/unit/index-integrationSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,49 @@ describe('integration settings', () => {
expect(replay.getOptions()._experiments).toEqual({});
});
});

describe('recordingOptions', () => {
let mockConsole: jest.SpyInstance<void>;

beforeEach(() => {
mockConsole = jest.spyOn(console, 'warn').mockImplementation(jest.fn());
});

afterEach(() => {
mockConsole.mockRestore();
});

it('shows warning when passing rrweb config directly', async () => {
const { replay } = await mockSdk({
replayOptions: {
// @ts-ignore for tests
otherThing: 'aha',
},
});

// @ts-ignore for tests
expect(replay['_options'].otherThing).toBeUndefined();
expect(replay['_recordingOptions'].otherThing).toBe('aha');
expect(mockConsole).toBeCalledTimes(1);
expect(mockConsole)
.toBeCalledWith(`[Replay] You are passing unknown option(s) to the Replay integration: otherThing
This is deprecated and will not be supported in future versions.
Instead, put recording-specific configuration into \`recordingOptions\`, e.g. \`recordingOptions: { inlineStylesheet: false }\`.`);
});

it('allows to pass rrweb config via recordingOptions', async () => {
const { replay } = await mockSdk({
replayOptions: {
recordingOptions: {
otherThing: 'aha',
},
},
});

// @ts-ignore for tests
expect(replay['_options'].otherThing).toBeUndefined();
expect(replay['_recordingOptions'].otherThing).toBe('aha');
expect(mockConsole).toBeCalledTimes(0);
});
});
});