From c288fdb77a503c5aa7307496cbaf7b36f2e109e6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 16 Jan 2026 10:35:41 +0100 Subject: [PATCH 1/3] feat: Expose iOS options to ignore views from subtree traversal --- CHANGELOG.md | 1 + .../RNSentryReplayOptionsTests.swift | 64 ++++++++++++++++++- packages/core/ios/RNSentryReplay.mm | 5 ++ packages/core/src/js/replay/mobilereplay.ts | 28 ++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10482337e5..a303fb9a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Features - Experimental support of UI profiling on Android ([#5518](https://github.com/getsentry/sentry-react-native/pull/5518)) +- Expose iOS options to ignore views from subtree traversal ([#5545](https://github.com/getsentry/sentry-react-native/pull/5545)) ### Fixes diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift index 0d7ef3aa12..fe4a7ed746 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift @@ -48,7 +48,7 @@ final class RNSentryReplayOptions: XCTestCase { } func assertAllDefaultReplayOptionsAreNotNil(replayOptions: [String: Any]) { - XCTAssertEqual(replayOptions.count, 9) + XCTAssertEqual(replayOptions.count, 11) XCTAssertNotNil(replayOptions["sessionSampleRate"]) XCTAssertNotNil(replayOptions["errorSampleRate"]) XCTAssertNotNil(replayOptions["maskAllImages"]) @@ -58,6 +58,8 @@ final class RNSentryReplayOptions: XCTestCase { XCTAssertNotNil(replayOptions["enableViewRendererV2"]) XCTAssertNotNil(replayOptions["enableFastViewRendering"]) XCTAssertNotNil(replayOptions["quality"]) + XCTAssertNotNil(replayOptions["includedViewClasses"]) + XCTAssertNotNil(replayOptions["excludedViewClasses"]) } func testSessionSampleRate() { @@ -318,4 +320,64 @@ final class RNSentryReplayOptions: XCTestCase { XCTAssertEqual(actualOptions.sessionReplay.quality, SentryReplayOptions.SentryReplayQuality.medium) } + + func testIncludedViewClasses() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "replaysOnErrorSampleRate": 0.75, + "mobileReplayOptions": [ "includedViewClasses": ["UILabel", "UIView", "UITextView"] ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + + RNSentryReplay.updateOptions(optionsDict) + + let actualOptions = try! SentryOptionsInternal.initWithDict(optionsDict as! [String: Any]) + + let includedViewClasses = actualOptions.sessionReplay.includedViewClasses + XCTAssertEqual(includedViewClasses.count, 3) + assertContainsClass(classArray: includedViewClasses, stringClass: "UILabel") + assertContainsClass(classArray: includedViewClasses, stringClass: "UIView") + assertContainsClass(classArray: includedViewClasses, stringClass: "UITextView") + } + + func testExcludedViewClasses() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "replaysOnErrorSampleRate": 0.75, + "mobileReplayOptions": [ "excludedViewClasses": ["UICollectionView", "UITableView", "UIScrollView"] ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + + RNSentryReplay.updateOptions(optionsDict) + + let actualOptions = try! SentryOptionsInternal.initWithDict(optionsDict as! [String: Any]) + + let excludedViewClasses = actualOptions.sessionReplay.excludedViewClasses + XCTAssertEqual(excludedViewClasses.count, 3) + assertContainsClass(classArray: excludedViewClasses, stringClass: "UICollectionView") + assertContainsClass(classArray: excludedViewClasses, stringClass: "UITableView") + assertContainsClass(classArray: excludedViewClasses, stringClass: "UIScrollView") + } + + func testIncludedAndExcludedViewClasses() { + let optionsDict = ([ + "dsn": "https://abc@def.ingest.sentry.io/1234567", + "replaysOnErrorSampleRate": 0.75, + "mobileReplayOptions": [ + "includedViewClasses": ["UILabel", "UIView"], + "excludedViewClasses": ["UICollectionView"] + ] + ] as NSDictionary).mutableCopy() as! NSMutableDictionary + + RNSentryReplay.updateOptions(optionsDict) + + let actualOptions = try! SentryOptionsInternal.initWithDict(optionsDict as! [String: Any]) + + let includedViewClasses = actualOptions.sessionReplay.includedViewClasses + XCTAssertEqual(includedViewClasses.count, 2) + assertContainsClass(classArray: includedViewClasses, stringClass: "UILabel") + assertContainsClass(classArray: includedViewClasses, stringClass: "UIView") + + let excludedViewClasses = actualOptions.sessionReplay.excludedViewClasses + XCTAssertEqual(excludedViewClasses.count, 1) + assertContainsClass(classArray: excludedViewClasses, stringClass: "UICollectionView") + } } diff --git a/packages/core/ios/RNSentryReplay.mm b/packages/core/ios/RNSentryReplay.mm index 94fa30b4e4..40575a9e4c 100644 --- a/packages/core/ios/RNSentryReplay.mm +++ b/packages/core/ios/RNSentryReplay.mm @@ -27,6 +27,9 @@ + (BOOL)updateOptions:(NSMutableDictionary *)options NSString *qualityString = options[@"replaysSessionQuality"]; + NSArray *includedViewClasses = replayOptions[@"includedViewClasses"]; + NSArray *excludedViewClasses = replayOptions[@"excludedViewClasses"]; + [options setValue:@{ @"sessionSampleRate" : sessionSampleRate ?: [NSNull null], @"errorSampleRate" : errorSampleRate ?: [NSNull null], @@ -36,6 +39,8 @@ + (BOOL)updateOptions:(NSMutableDictionary *)options @"enableViewRendererV2" : replayOptions[@"enableViewRendererV2"] ?: [NSNull null], @"enableFastViewRendering" : replayOptions[@"enableFastViewRendering"] ?: [NSNull null], @"maskedViewClasses" : [RNSentryReplay getReplayRNRedactClasses:replayOptions], + @"includedViewClasses" : includedViewClasses ?: [NSNull null], + @"excludedViewClasses" : excludedViewClasses ?: [NSNull null], @"sdkInfo" : @ { @"name" : REACT_NATIVE_SDK_NAME, @"version" : REACT_NATIVE_SDK_PACKAGE_VERSION } } diff --git a/packages/core/src/js/replay/mobilereplay.ts b/packages/core/src/js/replay/mobilereplay.ts index 437df76d3c..d0ae691966 100644 --- a/packages/core/src/js/replay/mobilereplay.ts +++ b/packages/core/src/js/replay/mobilereplay.ts @@ -81,6 +81,34 @@ export interface MobileReplayOptions { */ enableFastViewRendering?: boolean; + /** + * Array of view class names to include in subtree traversal during session replay and screenshot capture on iOS. + * + * Only views that are instances of these classes (or subclasses) will be traversed. + * This helps prevent crashes when traversing problematic view hierarchies by allowing you to explicitly include only safe view classes. + * + * If both `includedViewClasses` and `excludedViewClasses` are set, `excludedViewClasses` takes precedence: + * views matching excluded classes won't be traversed even if they match an included class. + * + * @default undefined + * @platform ios + */ + includedViewClasses?: string[]; + + /** + * Array of view class names to exclude from subtree traversal during session replay and screenshot capture on iOS. + * + * Views of these classes (or subclasses) will be skipped entirely, including all their children. + * This helps prevent crashes when traversing problematic view hierarchies by allowing you to explicitly exclude problematic view classes. + * + * If both `includedViewClasses` and `excludedViewClasses` are set, `excludedViewClasses` takes precedence: + * views matching excluded classes won't be traversed even if they match an included class. + * + * @default undefined + * @platform ios + */ + excludedViewClasses?: string[]; + /** * Sets the screenshot strategy used by the Session Replay integration on Android. * From ecd472544da19f908b7b133bd74c8fabee1ffe67 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 16 Jan 2026 10:38:31 +0100 Subject: [PATCH 2/3] Add example in the changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a303fb9a08..ce6b5812ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,20 @@ - Experimental support of UI profiling on Android ([#5518](https://github.com/getsentry/sentry-react-native/pull/5518)) - Expose iOS options to ignore views from subtree traversal ([#5545](https://github.com/getsentry/sentry-react-native/pull/5545)) + - Use `includedViewClasses` to only traverse specific view classes, or `excludedViewClasses` to skip problematic view classes during session replay and screenshot capture + ```js + import * as Sentry from '@sentry/react-native'; + + Sentry.init({ + replaysSessionSampleRate: 1.0, + integrations: [ + Sentry.mobileReplayIntegration({ + includedViewClasses: ['UILabel', 'UIView', 'MyCustomView'], + excludedViewClasses: ['WKWebView', 'UIWebView'], + }), + ], + }); + ``` ### Fixes From 62178a559526e035a7406846c1b944ad340f9164 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 16 Jan 2026 11:05:27 +0100 Subject: [PATCH 3/3] Fix tests --- .../RNSentryReplayOptionsTests.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift index fe4a7ed746..8709976231 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift @@ -334,9 +334,9 @@ final class RNSentryReplayOptions: XCTestCase { let includedViewClasses = actualOptions.sessionReplay.includedViewClasses XCTAssertEqual(includedViewClasses.count, 3) - assertContainsClass(classArray: includedViewClasses, stringClass: "UILabel") - assertContainsClass(classArray: includedViewClasses, stringClass: "UIView") - assertContainsClass(classArray: includedViewClasses, stringClass: "UITextView") + XCTAssertTrue(includedViewClasses.contains("UILabel")) + XCTAssertTrue(includedViewClasses.contains("UIView")) + XCTAssertTrue(includedViewClasses.contains("UITextView")) } func testExcludedViewClasses() { @@ -352,9 +352,9 @@ final class RNSentryReplayOptions: XCTestCase { let excludedViewClasses = actualOptions.sessionReplay.excludedViewClasses XCTAssertEqual(excludedViewClasses.count, 3) - assertContainsClass(classArray: excludedViewClasses, stringClass: "UICollectionView") - assertContainsClass(classArray: excludedViewClasses, stringClass: "UITableView") - assertContainsClass(classArray: excludedViewClasses, stringClass: "UIScrollView") + XCTAssertTrue(excludedViewClasses.contains("UICollectionView")) + XCTAssertTrue(excludedViewClasses.contains("UITableView")) + XCTAssertTrue(excludedViewClasses.contains("UIScrollView")) } func testIncludedAndExcludedViewClasses() { @@ -373,11 +373,11 @@ final class RNSentryReplayOptions: XCTestCase { let includedViewClasses = actualOptions.sessionReplay.includedViewClasses XCTAssertEqual(includedViewClasses.count, 2) - assertContainsClass(classArray: includedViewClasses, stringClass: "UILabel") - assertContainsClass(classArray: includedViewClasses, stringClass: "UIView") + XCTAssertTrue(includedViewClasses.contains("UILabel")) + XCTAssertTrue(includedViewClasses.contains("UIView")) let excludedViewClasses = actualOptions.sessionReplay.excludedViewClasses XCTAssertEqual(excludedViewClasses.count, 1) - assertContainsClass(classArray: excludedViewClasses, stringClass: "UICollectionView") + XCTAssertTrue(excludedViewClasses.contains("UICollectionView")) } }