Skip to content

Commit 7a35088

Browse files
authored
feat(replay): Allow to opt-in to capture replay exceptions (#6482)
You can define `_experiments: { captureExceptions: true }`.
1 parent 3dc61a5 commit 7a35088

File tree

8 files changed

+58
-58
lines changed

8 files changed

+58
-58
lines changed

packages/replay/src/integration.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Integration } from '@sentry/types';
55
import { DEFAULT_ERROR_SAMPLE_RATE, DEFAULT_SESSION_SAMPLE_RATE } from './constants';
66
import { ReplayContainer } from './replay';
77
import type { RecordingOptions, ReplayConfiguration, ReplayPluginOptions } from './types';
8-
import { captureInternalException } from './util/captureInternalException';
98
import { isBrowser } from './util/isBrowser';
109

1110
const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed,map,audio';
@@ -51,6 +50,7 @@ export class Replay implements Integration {
5150
maskAllText = true,
5251
maskAllInputs = true,
5352
blockAllMedia = true,
53+
_experiments = {},
5454
blockClass = 'sentry-block',
5555
ignoreClass = 'sentry-ignore',
5656
maskTextClass = 'sentry-mask',
@@ -76,6 +76,7 @@ export class Replay implements Integration {
7676
useCompression,
7777
maskAllText,
7878
blockAllMedia,
79+
_experiments,
7980
};
8081

8182
if (typeof sessionSampleRate === 'number') {
@@ -118,9 +119,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
118119
}
119120

120121
if (isBrowser() && this._isInitialized) {
121-
const error = new Error('Multiple Sentry Session Replay instances are not supported');
122-
captureInternalException(error);
123-
throw error;
122+
throw new Error('Multiple Sentry Session Replay instances are not supported');
124123
}
125124

126125
this._isInitialized = true;

packages/replay/src/replay.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable max-lines */ // TODO: We might want to split this file up
2-
import { addGlobalEventProcessor, getCurrentHub, Scope, setContext } from '@sentry/core';
2+
import { addGlobalEventProcessor, captureException, getCurrentHub, Scope, setContext } from '@sentry/core';
33
import { Breadcrumb, Client, Event } from '@sentry/types';
44
import { addInstrumentationHandler, createEnvelope, logger } from '@sentry/utils';
55
import debounce from 'lodash.debounce';
@@ -40,7 +40,6 @@ import type {
4040
} from './types';
4141
import { addEvent } from './util/addEvent';
4242
import { addMemoryEntry } from './util/addMemoryEntry';
43-
import { captureInternalException } from './util/captureInternalException';
4443
import { createBreadcrumb } from './util/createBreadcrumb';
4544
import { createPayload } from './util/createPayload';
4645
import { createPerformanceSpans } from './util/createPerformanceSpans';
@@ -160,7 +159,7 @@ export class ReplayContainer implements ReplayContainerInterface {
160159

161160
// If there is no session, then something bad has happened - can't continue
162161
if (!this.session) {
163-
captureInternalException(new Error('Invalid session'));
162+
this.handleException(new Error('No session found'));
164163
return;
165164
}
166165

@@ -208,8 +207,7 @@ export class ReplayContainer implements ReplayContainerInterface {
208207
emit: this.handleRecordingEmit,
209208
});
210209
} catch (err) {
211-
__DEBUG_BUILD__ && logger.error('[Replay]', err);
212-
captureInternalException(err);
210+
this.handleException(err);
213211
}
214212
}
215213

@@ -239,8 +237,7 @@ export class ReplayContainer implements ReplayContainerInterface {
239237
this.eventBuffer?.destroy();
240238
this.eventBuffer = null;
241239
} catch (err) {
242-
__DEBUG_BUILD__ && logger.error('[Replay]', err);
243-
captureInternalException(err);
240+
this.handleException(err);
244241
}
245242
}
246243

@@ -257,8 +254,7 @@ export class ReplayContainer implements ReplayContainerInterface {
257254
this._stopRecording = undefined;
258255
}
259256
} catch (err) {
260-
__DEBUG_BUILD__ && logger.error('[Replay]', err);
261-
captureInternalException(err);
257+
this.handleException(err);
262258
}
263259
}
264260

@@ -273,14 +269,22 @@ export class ReplayContainer implements ReplayContainerInterface {
273269
this.startRecording();
274270
}
275271

272+
/** A wrapper to conditionally capture exceptions. */
273+
handleException(error: unknown): void {
274+
__DEBUG_BUILD__ && logger.error('[Replay]', error);
275+
276+
if (this.options._experiments && this.options._experiments.captureExceptions) {
277+
captureException(error);
278+
}
279+
}
280+
276281
/** for tests only */
277282
clearSession(): void {
278283
try {
279284
deleteSession();
280285
this.session = undefined;
281286
} catch (err) {
282-
__DEBUG_BUILD__ && logger.error('[Replay]', err);
283-
captureInternalException(err);
287+
this.handleException(err);
284288
}
285289
}
286290

@@ -358,8 +362,7 @@ export class ReplayContainer implements ReplayContainerInterface {
358362
this._hasInitializedCoreListeners = true;
359363
}
360364
} catch (err) {
361-
__DEBUG_BUILD__ && logger.error('[Replay]', err);
362-
captureInternalException(err);
365+
this.handleException(err);
363366
}
364367

365368
// _performanceObserver //
@@ -387,8 +390,7 @@ export class ReplayContainer implements ReplayContainerInterface {
387390
this._performanceObserver = null;
388391
}
389392
} catch (err) {
390-
__DEBUG_BUILD__ && logger.error('[Replay]', err);
391-
captureInternalException(err);
393+
this.handleException(err);
392394
}
393395
}
394396

@@ -827,8 +829,7 @@ export class ReplayContainer implements ReplayContainerInterface {
827829
eventContext,
828830
});
829831
} catch (err) {
830-
__DEBUG_BUILD__ && logger.error(err);
831-
captureInternalException(err);
832+
this.handleException(err);
832833
}
833834
}
834835

@@ -1021,12 +1022,11 @@ export class ReplayContainer implements ReplayContainerInterface {
10211022
this.resetRetries();
10221023
return true;
10231024
} catch (err) {
1024-
__DEBUG_BUILD__ && logger.error(err);
10251025
// Capture error for every failed replay
10261026
setContext('Replays', {
10271027
_retryCount: this._retryCount,
10281028
});
1029-
captureInternalException(err);
1029+
this.handleException(err);
10301030

10311031
// If an error happened here, it's likely that uploading the attachment
10321032
// failed, we'll can retry with the same events payload

packages/replay/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ export interface ReplayPluginOptions extends SessionOptions {
105105
* Block all media (e.g. images, svg, video) in recordings.
106106
*/
107107
blockAllMedia: boolean;
108+
109+
/**
110+
* _experiments allows users to enable experimental or internal features.
111+
* We don't consider such features as part of the public API and hence we don't guarantee semver for them.
112+
* Experimental features can be added, changed or removed at any time.
113+
*
114+
* Default: undefined
115+
*/
116+
_experiments?: Partial<{ captureExceptions: boolean }>;
108117
}
109118

110119
// These are optional for ReplayPluginOptions because the plugin sets default values

packages/replay/src/util/captureInternalException.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

packages/replay/test/mocks/resetSdkMock.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export async function resetSdkMock({ replayOptions, sentryOptions }: MockSdkPara
1010
mockRecord: RecordMock;
1111
mockTransportSend: MockTransportSend;
1212
replay: ReplayContainer;
13-
spyCaptureException: jest.SpyInstance;
1413
}> {
1514
let domHandler: DomHandler;
1615

@@ -29,13 +28,6 @@ export async function resetSdkMock({ replayOptions, sentryOptions }: MockSdkPara
2928
const { mockRrweb } = await import('./mockRrweb');
3029
const { record: mockRecord } = mockRrweb();
3130

32-
// Because of `resetModules`, we need to import and add a spy for
33-
// `@sentry/core` here before `mockSdk` is called
34-
// XXX: This is probably going to make writing future tests difficult and/or
35-
// bloat this area of code
36-
const SentryCore = await import('@sentry/core');
37-
const spyCaptureException = jest.spyOn(SentryCore, 'captureException');
38-
3931
const { replay } = await mockSdk({
4032
replayOptions,
4133
sentryOptions,
@@ -54,6 +46,5 @@ export async function resetSdkMock({ replayOptions, sentryOptions }: MockSdkPara
5446
mockRecord,
5547
mockTransportSend,
5648
replay,
57-
spyCaptureException,
5849
};
5950
}

packages/replay/test/unit/index-errorSampleRate.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
jest.unmock('@sentry/browser');
2-
3-
import { captureException } from '@sentry/browser';
1+
import { captureException } from '@sentry/core';
42

53
import { REPLAY_SESSION_KEY, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from '../../src/constants';
64
import { addEvent } from '../../src/util/addEvent';

packages/replay/test/unit/index-integrationSettings.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,24 @@ describe('integration settings', () => {
189189
expect(replay.recordingOptions.maskTextSelector).toBe('*');
190190
});
191191
});
192+
193+
describe('_experiments', () => {
194+
it('works with defining _experiments in integration', async () => {
195+
const { replay } = await mockSdk({
196+
replayOptions: { _experiments: { captureExceptions: true } },
197+
sentryOptions: {},
198+
});
199+
200+
expect(replay.options._experiments).toEqual({ captureExceptions: true });
201+
});
202+
203+
it('works without defining _experiments in integration', async () => {
204+
const { replay } = await mockSdk({
205+
replayOptions: {},
206+
sentryOptions: {},
207+
});
208+
209+
expect(replay.options._experiments).toEqual({});
210+
});
211+
});
192212
});

packages/replay/test/unit/index.test.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
jest.mock('./../../src/util/isInternal', () => ({
2-
isInternal: jest.fn(() => true),
3-
}));
4-
51
import { EventType } from 'rrweb';
62

73
import { MAX_SESSION_LIFE, REPLAY_SESSION_KEY, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from '../../src/constants';
@@ -98,7 +94,6 @@ describe('Replay', () => {
9894
let mockRecord: RecordMock;
9995
let mockTransportSend: MockTransportSend;
10096
let domHandler: DomHandler;
101-
let spyCaptureException: jest.MockedFunction<any>;
10297
const prevLocation = WINDOW.location;
10398

10499
type MockSendReplayRequest = jest.MockedFunction<typeof replay.sendReplayRequest>;
@@ -110,7 +105,7 @@ describe('Replay', () => {
110105
});
111106

112107
beforeEach(async () => {
113-
({ mockRecord, mockTransportSend, domHandler, replay, spyCaptureException } = await resetSdkMock({
108+
({ mockRecord, mockTransportSend, domHandler, replay } = await resetSdkMock({
114109
replayOptions: {
115110
stickySession: false,
116111
},
@@ -631,6 +626,9 @@ describe('Replay', () => {
631626
// Suppress console.errors
632627
const mockConsole = jest.spyOn(console, 'error').mockImplementation(jest.fn());
633628

629+
// Check errors
630+
const spyHandleException = jest.spyOn(replay, 'handleException');
631+
634632
expect(replay.session?.segmentId).toBe(0);
635633

636634
// fail the first and second requests and pass the third one
@@ -662,11 +660,10 @@ describe('Replay', () => {
662660
expect(replay.sendReplayRequest).toHaveBeenCalledTimes(4);
663661
expect(replay.sendReplay).toHaveBeenCalledTimes(4);
664662

665-
expect(spyCaptureException).toHaveBeenCalledTimes(5);
666663
// Retries = 3 (total tries = 4 including initial attempt)
667664
// + last exception is max retries exceeded
668-
expect(spyCaptureException).toHaveBeenCalledTimes(5);
669-
expect(spyCaptureException).toHaveBeenLastCalledWith(new Error('Unable to send Replay - max retries exceeded'));
665+
expect(spyHandleException).toHaveBeenCalledTimes(5);
666+
expect(spyHandleException).toHaveBeenLastCalledWith(new Error('Unable to send Replay - max retries exceeded'));
670667

671668
// No activity has occurred, session's last activity should remain the same
672669
expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP);

0 commit comments

Comments
 (0)