diff --git a/CHANGELOG.md b/CHANGELOG.md index 745822869c..1f52f86acb 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..8c3de18599 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m @@ -776,6 +776,80 @@ - (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..f5fefc0cd6 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -94,6 +94,19 @@ - (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) {