diff --git a/packages/replay/README.md b/packages/replay/README.md index 3057992e1a88..c1fe7b31e5bc 100644 --- a/packages/replay/README.md +++ b/packages/replay/README.md @@ -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. | diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index b68ddd7f11fa..228229798fe2 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -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'; @@ -27,7 +27,7 @@ export class Replay implements Integration { */ readonly recordingOptions: RecordingOptions; - readonly options: ReplayPluginOptions; + readonly options: ReplayOptions; protected get _isInitialized(): boolean { return _initialized; @@ -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 = {}, + ...rest }: ReplayConfiguration = {}) { this.recordingOptions = { maskAllInputs, @@ -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( @@ -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 diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 7031e60f439d..53aa8b356067 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -34,7 +34,7 @@ import type { RecordingEvent, RecordingOptions, ReplayContainer as ReplayContainerInterface, - ReplayPluginOptions, + ReplayOptions, ReplayRecordingMode, SendReplay, Session, @@ -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; @@ -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; @@ -151,7 +151,7 @@ export class ReplayContainer implements ReplayContainerInterface { } /** Get the replay integration options. */ - public getOptions(): ReplayPluginOptions { + public getOptions(): ReplayOptions { return this._options; } diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index c331273b135e..f0afd49962c5 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -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 */ @@ -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; - -export interface ReplayConfiguration extends OptionalReplayPluginOptions, RecordingOptions {} +export type ReplayConfiguration = Partial; interface CommonEventContext { /** @@ -235,5 +280,5 @@ export interface ReplayContainer { flushImmediate(): void; triggerUserActivity(): void; addUpdate(cb: AddUpdateCallback): void; - getOptions(): ReplayPluginOptions; + getOptions(): ReplayOptions; } diff --git a/packages/replay/src/types/rrweb.ts b/packages/replay/src/types/rrweb.ts index 1c6ee198eb33..337020491e37 100644 --- a/packages/replay/src/types/rrweb.ts +++ b/packages/replay/src/types/rrweb.ts @@ -1,8 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -type blockClass = string | RegExp; -type maskTextClass = string | RegExp; - enum EventType { DomContentLoaded = 0, Load = 1, @@ -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; diff --git a/packages/replay/test/unit/index-integrationSettings.test.ts b/packages/replay/test/unit/index-integrationSettings.test.ts index c78214b8c2ea..a92b14e1c927 100644 --- a/packages/replay/test/unit/index-integrationSettings.test.ts +++ b/packages/replay/test/unit/index-integrationSettings.test.ts @@ -216,4 +216,49 @@ describe('integration settings', () => { expect(replay.getOptions()._experiments).toEqual({}); }); }); + + describe('recordingOptions', () => { + let mockConsole: jest.SpyInstance; + + 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); + }); + }); });