From 877a267410b6f7c8a1a401e315690e2f3fa87019 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 3 Jan 2023 15:51:23 +0100 Subject: [PATCH] feat(replay): Streamline replay options This streamlines options to be passed to replay a bit: First, we deprecate passing arbitrary rrweb config through. Instead, you can provide `recordingOptions` directly, which will be passed through. For now, the old way still works, but we will remove this eventually. Secondly, we explicitly defined the config options we _do_ support. This should cover most things that users are actually using, thus most users shouldn't need to actually do anything. Finally, this also separates the options we pass to the integration from the options we store on the replay container. There is no need to keep all the rrweb-specific config there, we only need this at init-time, and can then discard it. --- packages/replay/README.md | 2 +- packages/replay/src/integration.ts | 34 ++++++--- packages/replay/src/replay.ts | 8 +- packages/replay/src/types.ts | 73 +++++++++++++++---- packages/replay/src/types/rrweb.ts | 7 +- .../unit/index-integrationSettings.test.ts | 45 ++++++++++++ 6 files changed, 134 insertions(+), 35 deletions(-) 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); + }); + }); });