-
-
Notifications
You must be signed in to change notification settings - Fork 373
Description
Description
We need a public API to add and remove view types from the subtree traversal in the SentryUIRedactBuilder:
sentry-cocoa/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift
Lines 552 to 590 in 4558de2
| private func isViewSubtreeIgnored(_ view: UIView) -> Bool { | |
| // We intentionally avoid using `NSClassFromString` or directly referencing class objects here, | |
| // because both approaches can trigger the Objective-C `+initialize` method on the class. | |
| // This has side effects and can cause crashes, especially when performed off the main thread | |
| // or with UIKit classes that expect to be initialized on the main thread. | |
| // | |
| // Instead, we use the string description of the type (i.e., `type(of: view).description()`) | |
| // for comparison. This is a safer, more "Swifty" approach that avoids the pitfalls of | |
| // class initialization side effects. | |
| // | |
| // We have previously encountered related issues: | |
| // - In EmergeTools' snapshotting code where using `NSClassFromString` led to crashes [1] | |
| // - In Sentry's own SubClassFinder where storing or accessing class objects on a background thread caused crashes due to `+initialize` being called on UIKit classes [2] | |
| // | |
| // [1] https://github.com/EmergeTools/SnapshotPreviews/blob/main/Sources/SnapshotPreviewsCore/View%2BSnapshot.swift#L248 | |
| // [2] https://github.com/getsentry/sentry-cocoa/blob/00d97404946a37e983eabb21cc64bd3d5d2cb474/Sources/Sentry/SentrySubClassFinder.m#L58-L84 | |
| let viewTypeId = type(of: view).description() | |
| if #available(iOS 26.0, *), viewTypeId == Self.cameraSwiftUIViewClassId.classId { | |
| // CameraUI.ChromeSwiftUIView is a special case because it contains layers which can not be iterated due to this error: | |
| // | |
| // Fatal error: Use of unimplemented initializer 'init(layer:)' for class 'CameraUI.ModeLoupeLayer' | |
| // | |
| // This crash only occurs when building with Xcode 16 for iOS 26, so we add a runtime check | |
| return true | |
| } | |
| #if os(iOS) | |
| // UISwitch uses UIImageView internally, which can be in the list of redacted views. | |
| // But UISwitch is in the list of ignored class identifiers by default, because it uses | |
| // non-sensitive images. Therefore we want to ignore the subtree of UISwitch, unless | |
| // it was removed from the list of ignored classes | |
| if viewTypeId == "UISwitch" && containsIgnoreClassId(ClassIdentifier(classId: viewTypeId)) { | |
| return true | |
| } | |
| #endif // os(iOS) | |
| return false | |
| } |
It will allow SDK users to extend the list of views, in case they are experiencing crashes caused by the view traversal, e.g. activating internal animations for CoreAnimation.
Example:
Application Specific Information:
-[NSConcreteValue doubleValue]: unrecognized selector sent to instance 0x1409e0cc0
Thread 0 Crashed:
0 CoreFoundation 0x3252a1994 __exceptionPreprocess
1 libobjc.A.dylib 0x31f169810 objc_exception_throw
2 CoreFoundation 0x32533a8fc -[NSObject(NSObject) doesNotRecognizeSelector:]
3 CoreFoundation 0x325221358 ___forwarding___
4 CoreFoundation 0x3252291fc __forwarding_prep_0___
5 QuartzCore 0x326843c68 -[NSNumber(CAAnimatableValue) CA_interpolateValue:byFraction:]
6 QuartzCore 0x32683f99c -[CABasicAnimation applyForTime:presentationObject:modelObject:]
7 QuartzCore 0x3265f98bc CA::Layer::presentation_layer
8 QuartzCore 0x3265f4008 CA::Layer::sublayers
9 MyApp 0x200a98d40 SentryUIRedactBuilder.mapRedactRegion
10 MyApp. 0x200a98ed4 [inlined] SentryUIRedactBuilder.mapRedactRegion
A reference for similar options is the list of masked and unmasked view classes in the session replay options:
sentry-cocoa/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift
Lines 145 to 163 in 4558de2
| /** | |
| * A list of custom UIView subclasses that need | |
| * to be masked during session replay. | |
| * By default Sentry already mask text and image elements from UIKit | |
| * Every child of a view that is redacted will also be redacted. | |
| * | |
| * - Note: See ``SentryReplayOptions.DefaultValues.maskedViewClasses`` for the default value. | |
| */ | |
| public var maskedViewClasses: [AnyClass] | |
| /** | |
| * A list of custom UIView subclasses to be ignored | |
| * during masking step of the session replay. | |
| * The views of given classes will not be redacted but their children may be. | |
| * This property has precedence over `redactViewTypes`. | |
| * | |
| * - Note: See ``SentryReplayOptions.DefaultValues.unmaskedViewClasses`` for the default value. | |
| */ | |
| public var unmaskedViewClasses: [AnyClass] |
We want to offer the SDK users to add additional view types, while also removing ours pre-defined ones (for flexibility), which is the CameraUI.ChromeSwiftUIView at this point in time only.
sentry-cocoa/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift
Lines 72 to 77 in 4558de2
| /// Class identifier for ``CameraUI.ChromeSwiftUIView``, if it exists. | |
| /// | |
| /// This object identifier is used to identify views of this class type during the redaction process. | |
| /// This workaround is specifically for Xcode 16 building for iOS 26 where accessing CameraUI.ModeLoupeLayer | |
| /// causes a crash due to unimplemented init(layer:) initializer. | |
| private static let cameraSwiftUIViewClassId = ClassIdentifier(classId: "CameraUI.ChromeSwiftUIView") |
Therefore, define the list should be a private field on the SentryReplayOptions.swift with two public helper methods includeSubtreeTraversalForViewType(String) and excludeSubtreeTraversalForViewType(String). The given string value will be compared to type(of: view).description().