Skip to content

ref(replay): Log warning if sample rates are all undefined #6959

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 5 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
73 changes: 53 additions & 20 deletions packages/replay/src/integration.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,6 +11,9 @@ const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed

let _initialized = false;

type InitialReplayPluginOptions = Omit<ReplayPluginOptions, 'sessionSampleRate' | 'errorSampleRate'> &
Partial<Pick<ReplayPluginOptions, 'sessionSampleRate' | 'errorSampleRate'>>;

/**
* The main replay integration class, to be passed to `init({ integrations: [] })`.
*/
Expand All @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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') {
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
25 changes: 24 additions & 1 deletion packages/replay/test/integration/integrationSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -164,6 +164,29 @@ describe('Integration | integrationSettings', () => {
});
});

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

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: {} });
Expand Down