diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index daf676c82efd..91c8df5cbd40 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -1,5 +1,6 @@ import { getCurrentHub } from '@sentry/core'; import type { BrowserClientReplayOptions, Integration } from '@sentry/types'; +import { dropUndefinedKeys } from '@sentry/utils'; import { DEFAULT_FLUSH_MAX_DELAY, DEFAULT_FLUSH_MIN_DELAY, MASK_ALL_TEXT_SELECTOR } from './constants'; import { ReplayContainer } from './replay'; @@ -10,6 +11,9 @@ const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed let _initialized = false; +type InitialReplayPluginOptions = Omit & + Partial>; + /** * The main replay integration class, to be passed to `init({ integrations: [] })`. */ @@ -29,7 +33,14 @@ export class Replay implements Integration { */ private readonly _recordingOptions: RecordingOptions; - private readonly _options: ReplayPluginOptions; + /** + * Initial options passed to the replay integration, merged with default values. + * Note: `sessionSampleRate` and `errorSampleRate` are not required here, as they + * can only be finally set when setupOnce() is called. + * + * @private + */ + private readonly _initialOptions: InitialReplayPluginOptions; private _replay?: ReplayContainer; @@ -61,12 +72,12 @@ export class Replay implements Integration { ..._recordingOptions, }; - this._options = { + this._initialOptions = { flushMinDelay, flushMaxDelay, stickySession, - sessionSampleRate: 0, - errorSampleRate: 0, + sessionSampleRate, + errorSampleRate, useCompression, maskAllText: typeof maskAllText === 'boolean' ? maskAllText : !maskTextSelector, blockAllMedia, @@ -82,7 +93,7 @@ Instead, configure \`replaysSessionSampleRate\` directly in the SDK init options Sentry.init({ replaysSessionSampleRate: ${sessionSampleRate} })`, ); - this._options.sessionSampleRate = sessionSampleRate; + this._initialOptions.sessionSampleRate = sessionSampleRate; } if (typeof errorSampleRate === 'number') { @@ -94,17 +105,17 @@ Instead, configure \`replaysOnErrorSampleRate\` directly in the SDK init options Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, ); - this._options.errorSampleRate = errorSampleRate; + this._initialOptions.errorSampleRate = errorSampleRate; } - if (this._options.maskAllText) { + if (this._initialOptions.maskAllText) { // `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 (this._initialOptions.blockAllMedia) { // `blockAllMedia` is a more user friendly option to configure blocking // embedded media elements this._recordingOptions.blockSelector = !this._recordingOptions.blockSelector @@ -190,25 +201,47 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, /** Setup the integration. */ private _setup(): void { // Client is not available in constructor, so we need to wait until setupOnce - this._loadReplayOptionsFromClient(); + const finalOptions = loadReplayOptionsFromClient(this._initialOptions); this._replay = new ReplayContainer({ - options: this._options, + options: finalOptions, recordingOptions: this._recordingOptions, }); } +} - /** Parse Replay-related options from SDK options */ - private _loadReplayOptionsFromClient(): void { - const client = getCurrentHub().getClient(); - const opt = client && (client.getOptions() as BrowserClientReplayOptions | undefined); +/** Parse Replay-related options from SDK options */ +function loadReplayOptionsFromClient(initialOptions: InitialReplayPluginOptions): ReplayPluginOptions { + const client = getCurrentHub().getClient(); + const opt = client && (client.getOptions() as BrowserClientReplayOptions); - if (opt && typeof opt.replaysSessionSampleRate === 'number') { - this._options.sessionSampleRate = opt.replaysSessionSampleRate; - } + const finalOptions = { sessionSampleRate: 0, errorSampleRate: 0, ...dropUndefinedKeys(initialOptions) }; - if (opt && typeof opt.replaysOnErrorSampleRate === 'number') { - this._options.errorSampleRate = opt.replaysOnErrorSampleRate; - } + if (!opt) { + // eslint-disable-next-line no-console + console.warn('SDK client is not available.'); + return finalOptions; } + + if ( + initialOptions.sessionSampleRate == null && // TODO remove once deprecated rates are removed + initialOptions.errorSampleRate == null && // TODO remove once deprecated rates are removed + opt.replaysSessionSampleRate == null && + opt.replaysOnErrorSampleRate == null + ) { + // eslint-disable-next-line no-console + console.warn( + 'Replay is disabled because neither `replaysSessionSampleRate` nor `replaysOnErrorSampleRate` are set.', + ); + } + + if (typeof opt.replaysSessionSampleRate === 'number') { + finalOptions.sessionSampleRate = opt.replaysSessionSampleRate; + } + + if (typeof opt.replaysOnErrorSampleRate === 'number') { + finalOptions.errorSampleRate = opt.replaysOnErrorSampleRate; + } + + return finalOptions; } diff --git a/packages/replay/test/integration/integrationSettings.test.ts b/packages/replay/test/integration/integrationSettings.test.ts index 6c63839b1c06..cd6220486bdc 100644 --- a/packages/replay/test/integration/integrationSettings.test.ts +++ b/packages/replay/test/integration/integrationSettings.test.ts @@ -53,7 +53,7 @@ describe('Integration | integrationSettings', () => { expect(mockConsole).toBeCalledTimes(1); }); - it('works with defining 0 in integration', async () => { + it('works with defining 0 in integration but logs warnings', async () => { const { replay } = await mockSdk({ replayOptions: { sessionSampleRate: 0 }, sentryOptions: { replaysSessionSampleRate: undefined }, @@ -164,6 +164,29 @@ describe('Integration | integrationSettings', () => { }); }); + describe('all sample rates', () => { + let mockConsole: jest.SpyInstance; + + beforeEach(() => { + mockConsole = jest.spyOn(console, 'warn').mockImplementation(jest.fn()); + }); + + afterEach(() => { + mockConsole.mockRestore(); + }); + + it('logs warning if no sample rates are set', async () => { + const { replay } = await mockSdk({ + sentryOptions: { replaysOnErrorSampleRate: undefined, replaysSessionSampleRate: undefined }, + replayOptions: {}, + }); + + expect(replay.getOptions().sessionSampleRate).toBe(0); + expect(replay.getOptions().errorSampleRate).toBe(0); + expect(mockConsole).toBeCalledTimes(1); + }); + }); + describe('maskAllText', () => { it('works with default value', async () => { const { replay } = await mockSdk({ replayOptions: {} });