diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5684843d..7c07131e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ ## Unreleased +### Changes + +- Use `Replay` interface for `browserReplayIntegration` return type ([#4858](https://github.com/getsentry/sentry-react-native/pull/4858)) +- Allow using `browserReplayIntegration` without `isWeb` guard ([#4858](https://github.com/getsentry/sentry-react-native/pull/4858)) + - The integration returns noop in non-browser environments + ### Dependencies - Bump Android SDK from v8.11.1 to v8.12.0 ([#4847](https://github.com/getsentry/sentry-react-native/pull/4847)) @@ -59,7 +65,7 @@ Version 7 of the SDK is compatible with Sentry self-hosted versions 24.4.2 or hi - Fork `scope` if custom scope is passed to `startSpanManual` or `startSpan` - On React Native Web, `browserSessionIntegration` is added when `enableAutoSessionTracking` is set to `True` ([#4732](https://github.com/getsentry/sentry-react-native/pull/4732)) -Change `Cold/Warm App Start` span description to `Cold/Warm Start` ([#4636](https://github.com/getsentry/sentry-react-native/pull/4636)) +- Change `Cold/Warm App Start` span description to `Cold/Warm Start` ([#4636](https://github.com/getsentry/sentry-react-native/pull/4636)) ### Dependencies diff --git a/packages/core/src/js/replay/browserReplay.ts b/packages/core/src/js/replay/browserReplay.ts index b72c0be69f..a918b7e8f1 100644 --- a/packages/core/src/js/replay/browserReplay.ts +++ b/packages/core/src/js/replay/browserReplay.ts @@ -1,8 +1,30 @@ import { replayIntegration } from '@sentry/react'; -const browserReplayIntegration = ( - options: Parameters<typeof replayIntegration>[0] = {}, -): ReturnType<typeof replayIntegration> => { +import { notWeb } from '../utils/environment'; +import type { Replay } from './replayInterface'; + +/** + * ReplayConfiguration for browser replay integration. + * + * See the [Configuration documentation](https://docs.sentry.io/platforms/javascript/session-replay/configuration/) for more information. + */ +type ReplayConfiguration = Parameters<typeof replayIntegration>[0]; + +// https://github.com/getsentry/sentry-javascript/blob/e00cb04f1bbf494067cd8475d392266ba296987a/packages/replay-internal/src/integration.ts#L109 +const INTEGRATION_NAME = 'Replay'; + +/** + * Browser Replay integration for React Native. + * + * See the [Browser Replay documentation](https://docs.sentry.io/platforms/javascript/session-replay/) for more information. + */ +const browserReplayIntegration = (options: ReplayConfiguration = {}): Replay => { + if (notWeb()) { + // This is required because `replayIntegration` browser check doesn't + // work for React Native. + return browserReplayIntegrationNoop(); + } + return replayIntegration({ ...options, mask: ['.sentry-react-native-mask', ...(options.mask || [])], @@ -10,4 +32,18 @@ const browserReplayIntegration = ( }); }; +const browserReplayIntegrationNoop = (): Replay => { + return { + name: INTEGRATION_NAME, + // eslint-disable-next-line @typescript-eslint/no-empty-function + start: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + startBuffering: () => {}, + stop: () => Promise.resolve(), + flush: () => Promise.resolve(), + getReplayId: () => undefined, + getRecordingMode: () => undefined, + }; +}; + export { browserReplayIntegration }; diff --git a/packages/core/src/js/replay/mobilereplay.ts b/packages/core/src/js/replay/mobilereplay.ts index 3b63e40957..b55bf5950d 100644 --- a/packages/core/src/js/replay/mobilereplay.ts +++ b/packages/core/src/js/replay/mobilereplay.ts @@ -150,9 +150,6 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau // https://github.com/getsentry/sentry-javascript/blob/develop/packages/replay-internal/src/integration.ts#L45 return { name: MOBILE_REPLAY_INTEGRATION_NAME, - setupOnce() { - /* Noop */ - }, setup, processEvent, options: options, @@ -162,9 +159,6 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau const mobileReplayIntegrationNoop = (): MobileReplayIntegration => { return { name: MOBILE_REPLAY_INTEGRATION_NAME, - setupOnce() { - /* Noop */ - }, options: defaultOptions, }; }; diff --git a/packages/core/src/js/replay/replayInterface.ts b/packages/core/src/js/replay/replayInterface.ts new file mode 100644 index 0000000000..0308a5a385 --- /dev/null +++ b/packages/core/src/js/replay/replayInterface.ts @@ -0,0 +1,57 @@ +import type { Integration, ReplayRecordingMode } from '@sentry/core'; + +// Based on Replay Class https://github.com/getsentry/sentry-javascript/blob/e00cb04f1bbf494067cd8475d392266ba296987a/packages/replay-internal/src/integration.ts#L50 + +/** + * Common interface for React Native Replay integrations. + * + * Both browser and mobile replay integrations should implement this interface + * to allow user manually control the replay. + */ +export interface Replay extends Integration { + /** + * Start a replay regardless of sampling rate. Calling this will always + * create a new session. Will log a message if replay is already in progress. + * + * Creates or loads a session, attaches listeners to varying events (DOM, + * PerformanceObserver, Recording, Sentry SDK, etc) + */ + start(): void; + + /** + * Start replay buffering. Buffers until `flush()` is called or, if + * `replaysOnErrorSampleRate` > 0, until an error occurs. + */ + startBuffering(): void; + + /** + * Currently, this needs to be manually called (e.g. for tests). Sentry SDK + * does not support a teardown + */ + stop(): Promise<void>; + + /** + * If not in "session" recording mode, flush event buffer which will create a new replay. + * If replay is not enabled, a new session replay is started. + * Unless `continueRecording` is false, the replay will continue to record and + * behave as a "session"-based replay. + * + * Otherwise, queue up a flush. + */ + flush(options?: { continueRecording?: boolean }): Promise<void>; + + /** + * Get the current session ID. + */ + getReplayId(): string | undefined; + + /** + * Get the current recording mode. This can be either `session` or `buffer`. + * + * `session`: Recording the whole session, sending it continuously + * `buffer`: Always keeping the last 60s of recording, requires: + * - having replaysOnErrorSampleRate > 0 to capture replay when an error occurs + * - or calling `flush()` to send the replay + */ + getRecordingMode(): ReplayRecordingMode | undefined; +} diff --git a/packages/core/test/replay/browserReplay.test.ts b/packages/core/test/replay/browserReplay.test.ts new file mode 100644 index 0000000000..be12c27e21 --- /dev/null +++ b/packages/core/test/replay/browserReplay.test.ts @@ -0,0 +1,24 @@ +import { describe, test } from '@jest/globals'; +import * as SentryReact from '@sentry/react'; +import { spyOn } from 'jest-mock'; + +import { browserReplayIntegration } from '../../src/js/replay/browserReplay'; +import * as environment from '../../src/js/utils/environment'; + +describe('Browser Replay', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should not call replayIntegration if not web', () => { + spyOn(environment, 'notWeb').mockReturnValue(true); + spyOn(SentryReact, 'replayIntegration').mockImplementation(() => { + throw new Error('replayIntegration should not be called'); + }); + + const integration = browserReplayIntegration(); + + expect(integration).toBeDefined(); + expect(SentryReact.replayIntegration).not.toHaveBeenCalled(); + }); +}); diff --git a/samples/expo/app/_layout.tsx b/samples/expo/app/_layout.tsx index b3836cf6d3..f8a06db99f 100644 --- a/samples/expo/app/_layout.tsx +++ b/samples/expo/app/_layout.tsx @@ -10,7 +10,6 @@ import * as Sentry from '@sentry/react-native'; import { ErrorEvent } from '@sentry/core'; import { isExpoGo } from '../utils/isExpoGo'; import { LogBox } from 'react-native'; -import { isWeb } from '../utils/isWeb'; import * as ImagePicker from 'expo-image-picker'; export { @@ -58,13 +57,19 @@ Sentry.init({ }), navigationIntegration, Sentry.reactNativeTracingIntegration(), + Sentry.mobileReplayIntegration({ + maskAllImages: true, + maskAllText: true, + maskAllVectors: true, + }), + Sentry.browserReplayIntegration({ + maskAllInputs: true, + maskAllText: true, + }), Sentry.feedbackIntegration({ imagePicker: ImagePicker, }), ); - if (isWeb()) { - integrations.push(Sentry.browserReplayIntegration()); - } return integrations.filter(i => i.name !== 'Dedupe'); }, enableAutoSessionTracking: true,