Skip to content

Commit 3f14a57

Browse files
authored
feat(replay): Allow to define sample rates on SDK level (#6387)
1 parent d9727c6 commit 3f14a57

File tree

6 files changed

+157
-35
lines changed

6 files changed

+157
-35
lines changed

packages/browser/src/client.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,26 @@ import { Breadcrumbs } from './integrations';
88
import { BREADCRUMB_INTEGRATION_ID } from './integrations/breadcrumbs';
99
import { BrowserTransportOptions } from './transports/types';
1010

11+
type BrowserClientReplayOptions = {
12+
/**
13+
* The sample rate for session-long replays.
14+
* 1.0 will record all sessions and 0 will record none.
15+
*/
16+
replaysSampleRate?: number;
17+
18+
/**
19+
* The sample rate for sessions that has had an error occur.
20+
* This is independent of `sessionSampleRate`.
21+
* 1.0 will record all sessions and 0 will record none.
22+
*/
23+
replaysOnErrorSampleRate?: number;
24+
};
25+
1126
/**
1227
* Configuration options for the Sentry Browser SDK.
1328
* @see @sentry/types Options for more information.
1429
*/
15-
export type BrowserOptions = Options<BrowserTransportOptions>;
30+
export type BrowserOptions = Options<BrowserTransportOptions> & BrowserClientReplayOptions;
1631

1732
/**
1833
* Configuration options for the Sentry Browser SDK Client class

packages/replay/jest.setup.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ afterEach(() => {
2424
}
2525

2626
const client = hub?.getClient();
27-
if (typeof client?.getTransport !== 'function') {
27+
// This can be weirded up by mocks/tests
28+
if (
29+
!client ||
30+
!client.getTransport ||
31+
typeof client.getTransport !== 'function' ||
32+
typeof client.getTransport()?.send !== 'function'
33+
) {
2834
return;
2935
}
3036

packages/replay/src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable max-lines */ // TODO: We might want to split this file up
2+
import type { BrowserClient, BrowserOptions } from '@sentry/browser';
23
import { addGlobalEventProcessor, getCurrentHub, Scope, setContext } from '@sentry/core';
34
import { Breadcrumb, Client, Event, Integration } from '@sentry/types';
45
import { addInstrumentationHandler, createEnvelope, logger } from '@sentry/utils';
@@ -213,6 +214,10 @@ export class Replay implements Integration {
213214
if (!isBrowser()) {
214215
return;
215216
}
217+
218+
// Client is not available in constructor, so we need to wait until setupOnce
219+
this._loadReplayOptionsFromClient();
220+
216221
// XXX: See method comments above
217222
setTimeout(() => this.start());
218223
}
@@ -1333,4 +1338,18 @@ export class Replay implements Integration {
13331338
saveSession(this.session);
13341339
}
13351340
}
1341+
1342+
/** Parse Replay-related options from SDK options */
1343+
private _loadReplayOptionsFromClient(): void {
1344+
const client = getCurrentHub().getClient() as BrowserClient | undefined;
1345+
const opt = client && (client.getOptions() as BrowserOptions | undefined);
1346+
1347+
if (opt && opt.replaysSampleRate) {
1348+
this.options.sessionSampleRate = opt.replaysSampleRate;
1349+
}
1350+
1351+
if (opt && opt.replaysOnErrorSampleRate) {
1352+
this.options.errorSampleRate = opt.replaysOnErrorSampleRate;
1353+
}
1354+
}
13361355
}

packages/replay/test/mocks/mockSdk.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,12 @@ export async function mockSdk({
5454

5555
init({ ...sentryOptions, integrations: [replay] });
5656

57+
// setupOnce is only called the first time, so we ensure to re-parse the options every time
58+
replay['_loadReplayOptionsFromClient']();
59+
60+
// The first time the integration is used, `start()` is called (in setupOnce)
61+
// For consistency, we want to stop that
62+
replay.stop();
63+
5764
return { replay };
5865
}

packages/replay/test/unit/blockAllMedia.test.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { mockSdk } from '@test';
2+
3+
import { Replay } from '../../src';
4+
5+
let replay: Replay;
6+
7+
beforeEach(() => {
8+
jest.resetModules();
9+
});
10+
11+
describe('blockAllMedia', () => {
12+
it('sets the correct configuration when `blockAllMedia` is disabled', async () => {
13+
({ replay } = await mockSdk({ replayOptions: { blockAllMedia: false } }));
14+
15+
expect(replay.recordingOptions.blockSelector).toBe('[data-sentry-block]');
16+
});
17+
18+
it('sets the correct configuration when `blockSelector` is empty and `blockAllMedia` is enabled', async () => {
19+
({ replay } = await mockSdk({ replayOptions: { blockSelector: '' } }));
20+
21+
expect(replay.recordingOptions.blockSelector).toMatchInlineSnapshot(
22+
'"img,image,svg,path,rect,area,video,object,picture,embed,map,audio"',
23+
);
24+
});
25+
26+
it('preserves `blockSelector` when `blockAllMedia` is enabled', async () => {
27+
({ replay } = await mockSdk({
28+
replayOptions: { blockSelector: '[data-test-blockSelector]' },
29+
}));
30+
31+
expect(replay.recordingOptions.blockSelector).toMatchInlineSnapshot(
32+
'"[data-test-blockSelector],img,image,svg,path,rect,area,video,object,picture,embed,map,audio"',
33+
);
34+
});
35+
});
36+
37+
describe('replaysSampleRate', () => {
38+
it('works with defining settings in integration', async () => {
39+
({ replay } = await mockSdk({ replayOptions: { sessionSampleRate: 0.5 } }));
40+
41+
expect(replay.options.sessionSampleRate).toBe(0.5);
42+
});
43+
44+
it('works with defining settings in SDK', async () => {
45+
({ replay } = await mockSdk({ sentryOptions: { replaysSampleRate: 0.5 } }));
46+
47+
expect(replay.options.sessionSampleRate).toBe(0.5);
48+
});
49+
50+
it('SDK option takes precedence', async () => {
51+
({ replay } = await mockSdk({
52+
sentryOptions: { replaysSampleRate: 0.5 },
53+
replayOptions: { sessionSampleRate: 0.1 },
54+
}));
55+
56+
expect(replay.options.sessionSampleRate).toBe(0.5);
57+
});
58+
});
59+
60+
describe('replaysOnErrorSampleRate', () => {
61+
it('works with defining settings in integration', async () => {
62+
({ replay } = await mockSdk({ replayOptions: { errorSampleRate: 0.5 } }));
63+
64+
expect(replay.options.errorSampleRate).toBe(0.5);
65+
});
66+
67+
it('works with defining settings in SDK', async () => {
68+
({ replay } = await mockSdk({ sentryOptions: { replaysOnErrorSampleRate: 0.5 } }));
69+
70+
expect(replay.options.errorSampleRate).toBe(0.5);
71+
});
72+
73+
it('SDK option takes precedence', async () => {
74+
({ replay } = await mockSdk({
75+
sentryOptions: { replaysOnErrorSampleRate: 0.5 },
76+
replayOptions: { errorSampleRate: 0.1 },
77+
}));
78+
79+
expect(replay.options.errorSampleRate).toBe(0.5);
80+
});
81+
});
82+
83+
describe('maskAllText', () => {
84+
it('works with default value', async () => {
85+
({ replay } = await mockSdk({ replayOptions: {} }));
86+
87+
// Default is true
88+
expect(replay.recordingOptions.maskTextSelector).toBe('*');
89+
});
90+
91+
it('works with true', async () => {
92+
({ replay } = await mockSdk({ replayOptions: { maskAllText: true } }));
93+
94+
expect(replay.recordingOptions.maskTextSelector).toBe('*');
95+
});
96+
97+
it('works with false', async () => {
98+
({ replay } = await mockSdk({ replayOptions: { maskAllText: false } }));
99+
100+
expect(replay.recordingOptions.maskTextSelector).toBe(undefined);
101+
});
102+
103+
it('overwrites custom maskTextSelector option', async () => {
104+
({ replay } = await mockSdk({ replayOptions: { maskAllText: true, maskTextSelector: '[custom]' } }));
105+
106+
expect(replay.recordingOptions.maskTextSelector).toBe('*');
107+
});
108+
});

0 commit comments

Comments
 (0)