Skip to content

Commit 095dd92

Browse files
committed
ref: Set final options in setupOnce and warn of no rates are defined
1 parent 38e1097 commit 095dd92

File tree

4 files changed

+75
-28
lines changed

4 files changed

+75
-28
lines changed

packages/replay/src/integration.ts

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getCurrentHub } from '@sentry/core';
22
import type { BrowserClientReplayOptions, Integration } from '@sentry/types';
3+
import { dropUndefinedKeys } from '@sentry/utils';
34

45
import { DEFAULT_FLUSH_MAX_DELAY, DEFAULT_FLUSH_MIN_DELAY, MASK_ALL_TEXT_SELECTOR } from './constants';
56
import { ReplayContainer } from './replay';
@@ -10,6 +11,9 @@ const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed
1011

1112
let _initialized = false;
1213

14+
type InitialReplayPluginOptions = Omit<ReplayPluginOptions, 'sessionSampleRate' | 'errorSampleRate'> &
15+
Partial<Pick<ReplayPluginOptions, 'sessionSampleRate' | 'errorSampleRate'>>;
16+
1317
/**
1418
* The main replay integration class, to be passed to `init({ integrations: [] })`.
1519
*/
@@ -29,7 +33,14 @@ export class Replay implements Integration {
2933
*/
3034
private readonly _recordingOptions: RecordingOptions;
3135

32-
private readonly _options: ReplayPluginOptions;
36+
/**
37+
* Initial options passed to the replay integration, merged with default values.
38+
* Note: `sessionSampleRate` and `errorSampleRate` are not required here, as they
39+
* can only be finally set when setupOnce() is called.
40+
*
41+
* @private
42+
*/
43+
private readonly _initialOptions: InitialReplayPluginOptions;
3344

3445
private _replay?: ReplayContainer;
3546

@@ -61,12 +72,12 @@ export class Replay implements Integration {
6172
..._recordingOptions,
6273
};
6374

64-
this._options = {
75+
this._initialOptions = {
6576
flushMinDelay,
6677
flushMaxDelay,
6778
stickySession,
68-
sessionSampleRate: 0,
69-
errorSampleRate: 0,
79+
sessionSampleRate,
80+
errorSampleRate,
7081
useCompression,
7182
maskAllText: typeof maskAllText === 'boolean' ? maskAllText : !maskTextSelector,
7283
blockAllMedia,
@@ -82,7 +93,7 @@ Instead, configure \`replaysSessionSampleRate\` directly in the SDK init options
8293
Sentry.init({ replaysSessionSampleRate: ${sessionSampleRate} })`,
8394
);
8495

85-
this._options.sessionSampleRate = sessionSampleRate;
96+
this._initialOptions.sessionSampleRate = sessionSampleRate;
8697
}
8798

8899
if (typeof errorSampleRate === 'number') {
@@ -94,17 +105,17 @@ Instead, configure \`replaysOnErrorSampleRate\` directly in the SDK init options
94105
Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
95106
);
96107

97-
this._options.errorSampleRate = errorSampleRate;
108+
this._initialOptions.errorSampleRate = errorSampleRate;
98109
}
99110

100-
if (this._options.maskAllText) {
111+
if (this._initialOptions.maskAllText) {
101112
// `maskAllText` is a more user friendly option to configure
102113
// `maskTextSelector`. This means that all nodes will have their text
103114
// content masked.
104115
this._recordingOptions.maskTextSelector = MASK_ALL_TEXT_SELECTOR;
105116
}
106117

107-
if (this._options.blockAllMedia) {
118+
if (this._initialOptions.blockAllMedia) {
108119
// `blockAllMedia` is a more user friendly option to configure blocking
109120
// embedded media elements
110121
this._recordingOptions.blockSelector = !this._recordingOptions.blockSelector
@@ -190,30 +201,45 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
190201
/** Setup the integration. */
191202
private _setup(): void {
192203
// Client is not available in constructor, so we need to wait until setupOnce
193-
this._loadReplayOptionsFromClient();
194-
195-
if (!this._options.sessionSampleRate && !this._options.errorSampleRate) {
196-
// eslint-disable-next-line no-console
197-
console.warn('Replay is disabled because both `replaysSessionSampleRate` and `replaysOnErrorSampleRate` are 0');
198-
}
204+
const finalOptions = this._loadReplayOptionsFromClient(this._initialOptions);
199205

200206
this._replay = new ReplayContainer({
201-
options: this._options,
207+
options: finalOptions,
202208
recordingOptions: this._recordingOptions,
203209
});
204210
}
205211

206212
/** Parse Replay-related options from SDK options */
207-
private _loadReplayOptionsFromClient(): void {
213+
private _loadReplayOptionsFromClient(initialOptions: InitialReplayPluginOptions): ReplayPluginOptions {
208214
const client = getCurrentHub().getClient();
209215
const opt = client && (client.getOptions() as BrowserClientReplayOptions | undefined);
210216

211-
if (opt && typeof opt.replaysSessionSampleRate === 'number') {
212-
this._options.sessionSampleRate = opt.replaysSessionSampleRate;
217+
const finalOptions = { sessionSampleRate: 0, errorSampleRate: 0, ...dropUndefinedKeys(initialOptions) };
218+
219+
if (!opt) {
220+
return finalOptions;
213221
}
214222

215-
if (opt && typeof opt.replaysOnErrorSampleRate === 'number') {
216-
this._options.errorSampleRate = opt.replaysOnErrorSampleRate;
223+
if (
224+
initialOptions.sessionSampleRate == null && // TODO remove once deprecated rates are removed
225+
initialOptions.errorSampleRate == null && // TODO remove once deprecated rates are removed
226+
opt.replaysSessionSampleRate == null &&
227+
opt.replaysOnErrorSampleRate == null
228+
) {
229+
// eslint-disable-next-line no-console
230+
console.warn(
231+
'Replay is disabled because neither `replaysSessionSampleRate` nor `replaysOnErrorSampleRate` are set',
232+
);
233+
}
234+
235+
if (typeof opt.replaysSessionSampleRate === 'number') {
236+
finalOptions.sessionSampleRate = opt.replaysSessionSampleRate;
237+
}
238+
239+
if (typeof opt.replaysOnErrorSampleRate === 'number') {
240+
finalOptions.errorSampleRate = opt.replaysOnErrorSampleRate;
217241
}
242+
243+
return finalOptions;
218244
}
219245
}

packages/replay/test/integration/integrationSettings.test.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('Integration | integrationSettings', () => {
6060
});
6161

6262
expect(replay.getOptions().sessionSampleRate).toBe(0);
63-
expect(mockConsole).toBeCalledTimes(2);
63+
expect(mockConsole).toBeCalledTimes(1);
6464
});
6565

6666
it('works with defining settings in SDK', async () => {
@@ -70,11 +70,11 @@ describe('Integration | integrationSettings', () => {
7070
expect(mockConsole).toBeCalledTimes(0);
7171
});
7272

73-
it('works with defining 0 in SDK but a warning is logged', async () => {
73+
it('works with defining 0 in SDK', async () => {
7474
const { replay } = await mockSdk({ sentryOptions: { replaysSessionSampleRate: 0 }, replayOptions: {} });
7575

7676
expect(replay.getOptions().sessionSampleRate).toBe(0);
77-
expect(mockConsole).toBeCalledTimes(1);
77+
expect(mockConsole).toBeCalledTimes(0);
7878
});
7979

8080
it('SDK option takes precedence', async () => {
@@ -87,14 +87,14 @@ describe('Integration | integrationSettings', () => {
8787
expect(mockConsole).toBeCalledTimes(1);
8888
});
8989

90-
it('SDK option takes precedence even when 0 but warnings are logged', async () => {
90+
it('SDK option takes precedence even when 0', async () => {
9191
const { replay } = await mockSdk({
9292
sentryOptions: { replaysSessionSampleRate: 0 },
9393
replayOptions: { sessionSampleRate: 0.1 },
9494
});
9595

9696
expect(replay.getOptions().sessionSampleRate).toBe(0);
97-
expect(mockConsole).toBeCalledTimes(2);
97+
expect(mockConsole).toBeCalledTimes(1);
9898
});
9999
});
100100

@@ -164,6 +164,29 @@ describe('Integration | integrationSettings', () => {
164164
});
165165
});
166166

167+
describe('all sample rates', () => {
168+
let mockConsole: jest.SpyInstance<void>;
169+
170+
beforeEach(() => {
171+
mockConsole = jest.spyOn(console, 'warn').mockImplementation(jest.fn());
172+
});
173+
174+
afterEach(() => {
175+
mockConsole.mockRestore();
176+
});
177+
178+
it('logs warning if no sample rates are set', async () => {
179+
const { replay } = await mockSdk({
180+
sentryOptions: { replaysOnErrorSampleRate: undefined, replaysSessionSampleRate: undefined },
181+
replayOptions: {},
182+
});
183+
184+
expect(replay.getOptions().sessionSampleRate).toBe(0);
185+
expect(replay.getOptions().errorSampleRate).toBe(0);
186+
expect(mockConsole).toBeCalledTimes(1);
187+
});
188+
});
189+
167190
describe('maskAllText', () => {
168191
it('works with default value', async () => {
169192
const { replay } = await mockSdk({ replayOptions: {} });

packages/replay/test/integration/sampling.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ useFakeTimers();
55

66
describe('Integration | sampling', () => {
77
it('does nothing if not sampled', async () => {
8-
const mockConsole = jest.spyOn(console, 'warn').mockImplementation(jest.fn());
98
const { record: mockRecord } = mockRrweb();
109
const { replay } = await mockSdk({
1110
replayOptions: {
@@ -30,6 +29,5 @@ describe('Integration | sampling', () => {
3029
);
3130
expect(mockRecord).not.toHaveBeenCalled();
3231
expect(spyAddListeners).not.toHaveBeenCalled();
33-
expect(mockConsole).toBeCalledTimes(1);
3432
});
3533
});

packages/replay/test/mocks/mockSdk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export async function mockSdk({ replayOptions, sentryOptions, autoStart = true }
7575
integrations: [replayIntegration],
7676
});
7777

78-
// Instead of `setupOnce`, which is tricky to test, we call this manually here)
78+
// Instead of `setupOnce`, which is tricky to test, we call this manually here
7979
replayIntegration['_setup']();
8080

8181
if (autoStart) {

0 commit comments

Comments
 (0)