Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Sources/Swift/Core/Protocol/SentryRedactOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public protocol SentryRedactOptions {
var maskAllImages: Bool { get }
var maskedViewClasses: [AnyClass] { get }
var unmaskedViewClasses: [AnyClass] { get }
var subtreeTraversalIgnoredViewTypes: Set<String> { get }
}

@objcMembers
Expand All @@ -14,4 +15,20 @@ public protocol SentryRedactOptions {
public var maskAllImages: Bool = true
public var maskedViewClasses: [AnyClass] = []
public var unmaskedViewClasses: [AnyClass] = []

/// Default view types for which subtree traversal should be ignored.
///
/// By default, includes `CameraUI.ChromeSwiftUIView` on iOS 26+ to avoid crashes
/// when accessing `CameraUI.ModeLoupeLayer`.
public var subtreeTraversalIgnoredViewTypes: Set<String> {
var defaults: Set<String> = []
// 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'
#if os(iOS)
if #available(iOS 26.0, *) {
defaults.insert("CameraUI.ChromeSwiftUIView")
}
#endif // os(iOS)
return defaults
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ final class SentryUIRedactBuilder {
/// 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")
static let cameraSwiftUIViewClassId = ClassIdentifier(classId: "CameraUI.ChromeSwiftUIView")

// MARK: - Properties

Expand Down Expand Up @@ -106,6 +106,12 @@ final class SentryUIRedactBuilder {
/// Optimized lookup: class IDs with layer constraints (includes both classId and layerId)
private var constrainedRedactClasses: Set<ClassIdentifier> = []

/// A set of view type identifiers (as strings) for which subtree traversal should be ignored.
///
/// Views matching these types will have their subtrees skipped during redaction to avoid crashes
/// caused by traversing problematic view hierarchies.
private var subtreeTraversalIgnoredViewTypes: Set<String>

/// Initializes a new instance of the redaction process with the specified options.
///
/// This initializer populates allow/deny lists for view types using `ExtendedClassIdentifier`,
Expand Down Expand Up @@ -208,6 +214,7 @@ final class SentryUIRedactBuilder {
}

redactClassesIdentifiers = redactClasses
subtreeTraversalIgnoredViewTypes = options.subtreeTraversalIgnoredViewTypes

// didSet doesn't run during initialization, so we need to manually build the optimization structures
rebuildOptimizedLookups()
Expand Down Expand Up @@ -567,12 +574,8 @@ final class SentryUIRedactBuilder {
// [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
// Check if this view type is in the configurable list of ignored subtree traversal types
if subtreeTraversalIgnoredViewTypes.contains(viewTypeId) {
return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions {
public static let maskAllImages: Bool = true
public static let maskedViewClasses: [AnyClass] = []
public static let unmaskedViewClasses: [AnyClass] = []
public static let subtreeTraversalIgnoredViewTypes: Set<String> = []
}

// MARK: - Rendering
Expand Down Expand Up @@ -94,6 +95,19 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions {
*/
public var unmaskedViewClasses: [AnyClass]

/**
* A set of view type identifiers (as strings) for which subtree traversal should be ignored.
*
* Views matching these types will have their subtrees skipped during redaction to avoid crashes
* caused by traversing problematic view hierarchies (e.g., views that activate internal CoreAnimation
* animations when their layers are accessed).
*
* The string values should match the result of `type(of: view).description()`.
*
* - Note: See ``SentryViewScreenshotOptions.init`` for the default value.
*/
public var subtreeTraversalIgnoredViewTypes: Set<String>

/**
* Initialize screenshot options disabled
*
Expand All @@ -109,7 +123,8 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions {
maskAllText: DefaultValues.maskAllText,
maskAllImages: DefaultValues.maskAllImages,
maskedViewClasses: DefaultValues.maskedViewClasses,
unmaskedViewClasses: DefaultValues.unmaskedViewClasses
unmaskedViewClasses: DefaultValues.unmaskedViewClasses,
subtreeTraversalIgnoredViewTypes: DefaultValues.subtreeTraversalIgnoredViewTypes
)
}

Expand All @@ -133,7 +148,8 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions {
}) ?? DefaultValues.maskedViewClasses,
unmaskedViewClasses: (dictionary["unmaskedViewClasses"] as? NSArray)?.compactMap({ element in
NSClassFromString((element as? String) ?? "")
}) ?? DefaultValues.unmaskedViewClasses
}) ?? DefaultValues.unmaskedViewClasses,
subtreeTraversalIgnoredViewTypes: (dictionary["subtreeTraversalIgnoredViewTypes"] as? [String]).map { Set($0) } ?? DefaultValues.subtreeTraversalIgnoredViewTypes
)
}

Expand All @@ -147,6 +163,7 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions {
* - maskAllImages: Flag to redact all images in the app by drawing a rectangle over it.
* - maskedViewClasses: A list of custom UIView subclasses that need to be masked during the screenshot.
* - unmaskedViewClasses: A list of custom UIView subclasses to be ignored during masking step of the screenshot.
* - subtreeTraversalIgnoredViewTypes: A set of view type identifiers for which subtree traversal should be ignored.
*
* - Note: See ``SentryViewScreenshotOptions.DefaultValues`` for the default values of each parameter.
*/
Expand All @@ -156,7 +173,8 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions {
maskAllText: Bool = DefaultValues.maskAllText,
maskAllImages: Bool = DefaultValues.maskAllImages,
maskedViewClasses: [AnyClass] = DefaultValues.maskedViewClasses,
unmaskedViewClasses: [AnyClass] = DefaultValues.unmaskedViewClasses
unmaskedViewClasses: [AnyClass] = DefaultValues.unmaskedViewClasses,
subtreeTraversalIgnoredViewTypes: Set<String> = DefaultValues.subtreeTraversalIgnoredViewTypes
) {
// - This initializer is publicly available for Swift, but not for Objective-C, because automatically bridged Swift initializers
// with default values result in a single initializer requiring all parameters.
Expand All @@ -170,6 +188,7 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions {
self.maskAllImages = maskAllImages
self.maskedViewClasses = maskedViewClasses
self.unmaskedViewClasses = unmaskedViewClasses
self.subtreeTraversalIgnoredViewTypes = subtreeTraversalIgnoredViewTypes

super.init()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@
public static let maskedViewClasses: [AnyClass] = []
public static let unmaskedViewClasses: [AnyClass] = []

/// Default view types for which subtree traversal should be ignored.
///
/// By default, includes:
/// - `CameraUI.ChromeSwiftUIView` on iOS 26+ to avoid crashes.
fileprivate static var subtreeTraversalIgnoredViewTypes: Set<String> {
var defaults: Set<String> = []
// 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'
if #available(iOS 26.0, *) {
defaults.insert(SentryUIRedactBuilder.cameraSwiftUIViewClassId.classId)

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Check no UIKit linkage (DebugWithoutUIKit)

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Sample macOS-Swift Debug

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Lint

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Unit macOS 14 Sentry

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Sample visionOS-Swift Debug

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Build with SPM

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Check no UIKit linkage (ReleaseWithoutUIKit)

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Unit macOS 15 Sentry

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -Dynamic, sentry-dynamic) / xros

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -Dynamic, sentry-dynamic) / xros

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, staticlib, sentry-static) / watchos

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, staticlib, sentry-static) / watchos

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -WithoutUIKitOrAppKit, WithoutUIKit, sentry-withoutui... / iphonesimulator

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Build XCFramework Slices (Sentry, mh_dylib, -WithoutUIKitOrAppKit, WithoutUIKit, sentry-withoutui... / iphonesimulator

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Sample watchOS-Swift WatchKit App Debug

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Sample visionOS-Swift DebugV9

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Unit with Test Server macOS 15

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Sample macOS-SwiftUI Debug

cannot find 'SentryUIRedactBuilder' in scope

Check failure on line 34 in Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

View workflow job for this annotation

GitHub Actions / Sample macOS-Swift DebugV9

cannot find 'SentryUIRedactBuilder' in scope
}
return defaults
}

// The following properties are defaults which are not configurable by the user.

fileprivate static let sdkInfo: [String: Any]? = nil
Expand Down Expand Up @@ -162,6 +176,49 @@
*/
public var unmaskedViewClasses: [AnyClass]

/**
* A set of view type identifiers (as strings) for which subtree traversal should be ignored.
*
* Views matching these types will have their subtrees skipped during redaction to avoid crashes
* caused by traversing problematic view hierarchies (e.g., views that activate internal CoreAnimation
* animations when their layers are accessed).
*
* The string values should match the result of `type(of: view).description()`.
*
* - Note: You must use the methods ``includeSubtreeTraversalForViewType(_:)`` and ``excludeSubtreeTraversalForViewType(_:)``
* to add and remove view types, so do not accidentally remove our defaults.
* - Note: By default, this includes `CameraUI.ChromeSwiftUIView` on iOS 26+ to avoid crashes
* when accessing `CameraUI.ModeLoupeLayer`.
*/
public private(set) var subtreeTraversalIgnoredViewTypes: Set<String>

/**
* Adds a view type to the list of views for which subtree traversal should be ignored.
*
* - Parameter viewType: The view type identifier as a string (e.g., "MyCustomView").
* This should match the result of `type(of: view).description()`.
*
* Use this method to prevent crashes when traversing problematic view hierarchies.
* For example, if you encounter crashes when certain views are traversed, you can add
* their type identifier to skip their subtrees.
*/
public func includeSubtreeTraversalForViewType(_ viewType: String) {
subtreeTraversalIgnoredViewTypes.insert(viewType)
}

/**
* Removes a view type from the list of views for which subtree traversal should be ignored.
*
* - Parameter viewType: The view type identifier as a string (e.g., "CameraUI.ChromeSwiftUIView").
* This should match the result of `type(of: view).description()`.
*
* Use this method to remove default or previously added view types from the ignore list,
* allowing their subtrees to be traversed normally.
*/
public func excludeSubtreeTraversalForViewType(_ viewType: String) {
subtreeTraversalIgnoredViewTypes.remove(viewType)
}

/**
* Alias for ``enableViewRendererV2``.
*
Expand Down Expand Up @@ -410,6 +467,7 @@
self.errorReplayDuration = errorReplayDuration ?? DefaultValues.errorReplayDuration
self.sessionSegmentDuration = sessionSegmentDuration ?? DefaultValues.sessionSegmentDuration
self.maximumDuration = maximumDuration ?? DefaultValues.maximumDuration
self.subtreeTraversalIgnoredViewTypes = DefaultValues.subtreeTraversalIgnoredViewTypes

super.init()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -850,4 +850,100 @@ class SentryReplayOptionsTests: XCTestCase {
// -- Assert --
XCTAssertNil(options.sdkInfo)
}

// MARK: subtreeTraversalIgnoredViewTypes

func testSubtreeTraversalIgnoredViewTypes_defaultInitialization_shouldIncludeCameraUIViewOniOS26() {
// -- Act --
let options = SentryReplayOptions()

// -- Assert --
if #available(iOS 26.0, *) {
XCTAssertTrue(options.subtreeTraversalIgnoredViewTypes.contains("CameraUI.ChromeSwiftUIView"))
} else {
XCTAssertFalse(options.subtreeTraversalIgnoredViewTypes.contains("CameraUI.ChromeSwiftUIView"))
}
}

func testSubtreeTraversalIgnoredViewTypes_includeSubtreeTraversalForViewType_shouldAddViewType() {
// -- Arrange --
let options = SentryReplayOptions()

// -- Act --
options.includeSubtreeTraversalForViewType("MyCustomView")

// -- Assert --
XCTAssertTrue(options.subtreeTraversalIgnoredViewTypes.contains("MyCustomView"))
}

func testSubtreeTraversalIgnoredViewTypes_includeSubtreeTraversalForViewType_multipleCalls_shouldAddAllViewTypes() {
// -- Arrange --
let options = SentryReplayOptions()

// -- Act --
options.includeSubtreeTraversalForViewType("MyCustomView1")
options.includeSubtreeTraversalForViewType("MyCustomView2")
options.includeSubtreeTraversalForViewType("MyCustomView3")

// -- Assert --
XCTAssertTrue(options.subtreeTraversalIgnoredViewTypes.contains("MyCustomView1"))
XCTAssertTrue(options.subtreeTraversalIgnoredViewTypes.contains("MyCustomView2"))
XCTAssertTrue(options.subtreeTraversalIgnoredViewTypes.contains("MyCustomView3"))
XCTAssertEqual(options.subtreeTraversalIgnoredViewTypes.count, 3 + (options.subtreeTraversalIgnoredViewTypes.contains("CameraUI.ChromeSwiftUIView") ? 1 : 0))
}

func testSubtreeTraversalIgnoredViewTypes_excludeSubtreeTraversalForViewType_shouldRemoveViewType() {
// -- Arrange --
let options = SentryReplayOptions()
options.includeSubtreeTraversalForViewType("MyCustomView")
XCTAssertTrue(options.subtreeTraversalIgnoredViewTypes.contains("MyCustomView"))

// -- Act --
options.excludeSubtreeTraversalForViewType("MyCustomView")

// -- Assert --
XCTAssertFalse(options.subtreeTraversalIgnoredViewTypes.contains("MyCustomView"))
}

func testSubtreeTraversalIgnoredViewTypes_excludeSubtreeTraversalForViewType_shouldRemoveDefaultCameraUIView() {
// -- Arrange --
let options = SentryReplayOptions()

// Only test on iOS 26+ where CameraUI.ChromeSwiftUIView is included by default
guard #available(iOS 26.0, *) else {
return
}

XCTAssertTrue(options.subtreeTraversalIgnoredViewTypes.contains("CameraUI.ChromeSwiftUIView"))

// -- Act --
options.excludeSubtreeTraversalForViewType("CameraUI.ChromeSwiftUIView")

// -- Assert --
XCTAssertFalse(options.subtreeTraversalIgnoredViewTypes.contains("CameraUI.ChromeSwiftUIView"))
}

func testSubtreeTraversalIgnoredViewTypes_excludeSubtreeTraversalForViewType_nonExistentViewType_shouldNotCrash() {
// -- Arrange --
let options = SentryReplayOptions()
let initialCount = options.subtreeTraversalIgnoredViewTypes.count

// -- Act --
options.excludeSubtreeTraversalForViewType("NonExistentView")

// -- Assert --
XCTAssertEqual(options.subtreeTraversalIgnoredViewTypes.count, initialCount)
}

func testSubtreeTraversalIgnoredViewTypes_propertyGetter_shouldReturnCurrentValue() {
// -- Arrange --
let options = SentryReplayOptions()
options.includeSubtreeTraversalForViewType("TestView")

// -- Act --
let retrievedSet = options.subtreeTraversalIgnoredViewTypes

// -- Assert --
XCTAssertTrue(retrievedSet.contains("TestView"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,16 @@ class SentryRedactDefaultOptionsTests: XCTestCase {
XCTAssertTrue(options.maskAllImages)
XCTAssertEqual(options.maskedViewClasses.count, 0)
XCTAssertEqual(options.unmaskedViewClasses.count, 0)
// On iOS 26+, CameraUI.ChromeSwiftUIView should be in the ignored set
#if os(iOS)
if #available(iOS 26.0, *) {
XCTAssertEqual(options.subtreeTraversalIgnoredViewTypes.count, 1)
XCTAssertTrue(options.subtreeTraversalIgnoredViewTypes.contains("CameraUI.ChromeSwiftUIView"), "CameraUI.ChromeSwiftUIView should be ignored by default on iOS 26+")
} else {
XCTAssertEqual(options.subtreeTraversalIgnoredViewTypes.count, 0)
}
#else
XCTAssertEqual(options.subtreeTraversalIgnoredViewTypes.count, 0)
#endif
}
}
Loading
Loading