From 933fa79523b8a02c2729e79a5188db4c7f58dfa7 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 23 May 2025 13:18:53 +0200 Subject: [PATCH 1/4] feat(replay): Allow using `browserReplayIntegration` without `isWeb` guard --- CHANGELOG.md | 5 +- packages/core/src/js/replay/browserReplay.ts | 40 ++++++++++++- packages/core/src/js/replay/mobilereplay.ts | 6 -- .../core/src/js/replay/replayInterface.ts | 59 +++++++++++++++++++ .../core/test/replay/browserReplay.test.ts | 24 ++++++++ samples/expo/app/_layout.tsx | 13 ++-- 6 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 packages/core/src/js/replay/replayInterface.ts create mode 100644 packages/core/test/replay/browserReplay.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5684843d..c6c8f37862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,7 +59,10 @@ 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)) +- 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 diff --git a/packages/core/src/js/replay/browserReplay.ts b/packages/core/src/js/replay/browserReplay.ts index b72c0be69f..bac73a4704 100644 --- a/packages/core/src/js/replay/browserReplay.ts +++ b/packages/core/src/js/replay/browserReplay.ts @@ -1,8 +1,32 @@ import { replayIntegration } from '@sentry/react'; +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[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: Parameters[0] = {}, -): ReturnType => { + options: ReplayConfiguration = {}, +): Replay => { + if (notWeb()) { + // This is required because because `replayIntegration` browser check doesn't + // work for React Native. + return browserReplayIntegrationNoop(); + } + return replayIntegration({ ...options, mask: ['.sentry-react-native-mask', ...(options.mask || [])], @@ -10,4 +34,16 @@ const browserReplayIntegration = ( }); }; +const browserReplayIntegrationNoop = (): Replay => { + return { + name: INTEGRATION_NAME, + start: () => {}, + 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..58d60d2218 --- /dev/null +++ b/packages/core/src/js/replay/replayInterface.ts @@ -0,0 +1,59 @@ +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; + + /** + * 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; + + /** + * 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..704fa44fcc --- /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, From 542bcd5b2b8f5b28bbd500e5055795ab58fd93f9 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 23 May 2025 13:23:32 +0200 Subject: [PATCH 2/4] fix lint --- packages/core/src/js/replay/browserReplay.ts | 6 +++--- packages/core/src/js/replay/replayInterface.ts | 4 +--- packages/core/test/replay/browserReplay.test.ts | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/core/src/js/replay/browserReplay.ts b/packages/core/src/js/replay/browserReplay.ts index bac73a4704..7b3dede780 100644 --- a/packages/core/src/js/replay/browserReplay.ts +++ b/packages/core/src/js/replay/browserReplay.ts @@ -18,9 +18,7 @@ const INTEGRATION_NAME = 'Replay'; * * See the [Browser Replay documentation](https://docs.sentry.io/platforms/javascript/session-replay/) for more information. */ -const browserReplayIntegration = ( - options: ReplayConfiguration = {}, -): Replay => { +const browserReplayIntegration = (options: ReplayConfiguration = {}): Replay => { if (notWeb()) { // This is required because because `replayIntegration` browser check doesn't // work for React Native. @@ -37,7 +35,9 @@ 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(), diff --git a/packages/core/src/js/replay/replayInterface.ts b/packages/core/src/js/replay/replayInterface.ts index 58d60d2218..0308a5a385 100644 --- a/packages/core/src/js/replay/replayInterface.ts +++ b/packages/core/src/js/replay/replayInterface.ts @@ -38,9 +38,7 @@ export interface Replay extends Integration { * * Otherwise, queue up a flush. */ - flush(options?: { - continueRecording?: boolean; - }): Promise; + flush(options?: { continueRecording?: boolean }): Promise; /** * Get the current session ID. diff --git a/packages/core/test/replay/browserReplay.test.ts b/packages/core/test/replay/browserReplay.test.ts index 704fa44fcc..be12c27e21 100644 --- a/packages/core/test/replay/browserReplay.test.ts +++ b/packages/core/test/replay/browserReplay.test.ts @@ -1,4 +1,4 @@ -import { describe, test, } from '@jest/globals'; +import { describe, test } from '@jest/globals'; import * as SentryReact from '@sentry/react'; import { spyOn } from 'jest-mock'; From f2706b3947e08efa32934c2c55e8b9f4aab4ea57 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 23 May 2025 13:47:41 +0200 Subject: [PATCH 3/4] fix changelog --- CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c8f37862..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)) @@ -60,9 +66,6 @@ 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)) -- 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 From 060a6a9e13ecacd4c53294ea60846a81fd662af4 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 27 May 2025 14:03:34 +0200 Subject: [PATCH 4/4] Update packages/core/src/js/replay/browserReplay.ts Co-authored-by: LucasZF --- packages/core/src/js/replay/browserReplay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/js/replay/browserReplay.ts b/packages/core/src/js/replay/browserReplay.ts index 7b3dede780..a918b7e8f1 100644 --- a/packages/core/src/js/replay/browserReplay.ts +++ b/packages/core/src/js/replay/browserReplay.ts @@ -20,7 +20,7 @@ const INTEGRATION_NAME = 'Replay'; */ const browserReplayIntegration = (options: ReplayConfiguration = {}): Replay => { if (notWeb()) { - // This is required because because `replayIntegration` browser check doesn't + // This is required because `replayIntegration` browser check doesn't // work for React Native. return browserReplayIntegrationNoop(); }