From 078562c4d77dd94305913d255000f1ec888928a0 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 14 Jan 2026 16:28:24 +0100 Subject: [PATCH 1/2] fix(ios): Fix duplicate error reporting on iOS with New Architecture --- CHANGELOG.md | 1 + .../RNSentryCocoaTesterTests/RNSentryTests.m | 66 +++++++++++++++++++ packages/core/ios/RNSentry.mm | 12 ++++ 3 files changed, 79 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef271ff7e6..f36d8241fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes +- Fix duplicate error reporting on iOS with New Architecture ([#5532](https://github.com/getsentry/sentry-react-native/pull/5532)) - Fix for missing `replay_id` from metrics ([#5483](https://github.com/getsentry/sentry-react-native/pull/5483)) - Skip span ID check when standalone mode is enabled ([#5493](https://github.com/getsentry/sentry-react-native/pull/5493)) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m index d4cd8e957d..7f0da718bd 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m @@ -776,6 +776,72 @@ - (void)testIgnoreErrorsRegexAndStringBothWork XCTAssertNotNil(result3, @"Event with non-matching error should not be dropped"); } +- (void)testBeforeSendFiltersOutUnhandledJSException +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + NSMutableDictionary *mockedOptions = [@{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + } mutableCopy]; + mockedOptions = [rnSentry prepareOptions:mockedOptions]; + SentryOptions *options = [SentrySDKWrapper createOptionsWithDictionary:mockedOptions + isSessionReplayEnabled:NO + error:&error]; + XCTAssertNotNil(options); + XCTAssertNil(error); + + SentryEvent *event = [[SentryEvent alloc] init]; + SentryException *exception = [SentryException alloc]; + exception.type = @"Unhandled JS Exception"; + exception.value = @"Error: Test error"; + event.exceptions = @[ exception ]; + SentryEvent *result = options.beforeSend(event); + XCTAssertNil(result, @"Event with Unhandled JS Exception should be dropped"); +} + +- (void)testBeforeSendFiltersOutJSErrorCppException +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + NSMutableDictionary *mockedOptions = [@{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + } mutableCopy]; + mockedOptions = [rnSentry prepareOptions:mockedOptions]; + SentryOptions *options = [SentrySDKWrapper createOptionsWithDictionary:mockedOptions + isSessionReplayEnabled:NO + error:&error]; + XCTAssertNotNil(options); + XCTAssertNil(error); + + // Test C++ exception with ExceptionsManager.reportException in value (actual format from New Architecture) + // The exception type is "C++ Exception" and the value contains the mangled name and error message + SentryEvent *event1 = [[SentryEvent alloc] init]; + SentryException *exception1 = [SentryException alloc]; + exception1.type = @"C++ Exception"; + exception1.value = @"N8facebook3jsi7JSErrorE: ExceptionsManager.reportException raised an exception: Unhandled JS Exception: Error: Test error"; + event1.exceptions = @[ exception1 ]; + SentryEvent *result1 = options.beforeSend(event1); + XCTAssertNil(result1, @"Event with ExceptionsManager.reportException in value should be dropped"); + + // Test exception value containing ExceptionsManager.reportException (alternative format) + SentryEvent *event2 = [[SentryEvent alloc] init]; + SentryException *exception2 = [SentryException alloc]; + exception2.type = @"SomeOtherException"; + exception2.value = @"ExceptionsManager.reportException raised an exception: Unhandled JS Exception: Error: Test"; + event2.exceptions = @[ exception2 ]; + SentryEvent *result2 = options.beforeSend(event2); + XCTAssertNil(result2, @"Event with ExceptionsManager.reportException in value should be dropped"); + + // Test that legitimate C++ exceptions without ExceptionsManager.reportException are not filtered + SentryEvent *event3 = [[SentryEvent alloc] init]; + SentryException *exception3 = [SentryException alloc]; + exception3.type = @"C++ Exception"; + exception3.value = @"std::runtime_error: Some other C++ error occurred"; + event3.exceptions = @[ exception3 ]; + SentryEvent *result3 = options.beforeSend(event3); + XCTAssertNotNil(result3, @"Legitimate C++ exception without ExceptionsManager.reportException should not be dropped"); +} + - (void)testCreateOptionsWithDictionaryEnableSessionReplayInUnreliableEnvironmentDefault { NSError *error = nil; diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 10586ab910..a3154bf89b 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -94,6 +94,18 @@ - (NSMutableDictionary *)prepareOptions:(NSDictionary *)options return nil; } + // With New Architecture, React Native wraps JS errors in C++ exceptions. + // These exceptions are caught by the native crash handler and should be filtered out + // since the JS error is already reported by the JS error handler. + // The key indicator is "ExceptionsManager.reportException" in the exception value, + // which is React Native's mechanism for reporting JS errors to the native layer. + for (SentryException *exception in event.exceptions) { + if (nil != exception.value && + [exception.value rangeOfString:@"ExceptionsManager.reportException"].location != NSNotFound) { + return nil; + } + } + // Regex and Str are set when one of them has value so we only need to check one of them. if (self->_ignoreErrorPatternsStr || self->_ignoreErrorPatternsRegex) { for (SentryException *exception in event.exceptions) { From e05b1fdb88550e51f2377e781fe12c17ab72e2b8 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 14 Jan 2026 16:42:03 +0100 Subject: [PATCH 2/2] Fix lint issues --- .../RNSentryCocoaTesterTests/RNSentryTests.m | 32 ++++++++++++------- packages/core/ios/RNSentry.mm | 3 +- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m index 7f0da718bd..8c3de18599 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m @@ -789,7 +789,7 @@ - (void)testBeforeSendFiltersOutUnhandledJSException error:&error]; XCTAssertNotNil(options); XCTAssertNil(error); - + SentryEvent *event = [[SentryEvent alloc] init]; SentryException *exception = [SentryException alloc]; exception.type = @"Unhandled JS Exception"; @@ -812,34 +812,42 @@ - (void)testBeforeSendFiltersOutJSErrorCppException error:&error]; XCTAssertNotNil(options); XCTAssertNil(error); - - // Test C++ exception with ExceptionsManager.reportException in value (actual format from New Architecture) - // The exception type is "C++ Exception" and the value contains the mangled name and error message + + // Test C++ exception with ExceptionsManager.reportException in value (actual format from New + // Architecture) The exception type is "C++ Exception" and the value contains the mangled name + // and error message SentryEvent *event1 = [[SentryEvent alloc] init]; SentryException *exception1 = [SentryException alloc]; exception1.type = @"C++ Exception"; - exception1.value = @"N8facebook3jsi7JSErrorE: ExceptionsManager.reportException raised an exception: Unhandled JS Exception: Error: Test error"; + exception1.value = @"N8facebook3jsi7JSErrorE: ExceptionsManager.reportException raised an " + @"exception: Unhandled JS Exception: Error: Test error"; event1.exceptions = @[ exception1 ]; SentryEvent *result1 = options.beforeSend(event1); - XCTAssertNil(result1, @"Event with ExceptionsManager.reportException in value should be dropped"); - + XCTAssertNil( + result1, @"Event with ExceptionsManager.reportException in value should be dropped"); + // Test exception value containing ExceptionsManager.reportException (alternative format) SentryEvent *event2 = [[SentryEvent alloc] init]; SentryException *exception2 = [SentryException alloc]; exception2.type = @"SomeOtherException"; - exception2.value = @"ExceptionsManager.reportException raised an exception: Unhandled JS Exception: Error: Test"; + exception2.value = @"ExceptionsManager.reportException raised an exception: Unhandled JS " + @"Exception: Error: Test"; event2.exceptions = @[ exception2 ]; SentryEvent *result2 = options.beforeSend(event2); - XCTAssertNil(result2, @"Event with ExceptionsManager.reportException in value should be dropped"); - - // Test that legitimate C++ exceptions without ExceptionsManager.reportException are not filtered + XCTAssertNil( + result2, @"Event with ExceptionsManager.reportException in value should be dropped"); + + // Test that legitimate C++ exceptions without ExceptionsManager.reportException are not + // filtered SentryEvent *event3 = [[SentryEvent alloc] init]; SentryException *exception3 = [SentryException alloc]; exception3.type = @"C++ Exception"; exception3.value = @"std::runtime_error: Some other C++ error occurred"; event3.exceptions = @[ exception3 ]; SentryEvent *result3 = options.beforeSend(event3); - XCTAssertNotNil(result3, @"Legitimate C++ exception without ExceptionsManager.reportException should not be dropped"); + XCTAssertNotNil(result3, + @"Legitimate C++ exception without ExceptionsManager.reportException should not be " + @"dropped"); } - (void)testCreateOptionsWithDictionaryEnableSessionReplayInUnreliableEnvironmentDefault diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index a3154bf89b..f5fefc0cd6 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -101,7 +101,8 @@ - (NSMutableDictionary *)prepareOptions:(NSDictionary *)options // which is React Native's mechanism for reporting JS errors to the native layer. for (SentryException *exception in event.exceptions) { if (nil != exception.value && - [exception.value rangeOfString:@"ExceptionsManager.reportException"].location != NSNotFound) { + [exception.value rangeOfString:@"ExceptionsManager.reportException"].location + != NSNotFound) { return nil; } }