From 33d77cd3327e3449e307f7f81c292385eb3dcc97 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 25 Jun 2026 10:23:11 +0200 Subject: [PATCH 1/2] fix(replay): Guard against non-dictionary touch breadcrumb path elements Fixes a fatal NSInvalidArgumentException (-[__NSCFString objectForKey:]: unrecognized selector) in RNSentryReplayBreadcrumbConverter when a touch breadcrumb path contains a non-dictionary element. - getTouchPathMessageFrom: now validates each element with isKindOfClass:[NSDictionary class] before calling objectForKey:. - convertTouch: now guards that `path` is an NSArray, matching convertMultiClick:. Closes #6342 Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + ...SentryReplayBreadcrumbConverterTests.swift | 47 +++++++++++++++++++ .../ios/RNSentryReplayBreadcrumbConverter.m | 17 ++++--- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0a5c5e5cb..a63be30746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes +- Fix fatal `NSInvalidArgumentException` crash in `RNSentryReplayBreadcrumbConverter` when a touch breadcrumb path contains a non-dictionary element ([#6342](https://github.com/getsentry/sentry-react-native/issues/6342)) - Apply `SENTRY_ENVIRONMENT`, `SENTRY_RELEASE` and `SENTRY_DIST` build-time overrides to the JS bundled options to match the native SDKs ([#6330](https://github.com/getsentry/sentry-react-native/pull/6330)) - Fix user `geo` being dropped from the native scope by forwarding it as a structured object instead of a JSON string ([#6309](https://github.com/getsentry/sentry-react-native/pull/6309)) - Remove unused `React/RCTTextView.h` import that broke iOS builds on React Native 0.87, where the header was removed as part of the legacy architecture cleanup ([#6322](https://github.com/getsentry/sentry-react-native/pull/6322)) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift index 50cf46cb1e..880ab48fdc 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift @@ -206,6 +206,53 @@ final class RNSentryReplayBreadcrumbConverterTests: XCTestCase { XCTAssertEqual(actual, nil) } + // Reproduces https://github.com/getsentry/sentry-react-native/issues/6342 + // A non-dictionary element in the path (e.g. an NSString) must not crash with + // `-[__NSCFString objectForKey:]: unrecognized selector sent to instance`. + func testTouchMessageReturnsNilOnNonDictionaryPathElement() throws { + let testPath: [Any?] = ["not-a-dictionary"] + let actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(from: testPath as [Any]) + XCTAssertEqual(actual, nil) + } + + func testTouchMessageReturnsNilWhenAnyPathElementIsNotADictionary() throws { + let testPath: [Any?] = [ + ["name": "name1"], + "not-a-dictionary", + ["name": "name3"] + ] + let actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(from: testPath as [Any]) + XCTAssertEqual(actual, nil) + } + + func testConvertTouchBreadcrumbWithNonDictionaryPathElementDoesNotCrash() { + let converter = RNSentryReplayBreadcrumbConverter() + let testBreadcrumb = Breadcrumb() + testBreadcrumb.timestamp = Date() + testBreadcrumb.level = .info + testBreadcrumb.type = "user" + testBreadcrumb.category = "touch" + testBreadcrumb.data = [ + "path": ["not-a-dictionary"] + ] + // Must not raise NSInvalidArgumentException. + _ = converter.convert(from: testBreadcrumb) + } + + func testConvertTouchBreadcrumbWithNonArrayPathDoesNotCrash() { + let converter = RNSentryReplayBreadcrumbConverter() + let testBreadcrumb = Breadcrumb() + testBreadcrumb.timestamp = Date() + testBreadcrumb.level = .info + testBreadcrumb.type = "user" + testBreadcrumb.category = "touch" + testBreadcrumb.data = [ + "path": "not-an-array" + ] + // Must not raise (NSString does not respond to `count`/`objectAtIndex:`). + _ = converter.convert(from: testBreadcrumb) + } + func testTouchMessageReturnsMessageOnValidPathExample1() throws { let testPath: [Any?] = [ ["label": "label0"], diff --git a/packages/core/ios/RNSentryReplayBreadcrumbConverter.m b/packages/core/ios/RNSentryReplayBreadcrumbConverter.m index d6d04b171d..1d47aec4d8 100644 --- a/packages/core/ios/RNSentryReplayBreadcrumbConverter.m +++ b/packages/core/ios/RNSentryReplayBreadcrumbConverter.m @@ -69,8 +69,12 @@ - (instancetype _Nonnull)init return nil; } - NSMutableArray *path = [breadcrumb.data valueForKey:@"path"]; - NSString *message = [RNSentryReplayBreadcrumbConverter getTouchPathMessageFrom:path]; + id maybePath = [breadcrumb.data valueForKey:@"path"]; + if (![maybePath isKindOfClass:[NSArray class]]) { + return nil; + } + + NSString *message = [RNSentryReplayBreadcrumbConverter getTouchPathMessageFrom:maybePath]; return [SentrySessionReplayHybridSDK createBreadcrumbwithTimestamp:breadcrumb.timestamp category:@"ui.tap" @@ -112,10 +116,11 @@ + (NSString *_Nullable)getTouchPathMessageFrom:(NSArray *_Nullable)path NSMutableString *message = [[NSMutableString alloc] init]; for (NSInteger i = MIN(3, pathCount - 1); i >= 0; i--) { - NSDictionary *item = [path objectAtIndex:i]; - if (item == nil) { - return nil; // There should be no nil (undefined) from JS, but to be safe we check it - // here + id item = [path objectAtIndex:i]; + if (![item isKindOfClass:[NSDictionary class]]) { + return nil; // There should be no nil (undefined) or non-dictionary entry from JS, but + // to be safe we check it here. See + // https://github.com/getsentry/sentry-react-native/issues/6342 } id name = [item objectForKey:@"name"]; From bcc2e2528b03961dffa21054313a993d80252357 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 25 Jun 2026 10:25:07 +0200 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a63be30746..a730393ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Fixes -- Fix fatal `NSInvalidArgumentException` crash in `RNSentryReplayBreadcrumbConverter` when a touch breadcrumb path contains a non-dictionary element ([#6342](https://github.com/getsentry/sentry-react-native/issues/6342)) +- Fix fatal `NSInvalidArgumentException` crash in `RNSentryReplayBreadcrumbConverter` when a touch breadcrumb path contains a non-dictionary element ([#6346](https://github.com/getsentry/sentry-react-native/pull/6346)) - Apply `SENTRY_ENVIRONMENT`, `SENTRY_RELEASE` and `SENTRY_DIST` build-time overrides to the JS bundled options to match the native SDKs ([#6330](https://github.com/getsentry/sentry-react-native/pull/6330)) - Fix user `geo` being dropped from the native scope by forwarding it as a structured object instead of a JSON string ([#6309](https://github.com/getsentry/sentry-react-native/pull/6309)) - Remove unused `React/RCTTextView.h` import that broke iOS builds on React Native 0.87, where the header was removed as part of the legacy architecture cleanup ([#6322](https://github.com/getsentry/sentry-react-native/pull/6322))