Skip to content

Commit 4997892

Browse files
authored
feat: Expose iOS options to ignore views from subtree traversal (#5545)
* feat: Expose iOS options to ignore views from subtree traversal * Add example in the changelog * Fix tests
1 parent 7b02433 commit 4997892

File tree

4 files changed

+111
-1
lines changed

4 files changed

+111
-1
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@
1111
### Features
1212

1313
- Experimental support of UI profiling on Android ([#5518](https://github.com/getsentry/sentry-react-native/pull/5518))
14+
- Expose iOS options to ignore views from subtree traversal ([#5545](https://github.com/getsentry/sentry-react-native/pull/5545))
15+
- Use `includedViewClasses` to only traverse specific view classes, or `excludedViewClasses` to skip problematic view classes during session replay and screenshot capture
16+
```js
17+
import * as Sentry from '@sentry/react-native';
18+
19+
Sentry.init({
20+
replaysSessionSampleRate: 1.0,
21+
integrations: [
22+
Sentry.mobileReplayIntegration({
23+
includedViewClasses: ['UILabel', 'UIView', 'MyCustomView'],
24+
excludedViewClasses: ['WKWebView', 'UIWebView'],
25+
}),
26+
],
27+
});
28+
```
1429

1530
### Fixes
1631

packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ final class RNSentryReplayOptions: XCTestCase {
4848
}
4949

5050
func assertAllDefaultReplayOptionsAreNotNil(replayOptions: [String: Any]) {
51-
XCTAssertEqual(replayOptions.count, 9)
51+
XCTAssertEqual(replayOptions.count, 11)
5252
XCTAssertNotNil(replayOptions["sessionSampleRate"])
5353
XCTAssertNotNil(replayOptions["errorSampleRate"])
5454
XCTAssertNotNil(replayOptions["maskAllImages"])
@@ -58,6 +58,8 @@ final class RNSentryReplayOptions: XCTestCase {
5858
XCTAssertNotNil(replayOptions["enableViewRendererV2"])
5959
XCTAssertNotNil(replayOptions["enableFastViewRendering"])
6060
XCTAssertNotNil(replayOptions["quality"])
61+
XCTAssertNotNil(replayOptions["includedViewClasses"])
62+
XCTAssertNotNil(replayOptions["excludedViewClasses"])
6163
}
6264

6365
func testSessionSampleRate() {
@@ -318,4 +320,64 @@ final class RNSentryReplayOptions: XCTestCase {
318320

319321
XCTAssertEqual(actualOptions.sessionReplay.quality, SentryReplayOptions.SentryReplayQuality.medium)
320322
}
323+
324+
func testIncludedViewClasses() {
325+
let optionsDict = ([
326+
"dsn": "https://[email protected]/1234567",
327+
"replaysOnErrorSampleRate": 0.75,
328+
"mobileReplayOptions": [ "includedViewClasses": ["UILabel", "UIView", "UITextView"] ]
329+
] as NSDictionary).mutableCopy() as! NSMutableDictionary
330+
331+
RNSentryReplay.updateOptions(optionsDict)
332+
333+
let actualOptions = try! SentryOptionsInternal.initWithDict(optionsDict as! [String: Any])
334+
335+
let includedViewClasses = actualOptions.sessionReplay.includedViewClasses
336+
XCTAssertEqual(includedViewClasses.count, 3)
337+
XCTAssertTrue(includedViewClasses.contains("UILabel"))
338+
XCTAssertTrue(includedViewClasses.contains("UIView"))
339+
XCTAssertTrue(includedViewClasses.contains("UITextView"))
340+
}
341+
342+
func testExcludedViewClasses() {
343+
let optionsDict = ([
344+
"dsn": "https://[email protected]/1234567",
345+
"replaysOnErrorSampleRate": 0.75,
346+
"mobileReplayOptions": [ "excludedViewClasses": ["UICollectionView", "UITableView", "UIScrollView"] ]
347+
] as NSDictionary).mutableCopy() as! NSMutableDictionary
348+
349+
RNSentryReplay.updateOptions(optionsDict)
350+
351+
let actualOptions = try! SentryOptionsInternal.initWithDict(optionsDict as! [String: Any])
352+
353+
let excludedViewClasses = actualOptions.sessionReplay.excludedViewClasses
354+
XCTAssertEqual(excludedViewClasses.count, 3)
355+
XCTAssertTrue(excludedViewClasses.contains("UICollectionView"))
356+
XCTAssertTrue(excludedViewClasses.contains("UITableView"))
357+
XCTAssertTrue(excludedViewClasses.contains("UIScrollView"))
358+
}
359+
360+
func testIncludedAndExcludedViewClasses() {
361+
let optionsDict = ([
362+
"dsn": "https://[email protected]/1234567",
363+
"replaysOnErrorSampleRate": 0.75,
364+
"mobileReplayOptions": [
365+
"includedViewClasses": ["UILabel", "UIView"],
366+
"excludedViewClasses": ["UICollectionView"]
367+
]
368+
] as NSDictionary).mutableCopy() as! NSMutableDictionary
369+
370+
RNSentryReplay.updateOptions(optionsDict)
371+
372+
let actualOptions = try! SentryOptionsInternal.initWithDict(optionsDict as! [String: Any])
373+
374+
let includedViewClasses = actualOptions.sessionReplay.includedViewClasses
375+
XCTAssertEqual(includedViewClasses.count, 2)
376+
XCTAssertTrue(includedViewClasses.contains("UILabel"))
377+
XCTAssertTrue(includedViewClasses.contains("UIView"))
378+
379+
let excludedViewClasses = actualOptions.sessionReplay.excludedViewClasses
380+
XCTAssertEqual(excludedViewClasses.count, 1)
381+
XCTAssertTrue(excludedViewClasses.contains("UICollectionView"))
382+
}
321383
}

packages/core/ios/RNSentryReplay.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ + (BOOL)updateOptions:(NSMutableDictionary *)options
2727

2828
NSString *qualityString = options[@"replaysSessionQuality"];
2929

30+
NSArray *includedViewClasses = replayOptions[@"includedViewClasses"];
31+
NSArray *excludedViewClasses = replayOptions[@"excludedViewClasses"];
32+
3033
[options setValue:@{
3134
@"sessionSampleRate" : sessionSampleRate ?: [NSNull null],
3235
@"errorSampleRate" : errorSampleRate ?: [NSNull null],
@@ -36,6 +39,8 @@ + (BOOL)updateOptions:(NSMutableDictionary *)options
3639
@"enableViewRendererV2" : replayOptions[@"enableViewRendererV2"] ?: [NSNull null],
3740
@"enableFastViewRendering" : replayOptions[@"enableFastViewRendering"] ?: [NSNull null],
3841
@"maskedViewClasses" : [RNSentryReplay getReplayRNRedactClasses:replayOptions],
42+
@"includedViewClasses" : includedViewClasses ?: [NSNull null],
43+
@"excludedViewClasses" : excludedViewClasses ?: [NSNull null],
3944
@"sdkInfo" :
4045
@ { @"name" : REACT_NATIVE_SDK_NAME, @"version" : REACT_NATIVE_SDK_PACKAGE_VERSION }
4146
}

packages/core/src/js/replay/mobilereplay.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,34 @@ export interface MobileReplayOptions {
8181
*/
8282
enableFastViewRendering?: boolean;
8383

84+
/**
85+
* Array of view class names to include in subtree traversal during session replay and screenshot capture on iOS.
86+
*
87+
* Only views that are instances of these classes (or subclasses) will be traversed.
88+
* This helps prevent crashes when traversing problematic view hierarchies by allowing you to explicitly include only safe view classes.
89+
*
90+
* If both `includedViewClasses` and `excludedViewClasses` are set, `excludedViewClasses` takes precedence:
91+
* views matching excluded classes won't be traversed even if they match an included class.
92+
*
93+
* @default undefined
94+
* @platform ios
95+
*/
96+
includedViewClasses?: string[];
97+
98+
/**
99+
* Array of view class names to exclude from subtree traversal during session replay and screenshot capture on iOS.
100+
*
101+
* Views of these classes (or subclasses) will be skipped entirely, including all their children.
102+
* This helps prevent crashes when traversing problematic view hierarchies by allowing you to explicitly exclude problematic view classes.
103+
*
104+
* If both `includedViewClasses` and `excludedViewClasses` are set, `excludedViewClasses` takes precedence:
105+
* views matching excluded classes won't be traversed even if they match an included class.
106+
*
107+
* @default undefined
108+
* @platform ios
109+
*/
110+
excludedViewClasses?: string[];
111+
84112
/**
85113
* Sets the screenshot strategy used by the Session Replay integration on Android.
86114
*

0 commit comments

Comments
 (0)