From c5500c0e7dda0172997f95bc73f01c9ee6dde6d4 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Tue, 6 Oct 2026 10:55:23 +0200 Subject: [PATCH 1/2] fix(core): Strip callback options before the native SDK starts A JS function crosses the bridge as a native callback that only accepts an array. sentry-cocoa reads `beforeSendLog` from the options dictionary and calls it with a `SentryLog`, which crashes the app. Remove every function value from the options before `initNativeSdk`, instead of a fixed list of names. This also covers `beforeSendSpan` and any callback the JavaScript SDK adds later. Fixes #6842 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 3 ++ packages/core/src/js/wrapper.ts | 33 +++++++++++------- packages/core/test/wrapper.test.ts | 56 ++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bf57c9e2c..6e022346e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ - Populate the Hermes runtime version on JS profiles instead of sending an empty value ([#6817](https://github.com/getsentry/sentry-react-native/pull/6817)) - Mark `@sentry/react-native` as side-effect free so bundlers can tree-shake unused exports ([#6829](https://github.com/getsentry/sentry-react-native/pull/6829)) - Resolve `@sentry/react-native` from the Android project directory in the Expo plugin `build.gradle` line, instead of the directory where Gradle started ([#6840](https://github.com/getsentry/sentry-react-native/pull/6840)) +- If `beforeSendLog` or another callback option is set, the app no longer crashes on iOS when the native SDK emits a log ([#PR_NUMBER](https://github.com/getsentry/sentry-react-native/pull/PR_NUMBER)) + + Callback options are for the JavaScript layer only. The SDK now removes all of them before it starts the native SDK. ### Dependencies diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index f9c9ba2a45..8957e3d320 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -170,6 +170,22 @@ interface SentryNativeWrapper { const EOL = encodeUTF8('\n'); +/** + * A JS function crosses the bridge as a native callback that only accepts an array. + * The native SDK calls hooks such as `beforeSend` or `beforeSendLog` with a native + * object instead, which crashes the app. + */ +function withoutFunctionValues(options: T): T { + const source = options as Record; + const result: Record = {}; + for (const key of Object.keys(source)) { + if (typeof source[key] !== 'function') { + result[key] = source[key]; + } + } + return result as T; +} + /** * Our internal interface for calling native functions */ @@ -314,21 +330,12 @@ export const NATIVE: SentryNativeWrapper = { // filter out all the options that would crash native. /* oxlint-disable typescript-eslint(no-unused-vars) */ - const { - beforeSend, - beforeBreadcrumb, - beforeSendTransaction, - beforeSendMetric, - integrations, - ignoreErrors, - logsOrigin, - profilingOptions, - androidProfilingOptions, - onNativeLog, - ...filteredOptions - } = options; + const { integrations, ignoreErrors, logsOrigin, profilingOptions, androidProfilingOptions, ...remainingOptions } = + options; /* oxlint-enable typescript-eslint(no-unused-vars) */ + const filteredOptions = withoutFunctionValues(remainingOptions); + // Move profilingOptions into _experiments // Support deprecated androidProfilingOptions for backwards compatibility const resolvedProfilingOptions = profilingOptions ?? androidProfilingOptions; diff --git a/packages/core/test/wrapper.test.ts b/packages/core/test/wrapper.test.ts index b29d98da35..b9e21fa5a9 100644 --- a/packages/core/test/wrapper.test.ts +++ b/packages/core/test/wrapper.test.ts @@ -398,6 +398,62 @@ describe('Tests Native Wrapper', () => { expect(NATIVE.enableNative).toBe(true); }); + test('filter beforeSendLog when initializing Native SDK', async () => { + await NATIVE.initNativeSdk({ + dsn: VALID_DSN, + enableNative: true, + autoInitializeNativeSdk: true, + beforeSendLog: jest.fn(), + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + + expect(RNSentry.initNativeSdk).toHaveBeenCalled(); + // @ts-expect-error mock value + const initParameter = RNSentry.initNativeSdk.mock.calls[0][0]; + expect(initParameter).not.toHaveProperty('beforeSendLog'); + expect(NATIVE.enableNative).toBe(true); + }); + + test('filter beforeSendSpan when initializing Native SDK', async () => { + await NATIVE.initNativeSdk({ + dsn: VALID_DSN, + enableNative: true, + autoInitializeNativeSdk: true, + beforeSendSpan: jest.fn(), + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + + expect(RNSentry.initNativeSdk).toHaveBeenCalled(); + // @ts-expect-error mock value + const initParameter = RNSentry.initNativeSdk.mock.calls[0][0]; + expect(initParameter).not.toHaveProperty('beforeSendSpan'); + expect(NATIVE.enableNative).toBe(true); + }); + + test('filter every function option when initializing Native SDK', async () => { + await NATIVE.initNativeSdk({ + dsn: VALID_DSN, + enableNative: true, + autoInitializeNativeSdk: true, + onReady: jest.fn(), + onNativeLog: jest.fn(), + tracesSampler: jest.fn(), + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + + expect(RNSentry.initNativeSdk).toHaveBeenCalled(); + // @ts-expect-error mock value + const initParameter = RNSentry.initNativeSdk.mock.calls[0][0]; + expect(Object.values(initParameter).every(value => typeof value !== 'function')).toBe(true); + expect(NATIVE.enableNative).toBe(true); + }); + test('passes sdkVersion to native SDK', async () => { await NATIVE.initNativeSdk({ dsn: VALID_DSN, From 469b756964e31b54e4c16bfcdc11635b81089438 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Tue, 6 Oct 2026 10:56:26 +0200 Subject: [PATCH 2/2] chore: Link the CHANGELOG entry to the PR Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e022346e4..564bdbe63b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ - Populate the Hermes runtime version on JS profiles instead of sending an empty value ([#6817](https://github.com/getsentry/sentry-react-native/pull/6817)) - Mark `@sentry/react-native` as side-effect free so bundlers can tree-shake unused exports ([#6829](https://github.com/getsentry/sentry-react-native/pull/6829)) - Resolve `@sentry/react-native` from the Android project directory in the Expo plugin `build.gradle` line, instead of the directory where Gradle started ([#6840](https://github.com/getsentry/sentry-react-native/pull/6840)) -- If `beforeSendLog` or another callback option is set, the app no longer crashes on iOS when the native SDK emits a log ([#PR_NUMBER](https://github.com/getsentry/sentry-react-native/pull/PR_NUMBER)) +- If `beforeSendLog` or another callback option is set, the app no longer crashes on iOS when the native SDK emits a log ([#6847](https://github.com/getsentry/sentry-react-native/pull/6847)) Callback options are for the JavaScript layer only. The SDK now removes all of them before it starts the native SDK.