diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e22a82970..2fe0a960f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes +- Don't crash the iOS app when Sentry is initialized with an invalid DSN ([#6825](https://github.com/getsentry/sentry-react-native/pull/6825)) - Populate the Hermes runtime version on JS profiles instead of sending an empty value ([#6817](https://github.com/getsentry/sentry-react-native/pull/6817)) ## 8.29.0 diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleInitInvalidDsnTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleInitInvalidDsnTest.kt new file mode 100644 index 0000000000..6f0eb110ec --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleInitInvalidDsnTest.kt @@ -0,0 +1,37 @@ +package io.sentry.react + +import androidx.test.core.app.ApplicationProvider +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.Promise +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RNSentryModuleInitInvalidDsnTest { + // Parity with the iOS regression test: an invalid DSN must be rejected at the + // bridge, not crash the host app. The Android SDK throws while parsing the DSN + // during init; initNativeSdk catches it and rejects the promise. + @Test + fun `initNativeSdk rejects on an invalid DSN instead of crashing`() { + val reactContext = Utils.makeReactContextMock() + whenever(reactContext.applicationContext).thenReturn(ApplicationProvider.getApplicationContext()) + val module = RNSentryModuleImpl(reactContext) + + val rnOptions = JavaOnlyMap.of("dsn", "\$SOME_ENV_VAR") + val promise = mock() + + // Must not throw out of the bridge (that would crash the app). + module.initNativeSdk(rnOptions, promise) + + verify(promise).reject(eq("SentryReactNative"), anyOrNull(), any()) + verify(promise, never()).resolve(anyOrNull()) + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 0c84b8d7ef..0bf522c286 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 00C7702ED388EA0682DBCC48 /* RNSentryStartInvalidOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C9B2501FF3EBD8B91BA6C1 /* RNSentryStartInvalidOptionsTests.swift */; }; + 2639D71D3BD04F17B0BAC987 /* RNSentryTurboModulePerfControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E795057A6D534A80A9D06356 /* RNSentryTurboModulePerfControllerTests.mm */; }; 332D33472CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332D33462CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift */; }; 3339C4812D6625570088EB3A /* RNSentryUserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3339C4802D6625570088EB3A /* RNSentryUserTests.m */; }; 3339C4832D6625570088EB3A /* RNSentryReplayControlTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3339C4822D6625570088EB3A /* RNSentryReplayControlTests.m */; }; @@ -19,7 +21,7 @@ 33DEDFF02D9185EB006066E4 /* RNSentryTimeToDisplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33DEDFEF2D9185E3006066E4 /* RNSentryTimeToDisplayTests.swift */; }; 33F58AD02977037D008F60EA /* RNSentryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.m */; }; 33F58AD12977037D008F60EB /* RNSentryScreenshotSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACE2977037D008F60EB /* RNSentryScreenshotSourceTests.swift */; }; - 2639D71D3BD04F17B0BAC987 /* RNSentryTurboModulePerfControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E795057A6D534A80A9D06356 /* RNSentryTurboModulePerfControllerTests.mm */; }; + AC4EE11A6E697E75C3753C16 /* RNSentrySDKStartInvalidOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 864F5BFD9CFDA4502ADAE72E /* RNSentrySDKStartInvalidOptionsTests.swift */; }; AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */; }; B4DEB41739F14AA38202D4D4 /* RNSentryUriValidationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3E3742693F7643C2ADE1BDF2 /* RNSentryUriValidationTests.m */; }; B5859A50A3E865EF5E61465A /* libPods-RNSentryCocoaTesterTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */; }; @@ -27,6 +29,7 @@ /* Begin PBXFileReference section */ 1482D5685A340AB93348A43D /* Pods-RNSentryCocoaTesterTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.release.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.release.xcconfig"; sourceTree = ""; }; + 26C9B2501FF3EBD8B91BA6C1 /* RNSentryStartInvalidOptionsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RNSentryStartInvalidOptionsTests.swift; sourceTree = ""; }; 330F308D2C0F385A002A0D4E /* RNSentryBreadcrumb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryBreadcrumb.h; path = ../ios/RNSentryBreadcrumb.h; sourceTree = ""; }; 332D33462CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryReplayOptionsTests.swift; sourceTree = ""; }; 332D33482CDBDC7300547D76 /* RNSentry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentry.h; path = ../ios/RNSentry.h; sourceTree = SOURCE_ROOT; }; @@ -52,12 +55,13 @@ 33DEDFEC2D8DC820006066E4 /* RNSentryOnDrawReporter+Test.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "RNSentryOnDrawReporter+Test.mm"; sourceTree = ""; }; 33DEDFEE2D8DD431006066E4 /* RNSentryTimeToDisplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryTimeToDisplay.h; path = ../ios/RNSentryTimeToDisplay.h; sourceTree = SOURCE_ROOT; }; 33DEDFEF2D9185E3006066E4 /* RNSentryTimeToDisplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryTimeToDisplayTests.swift; sourceTree = ""; }; - 33F58ACF2977037D008F60EA /* RNSentryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSentryTests.m; sourceTree = ""; }; 33F58ACE2977037D008F60EB /* RNSentryScreenshotSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryScreenshotSourceTests.swift; sourceTree = ""; }; + 33F58ACF2977037D008F60EA /* RNSentryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSentryTests.m; sourceTree = ""; }; 3E3742693F7643C2ADE1BDF2 /* RNSentryUriValidationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryUriValidationTests.m; sourceTree = ""; }; 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNSentryCocoaTesterTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - E795057A6D534A80A9D06356 /* RNSentryTurboModulePerfControllerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RNSentryTurboModulePerfControllerTests.mm; sourceTree = ""; }; + 864F5BFD9CFDA4502ADAE72E /* RNSentrySDKStartInvalidOptionsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RNSentrySDKStartInvalidOptionsTests.swift; sourceTree = ""; }; E2321E7CFA55AB617247098E /* Pods-RNSentryCocoaTesterTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.debug.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.debug.xcconfig"; sourceTree = ""; }; + E795057A6D534A80A9D06356 /* RNSentryTurboModulePerfControllerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RNSentryTurboModulePerfControllerTests.mm; sourceTree = ""; }; F48F26542EA2A481008A185E /* RNSentryEmitNewFrameEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryEmitNewFrameEvent.h; path = ../ios/RNSentryEmitNewFrameEvent.h; sourceTree = SOURCE_ROOT; }; F48F26552EA2A4D4008A185E /* RNSentryFramesTrackerListener.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryFramesTrackerListener.h; path = ../ios/RNSentryFramesTrackerListener.h; sourceTree = SOURCE_ROOT; }; FADF868E2EBD053E00D6652D /* SentrySDKWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySDKWrapper.h; path = ../ios/SentrySDKWrapper.h; sourceTree = SOURCE_ROOT; }; @@ -125,6 +129,8 @@ 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */, 332D33462CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift */, 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */, + 26C9B2501FF3EBD8B91BA6C1 /* RNSentryStartInvalidOptionsTests.swift */, + 864F5BFD9CFDA4502ADAE72E /* RNSentrySDKStartInvalidOptionsTests.swift */, ); path = RNSentryCocoaTesterTests; sourceTree = ""; @@ -281,6 +287,8 @@ 33DEDFF02D9185EB006066E4 /* RNSentryTimeToDisplayTests.swift in Sources */, 3380C6C42CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift in Sources */, 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */, + 00C7702ED388EA0682DBCC48 /* RNSentryStartInvalidOptionsTests.swift in Sources */, + AC4EE11A6E697E75C3753C16 /* RNSentrySDKStartInvalidOptionsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentrySDKStartInvalidOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentrySDKStartInvalidOptionsTests.swift new file mode 100644 index 0000000000..0162e69ba0 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentrySDKStartInvalidOptionsTests.swift @@ -0,0 +1,18 @@ +@_spi(Private) import Sentry +import XCTest + +final class RNSentrySDKStartInvalidOptionsTests: XCTestCase { + + // When neither the options file nor the fallback can produce options, + // the native auto-init path must not start the SDK with nil options. + func testStartWithUnparseableOptionsFileDoesNotStartSdk() { + // Arrange + SentrySDK.close() + + // Act — a non-existent options path forces the nil-options fallback + RNSentrySDK.start("/does/not/exist/sentry.options.json", configureOptions: nil) + + // Assert + XCTAssertFalse(SentrySDK.isEnabled, "the native SDK must not start when options cannot be created") + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartInvalidOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartInvalidOptionsTests.swift new file mode 100644 index 0000000000..3ad31e09a8 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartInvalidOptionsTests.swift @@ -0,0 +1,23 @@ +@_spi(Private) import Sentry +import XCTest + +final class RNSentryStartInvalidOptionsTests: XCTestCase { + + // An invalid DSN must surface a parse error, not start the native SDK with nil + // options (which crashes it). + func testStartWithInvalidDsnSurfacesErrorAndDoesNotStartSdk() { + // Arrange + SentrySDK.close() + + var error: NSError? + + // Act — mirrors an unexpanded build variable reaching the DSN + RNSentryStart.start(options: [ + "dsn": "$SOME_ENV_VAR" + ], error: &error) + + // Assert + XCTAssertNotNil(error, "an invalid DSN must surface a parse error instead of starting the SDK with nil options") + XCTAssertFalse(SentrySDK.isEnabled, "the native SDK must not start when option parsing fails") + } +} diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m index f475040577..7f1f7f62b5 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -67,6 +67,13 @@ + (void)start:(NSString *)path configureOptions:(void (^)(SentryOptions *options } } + if (options == nil) { + // Even the fallback failed. Skip starting instead of passing nil options to + // the native SDK, which would crash it. + NSLog(@"[RNSentry] Could not create options; skipping native SDK start."); + return; + } + [RNSentryStart updateWithReactDefaults:options]; if (configureOptions != nil) { configureOptions(options); diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 029a4558b4..b5493dcd5b 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -24,6 +24,11 @@ + (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions { SentryOptions *options = [self createOptionsWithDictionary:javascriptOptions error:errorPointer]; + if (options == nil) { + // Parsing failed (e.g. invalid DSN) and set `*errorPointer`. Return early so + // the caller can reject; starting with nil options crashes the native SDK. + return; + } [self updateWithReactDefaults:options]; [self updateWithReactFinals:options]; NSString *jsSdkVersion = nil;