From 77924c6a6252b3deba8e23f1042d031e26aed5e5 Mon Sep 17 00:00:00 2001 From: Oleg Shparber Date: Tue, 27 Oct 2026 21:27:41 -0400 Subject: [PATCH] fix(ios): Start native SDK without options file RNSentrySDK fell back to parsing an empty dictionary when sentry.options.json was missing or could not be parsed. Cocoa reads a missing "dsn" key as an empty DSN and rejects it, so the fallback always failed and configureOptions never ran. Before 8.30.0 this crashed at launch; since then the native SDK silently never starts. Fall back to default SentryOptions instead, and skip the start only when neither the file nor configureOptions set a valid DSN, so that a later init from JS can still start the native SDK. Register RNSentryStartTests, RNSentryStartFromFileTests and their fixtures in the Cocoa tester project again. They were lost when #5582 landed and have not run since. Update them for Cocoa 9 and add regression tests for a DSN set in configureOptions. --- CHANGELOG.md | 2 + .../project.pbxproj | 39 +++++++++++++++++++ .../RNSentrySDKStartInvalidOptionsTests.swift | 6 +-- .../RNSentryStartFromFileTests.swift | 33 ++++++++++++---- .../RNSentryStartTests.swift | 1 - packages/core/ios/RNSentrySDK.m | 22 +++++------ 6 files changed, 78 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc5208173a..e838431b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ - Persist fatal JS error envelopes to disk on Android before the process exits ([#6937](https://github.com/getsentry/sentry-react-native/pull/6937)) - The hard-crash envelope was only queued on the transport thread, so the event was lost when React Native terminated the process right after. It is now written to the offline cache on the calling thread and sent on the next launch, matching iOS. - Support Expo Router 58 in `expoRouterIntegration`. Metro no longer warns about the removed `expo-router/build/global-state/router-store` module, and the integration creates navigation spans again ([#6923](https://github.com/getsentry/sentry-react-native/pull/6923)) +- Start the native iOS SDK from `RNSentrySDK` when `sentry.options.json` is missing or can't be parsed and the DSN is set in `configureOptions` ([#6956](https://github.com/getsentry/sentry-react-native/pull/6956)) + - The fallback options always failed to parse, so `configureOptions` never ran. Before 8.30.0 the app crashed at launch; since then the native SDK silently never started. ### Internal diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 0bf522c286..f18ca43a40 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -14,6 +14,11 @@ 3339C4832D6625570088EB3A /* RNSentryReplayControlTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3339C4822D6625570088EB3A /* RNSentryReplayControlTests.m */; }; 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */; }; 3380C6C42CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */; }; + 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */; }; + 339C6C422D3FD3AE00CA72ED /* RNSentryStartFromFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */; }; + 339C6C482D3FD9A700CA72ED /* invalid.options.json in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C462D3FD91900CA72ED /* invalid.options.json */; }; + 339C6C492D3FD9A700CA72ED /* invalid.options.txt in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C452D3FD90200CA72ED /* invalid.options.txt */; }; + 339C6C4B2D3FD9B200CA72ED /* valid.options.json in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */; }; 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; }; 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; }; 33DEDFEA2D8DBE67006066E4 /* RNSentryOnDrawReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33DEDFE92D8DBE5B006066E4 /* RNSentryOnDrawReporterTests.swift */; }; @@ -47,6 +52,11 @@ 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryReplayPostInitTests.swift; sourceTree = ""; }; 338739072A7D7D2800950DDD /* RNSentryReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryReplay.h; path = ../ios/RNSentryReplay.h; sourceTree = ""; }; 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryOnDrawReporter.h; path = ../ios/RNSentryOnDrawReporter.h; sourceTree = ""; }; + 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartTests.swift; sourceTree = ""; }; + 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartFromFileTests.swift; sourceTree = ""; }; + 339C6C452D3FD90200CA72ED /* invalid.options.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = invalid.options.txt; sourceTree = ""; }; + 339C6C462D3FD91900CA72ED /* invalid.options.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = invalid.options.json; sourceTree = ""; }; + 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = valid.options.json; sourceTree = ""; }; 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryFramesTrackerListenerTests.m; sourceTree = ""; }; 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryDependencyContainerTests.m; sourceTree = ""; }; 33AFE0132B8F31AF00AAB120 /* RNSentryDependencyContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryDependencyContainer.h; path = ../ios/RNSentryDependencyContainer.h; sourceTree = ""; }; @@ -91,6 +101,7 @@ 3360896929524163007C7730 = { isa = PBXGroup; children = ( + 339C6C432D3FD41C00CA72ED /* TestAssets */, 33AFE0122B8F319000AAB120 /* RNSentry */, 3360899029524164007C7730 /* RNSentryCocoaTesterTests */, 3360897329524163007C7730 /* Products */, @@ -131,6 +142,8 @@ 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */, 26C9B2501FF3EBD8B91BA6C1 /* RNSentryStartInvalidOptionsTests.swift */, 864F5BFD9CFDA4502ADAE72E /* RNSentrySDKStartInvalidOptionsTests.swift */, + 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */, + 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */, ); path = RNSentryCocoaTesterTests; sourceTree = ""; @@ -144,6 +157,16 @@ path = Replay; sourceTree = ""; }; + 339C6C432D3FD41C00CA72ED /* TestAssets */ = { + isa = PBXGroup; + children = ( + 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */, + 339C6C462D3FD91900CA72ED /* invalid.options.json */, + 339C6C452D3FD90200CA72ED /* invalid.options.txt */, + ); + path = TestAssets; + sourceTree = ""; + }; 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( @@ -181,6 +204,7 @@ 3360898929524164007C7730 /* Sources */, BB7D14838753E6599863899B /* Frameworks */, CC7959F3721CB3AD7CB6A047 /* [CP] Copy Pods Resources */, + 339C6C472D3FD99900CA72ED /* Resources */, ); buildRules = ( ); @@ -225,6 +249,19 @@ }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + 339C6C472D3FD99900CA72ED /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 339C6C482D3FD9A700CA72ED /* invalid.options.json in Resources */, + 339C6C4B2D3FD9B200CA72ED /* valid.options.json in Resources */, + 339C6C492D3FD9A700CA72ED /* invalid.options.txt in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXShellScriptBuildPhase section */ 30F19D4E16BEEFEC68733838 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -289,6 +326,8 @@ 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */, 00C7702ED388EA0682DBCC48 /* RNSentryStartInvalidOptionsTests.swift in Sources */, AC4EE11A6E697E75C3753C16 /* RNSentrySDKStartInvalidOptionsTests.swift in Sources */, + 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */, + 339C6C422D3FD3AE00CA72ED /* RNSentryStartFromFileTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentrySDKStartInvalidOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentrySDKStartInvalidOptionsTests.swift index 0162e69ba0..4a2cc6d588 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentrySDKStartInvalidOptionsTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentrySDKStartInvalidOptionsTests.swift @@ -3,13 +3,13 @@ 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. + // When neither the options file nor configureOptions provides a DSN, + // the native auto-init path must not start the SDK. func testStartWithUnparseableOptionsFileDoesNotStartSdk() { // Arrange SentrySDK.close() - // Act — a non-existent options path forces the nil-options fallback + // Act — a non-existent options path and no configureOptions leave no DSN RNSentrySDK.start("/does/not/exist/sentry.options.json", configureOptions: nil) // Assert diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift index 38107d3f66..fb1d0b9de6 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift @@ -3,6 +3,11 @@ import XCTest final class RNSentryStartFromFileTests: XCTestCase { + override func setUp() { + super.setUp() + SentrySDK.close() + } + func testNoThrowOnMissingOptionsFile() { var wasConfigurationCalled = false @@ -11,10 +16,16 @@ final class RNSentryStartFromFileTests: XCTestCase { }) XCTAssertTrue(wasConfigurationCalled) + XCTAssertFalse(SentrySDK.isEnabled) + } - let actualOptions = SentrySDK.internal.options - XCTAssertNil(actualOptions.dsn) - XCTAssertNil(actualOptions.parsedDsn) + func testStartsWithConfigureDsnOnMissingOptionsFile() { + RNSentrySDK.start(getNonExistingOptionsPath(), configureOptions: { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + }) + + XCTAssertTrue(SentrySDK.isEnabled) + XCTAssertEqual(SentrySDK.internal.options.dsn, "https://abcd@efgh.ingest.sentry.io/123456") } func testNoThrowOnInvalidFileType() { @@ -25,10 +36,16 @@ final class RNSentryStartFromFileTests: XCTestCase { }) XCTAssertTrue(wasConfigurationCalled) + XCTAssertFalse(SentrySDK.isEnabled) + } - let actualOptions = SentrySDK.internal.options - XCTAssertNil(actualOptions.dsn) - XCTAssertNil(actualOptions.parsedDsn) + func testStartsWithConfigureDsnOnInvalidFileType() { + RNSentrySDK.start(getInvalidOptionsTypePath(), configureOptions: { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + }) + + XCTAssertTrue(SentrySDK.isEnabled) + XCTAssertEqual(SentrySDK.internal.options.dsn, "https://abcd@efgh.ingest.sentry.io/123456") } func testNoThrowOnInvalidOptions() { @@ -41,7 +58,7 @@ final class RNSentryStartFromFileTests: XCTestCase { XCTAssertTrue(wasConfigurationCalled) let actualOptions = SentrySDK.internal.options - XCTAssertNil(actualOptions.dsn) + XCTAssertEqual(actualOptions.dsn, "https://abcd@efgh.ingest.sentry.io/123456") XCTAssertNotNil(actualOptions.parsedDsn) XCTAssertEqual(actualOptions.environment, "environment-from-invalid-file") } @@ -56,7 +73,7 @@ final class RNSentryStartFromFileTests: XCTestCase { XCTAssertTrue(wasConfigurationCalled) let actualOptions = SentrySDK.internal.options - XCTAssertNil(actualOptions.dsn) + XCTAssertEqual(actualOptions.dsn, "https://abcd@efgh.ingest.sentry.io/123456") XCTAssertNotNil(actualOptions.parsedDsn) XCTAssertEqual(actualOptions.environment, "environment-from-valid-file") } diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift index 431bc62123..1e875e74fc 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift @@ -11,7 +11,6 @@ final class RNSentryStartTests: XCTestCase { XCTAssertFalse(actualOptions!.enableCaptureFailedRequests) XCTAssertNil(actualOptions!.tracesSampleRate) XCTAssertNil(actualOptions!.tracesSampler) - XCTAssertFalse(actualOptions!.enableTracing) } func testStartSetsReactDeafults() { diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m index 7f1f7f62b5..7982394248 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -59,19 +59,7 @@ + (void)start:(NSString *)path configureOptions:(void (^)(SentryOptions *options if (options == nil) { // Fallback in case that options file could not be parsed. - NSError *fallbackError = nil; - options = [RNSentryInternal optionsFromDictionary:@{ } error:&fallbackError]; - if (fallbackError != nil) { - NSLog(@"[RNSentry] Failed to create fallback options with error: %@", - fallbackError.localizedDescription); - } - } - - 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; + options = [[SentryOptions alloc] init]; } [RNSentryStart updateWithReactDefaults:options]; @@ -79,6 +67,14 @@ + (void)start:(NSString *)path configureOptions:(void (^)(SentryOptions *options configureOptions(options); } [RNSentryStart updateWithReactFinals:options]; + + if (options.parsedDsn == nil) { + // Don't start without a DSN, so that a later init from JS can still start the native SDK. + NSLog(@"[RNSentry] No valid DSN in the options file or configureOptions; skipping native " + @"SDK start."); + return; + } + [RNSentryStart startWithOptions:options]; }