Skip to content

Commit c73f7cc

Browse files
authored
fix(ios): Fix duplicate JS error reporting on iOS with New Architecture (#5532)
* fix(ios): Fix duplicate error reporting on iOS with New Architecture * Fix lint issues
1 parent c155be5 commit c73f7cc

File tree

3 files changed

+88
-0
lines changed

3 files changed

+88
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### Fixes
1212

13+
- Fix duplicate error reporting on iOS with New Architecture ([#5532](https://github.com/getsentry/sentry-react-native/pull/5532))
1314
- Fix for missing `replay_id` from metrics ([#5483](https://github.com/getsentry/sentry-react-native/pull/5483))
1415
- Skip span ID check when standalone mode is enabled ([#5493](https://github.com/getsentry/sentry-react-native/pull/5493))
1516

packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,80 @@ - (void)testIgnoreErrorsRegexAndStringBothWork
776776
XCTAssertNotNil(result3, @"Event with non-matching error should not be dropped");
777777
}
778778

779+
- (void)testBeforeSendFiltersOutUnhandledJSException
780+
{
781+
RNSentry *rnSentry = [[RNSentry alloc] init];
782+
NSError *error = nil;
783+
NSMutableDictionary *mockedOptions = [@{
784+
@"dsn" : @"https://abc@def.ingest.sentry.io/1234567",
785+
} mutableCopy];
786+
mockedOptions = [rnSentry prepareOptions:mockedOptions];
787+
SentryOptions *options = [SentrySDKWrapper createOptionsWithDictionary:mockedOptions
788+
isSessionReplayEnabled:NO
789+
error:&error];
790+
XCTAssertNotNil(options);
791+
XCTAssertNil(error);
792+
793+
SentryEvent *event = [[SentryEvent alloc] init];
794+
SentryException *exception = [SentryException alloc];
795+
exception.type = @"Unhandled JS Exception";
796+
exception.value = @"Error: Test error";
797+
event.exceptions = @[ exception ];
798+
SentryEvent *result = options.beforeSend(event);
799+
XCTAssertNil(result, @"Event with Unhandled JS Exception should be dropped");
800+
}
801+
802+
- (void)testBeforeSendFiltersOutJSErrorCppException
803+
{
804+
RNSentry *rnSentry = [[RNSentry alloc] init];
805+
NSError *error = nil;
806+
NSMutableDictionary *mockedOptions = [@{
807+
@"dsn" : @"https://abc@def.ingest.sentry.io/1234567",
808+
} mutableCopy];
809+
mockedOptions = [rnSentry prepareOptions:mockedOptions];
810+
SentryOptions *options = [SentrySDKWrapper createOptionsWithDictionary:mockedOptions
811+
isSessionReplayEnabled:NO
812+
error:&error];
813+
XCTAssertNotNil(options);
814+
XCTAssertNil(error);
815+
816+
// Test C++ exception with ExceptionsManager.reportException in value (actual format from New
817+
// Architecture) The exception type is "C++ Exception" and the value contains the mangled name
818+
// and error message
819+
SentryEvent *event1 = [[SentryEvent alloc] init];
820+
SentryException *exception1 = [SentryException alloc];
821+
exception1.type = @"C++ Exception";
822+
exception1.value = @"N8facebook3jsi7JSErrorE: ExceptionsManager.reportException raised an "
823+
@"exception: Unhandled JS Exception: Error: Test error";
824+
event1.exceptions = @[ exception1 ];
825+
SentryEvent *result1 = options.beforeSend(event1);
826+
XCTAssertNil(
827+
result1, @"Event with ExceptionsManager.reportException in value should be dropped");
828+
829+
// Test exception value containing ExceptionsManager.reportException (alternative format)
830+
SentryEvent *event2 = [[SentryEvent alloc] init];
831+
SentryException *exception2 = [SentryException alloc];
832+
exception2.type = @"SomeOtherException";
833+
exception2.value = @"ExceptionsManager.reportException raised an exception: Unhandled JS "
834+
@"Exception: Error: Test";
835+
event2.exceptions = @[ exception2 ];
836+
SentryEvent *result2 = options.beforeSend(event2);
837+
XCTAssertNil(
838+
result2, @"Event with ExceptionsManager.reportException in value should be dropped");
839+
840+
// Test that legitimate C++ exceptions without ExceptionsManager.reportException are not
841+
// filtered
842+
SentryEvent *event3 = [[SentryEvent alloc] init];
843+
SentryException *exception3 = [SentryException alloc];
844+
exception3.type = @"C++ Exception";
845+
exception3.value = @"std::runtime_error: Some other C++ error occurred";
846+
event3.exceptions = @[ exception3 ];
847+
SentryEvent *result3 = options.beforeSend(event3);
848+
XCTAssertNotNil(result3,
849+
@"Legitimate C++ exception without ExceptionsManager.reportException should not be "
850+
@"dropped");
851+
}
852+
779853
- (void)testCreateOptionsWithDictionaryEnableSessionReplayInUnreliableEnvironmentDefault
780854
{
781855
NSError *error = nil;

packages/core/ios/RNSentry.mm

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ - (NSMutableDictionary *)prepareOptions:(NSDictionary *)options
9494
return nil;
9595
}
9696

97+
// With New Architecture, React Native wraps JS errors in C++ exceptions.
98+
// These exceptions are caught by the native crash handler and should be filtered out
99+
// since the JS error is already reported by the JS error handler.
100+
// The key indicator is "ExceptionsManager.reportException" in the exception value,
101+
// which is React Native's mechanism for reporting JS errors to the native layer.
102+
for (SentryException *exception in event.exceptions) {
103+
if (nil != exception.value &&
104+
[exception.value rangeOfString:@"ExceptionsManager.reportException"].location
105+
!= NSNotFound) {
106+
return nil;
107+
}
108+
}
109+
97110
// Regex and Str are set when one of them has value so we only need to check one of them.
98111
if (self->_ignoreErrorPatternsStr || self->_ignoreErrorPatternsRegex) {
99112
for (SentryException *exception in event.exceptions) {

0 commit comments

Comments
 (0)