diff --git a/CHANGELOG.md b/CHANGELOG.md index b9063eaf704..b399d503f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Add options `options.sessionReplay.includeSubtreeTraversalForViewType` and `options.sessionReplay.excludeSubtreeTraversalForViewType` to ignore views from subtree traversal (#7063) + ## 8.57.3 ### Fixes diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 77ac1da40e0..5d0779c72c3 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -698,8 +698,6 @@ 84EB21942BF01C6C00EDDA28 /* TestNSNotificationCenterWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B18DE4328D9F8F6004845C6 /* TestNSNotificationCenterWrapper.swift */; }; 84EB21962BF01CEA00EDDA28 /* SentryCrashInstallationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EB21952BF01CEA00EDDA28 /* SentryCrashInstallationTests.swift */; }; 84F2A1CE2E06001300A94524 /* (null) in Sources */ = {isa = PBXBuildFile; }; - 84F994E62A6894B500EC0190 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84F994E52A6894B500EC0190 /* CoreData.framework */; }; - 84F994E82A6894BD00EC0190 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84F994E72A6894BD00EC0190 /* SystemConfiguration.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); }; 861265F92404EC1500C4AFDE /* SentryArray.h in Headers */ = {isa = PBXBuildFile; fileRef = 861265F72404EC1500C4AFDE /* SentryArray.h */; }; 861265FA2404EC1500C4AFDE /* SentryArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 861265F82404EC1500C4AFDE /* SentryArray.m */; }; 8E0551E026A7A63C00400526 /* TestProtocolClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E0551DF26A7A63C00400526 /* TestProtocolClient.swift */; }; @@ -1015,6 +1013,8 @@ F41362152E1C568400B84443 /* SentryScopePersistentStore+Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41362142E1C568400B84443 /* SentryScopePersistentStore+Context.swift */; }; F4227F092EFB0D8C004A27DB /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 6387B82F1ED851970045A84C /* libz.tbd */; }; F426748D2EB11E7900E09150 /* SentryReplayApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42674872EB11E7000E09150 /* SentryReplayApiTests.swift */; }; + F43F8CBC2EFC1E970004C45D /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43F8CB62EFC1E970004C45D /* SystemConfiguration.framework */; platformFilters = (ios, maccatalyst, macos, tvos, xros, ); }; + F43F8CBE2EFC1EA00004C45D /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F43F8CBD2EFC1EA00004C45D /* CoreData.framework */; }; F443DB272E09BE8C009A9045 /* LoadValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F443DB262E09BE8C009A9045 /* LoadValidatorTests.swift */; }; F44858132E03579D0013E63B /* SentryCrashDynamicLinker+Test.h in Headers */ = {isa = PBXBuildFile; fileRef = F44858122E0357940013E63B /* SentryCrashDynamicLinker+Test.h */; }; F451FAA62E0B304E0050ACF2 /* LoadValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F451FAA52E0B304E0050ACF2 /* LoadValidator.swift */; }; @@ -2036,8 +2036,6 @@ 84EACEBC2C33CA7A009B8753 /* SentryWithoutUIKit.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; name = SentryWithoutUIKit.modulemap; path = Sources/Resources/SentryWithoutUIKit.modulemap; sourceTree = SOURCE_ROOT; }; 84EACEDF2C3DCAE2009B8753 /* DeploymentTargets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DeploymentTargets.xcconfig; sourceTree = ""; }; 84EB21952BF01CEA00EDDA28 /* SentryCrashInstallationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashInstallationTests.swift; sourceTree = ""; }; - 84F994E52A6894B500EC0190 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = ./System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; - 84F994E72A6894BD00EC0190 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = ./System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 861265F72404EC1500C4AFDE /* SentryArray.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryArray.h; path = include/SentryArray.h; sourceTree = ""; }; 861265F82404EC1500C4AFDE /* SentryArray.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryArray.m; sourceTree = ""; }; 8E0551DF26A7A63C00400526 /* TestProtocolClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestProtocolClient.swift; sourceTree = ""; }; @@ -2380,6 +2378,8 @@ F41362122E1C566100B84443 /* SentryScopePersistentStore+User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryScopePersistentStore+User.swift"; sourceTree = ""; }; F41362142E1C568400B84443 /* SentryScopePersistentStore+Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryScopePersistentStore+Context.swift"; sourceTree = ""; }; F42674872EB11E7000E09150 /* SentryReplayApiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayApiTests.swift; sourceTree = ""; }; + F43F8CB62EFC1E970004C45D /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + F43F8CBD2EFC1EA00004C45D /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; F443DB262E09BE8C009A9045 /* LoadValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadValidatorTests.swift; sourceTree = ""; }; F44858122E0357940013E63B /* SentryCrashDynamicLinker+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryCrashDynamicLinker+Test.h"; sourceTree = ""; }; F451FAA52E0B304E0050ACF2 /* LoadValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadValidator.swift; sourceTree = ""; }; @@ -2520,9 +2520,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 84F994E82A6894BD00EC0190 /* SystemConfiguration.framework in Frameworks */, + F43F8CBC2EFC1E970004C45D /* SystemConfiguration.framework in Frameworks */, + F43F8CBE2EFC1EA00004C45D /* CoreData.framework in Frameworks */, F4227F092EFB0D8C004A27DB /* libz.tbd in Frameworks */, - 84F994E62A6894B500EC0190 /* CoreData.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2815,9 +2815,9 @@ 6304360C1EC05CEF00C4D3FA /* Frameworks */ = { isa = PBXGroup; children = ( + F43F8CBD2EFC1EA00004C45D /* CoreData.framework */, + F43F8CB62EFC1E970004C45D /* SystemConfiguration.framework */, D483AFA32E9D555300B43C27 /* XCTest.framework */, - 84F994E72A6894BD00EC0190 /* SystemConfiguration.framework */, - 84F994E52A6894B500EC0190 /* CoreData.framework */, 6387B82F1ED851970045A84C /* libz.tbd */, ); name = Frameworks; diff --git a/Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift b/Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift index c9452e40e56..9a2744814dc 100644 --- a/Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift +++ b/Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift @@ -35,6 +35,44 @@ public class PreviewRedactOptions: SentryRedactOptions { */ public let unmaskedViewClasses: [AnyClass] + /** + * A set of view type identifier strings that should be excluded from subtree traversal. + * + * 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). + * + * Matching uses partial string containment: if a view's class name (from `type(of: view).description()`) + * contains any of these strings, the subtree will be ignored. For example, "MyView" will match + * "MyApp.MyView", "MyViewSubclass", "Some.MyView.Container", etc. + * + * - Note: See ``SentryReplayOptions.DefaultValues.excludedViewClasses`` for the default value. + * - Note: The final set of excluded view types is computed by `SentryUIRedactBuilder` using the formula: + * **Default View Classes + Excluded View Classes - Included View Classes** + * Default view classes are defined in `SentryUIRedactBuilder` (e.g., `CameraUI.ChromeSwiftUIView` on iOS 26+). + */ + public let excludedViewClasses: Set + + /** + * A set of view type identifier strings that should be included in subtree traversal. + * + * View types exactly matching these strings will be removed from the excluded set, allowing their subtrees + * to be traversed even if they would otherwise be excluded by default or via `excludedViewClasses`. + * + * Matching uses exact string matching: the view's class name (from `type(of: view).description()`) + * must exactly equal one of these strings. For example, "MyApp.MyView" will only match exactly "MyApp.MyView", + * not "MyApp.MyViewSubclass". + * + * - Note: See ``SentryReplayOptions.DefaultValues.includedViewClasses`` for the default value. + * - Note: The final set of excluded view types is computed by `SentryUIRedactBuilder` using the formula: + * **Default View Classes + Excluded View Classes - Included View Classes** + * Default view classes are defined in `SentryUIRedactBuilder` (e.g., `CameraUI.ChromeSwiftUIView` on iOS 26+). + * - Note: Included patterns use exact matching (not partial) to prevent accidental matches. For example, + * if "ChromeCameraUI" is excluded and "Camera" is included, "ChromeCameraUI" will still be excluded + * because "Camera" doesn't exactly match "ChromeCameraUI". + */ + public let includedViewClasses: Set + /** * Enables the up to 5x faster view renderer. * @@ -50,6 +88,8 @@ public class PreviewRedactOptions: SentryRedactOptions { * - maskAllImages: Flag to redact all images in the app by drawing a rectangle over it. * - maskedViewClasses: The classes of views to mask. * - unmaskedViewClasses: The classes of views to exclude from masking. + * - excludedViewClasses: A set of view type identifiers that should be excluded from subtree traversal. + * - includedViewClasses: A set of view type identifiers that should be included in subtree traversal. * - enableViewRendererV2: Enables the up to 5x faster view renderer. * * - Note: See ``SentryReplayOptions.DefaultValues`` for the default values of each parameter. @@ -59,12 +99,16 @@ public class PreviewRedactOptions: SentryRedactOptions { maskAllImages: Bool = SentryReplayOptions.DefaultValues.maskAllImages, maskedViewClasses: [AnyClass] = SentryReplayOptions.DefaultValues.maskedViewClasses, unmaskedViewClasses: [AnyClass] = SentryReplayOptions.DefaultValues.unmaskedViewClasses, + excludedViewClasses: Set = SentryReplayOptions.DefaultValues.excludedViewClasses, + includedViewClasses: Set = SentryReplayOptions.DefaultValues.includedViewClasses, enableViewRendererV2: Bool = SentryReplayOptions.DefaultValues.enableViewRendererV2 ) { self.maskAllText = maskAllText self.maskAllImages = maskAllImages self.maskedViewClasses = maskedViewClasses self.unmaskedViewClasses = unmaskedViewClasses + self.excludedViewClasses = excludedViewClasses + self.includedViewClasses = includedViewClasses self.enableViewRendererV2 = enableViewRendererV2 } } diff --git a/Sources/Swift/Core/Protocol/SentryRedactOptions.swift b/Sources/Swift/Core/Protocol/SentryRedactOptions.swift index 00c3577db59..a66df573f81 100644 --- a/Sources/Swift/Core/Protocol/SentryRedactOptions.swift +++ b/Sources/Swift/Core/Protocol/SentryRedactOptions.swift @@ -6,6 +6,8 @@ public protocol SentryRedactOptions { var maskAllImages: Bool { get } var maskedViewClasses: [AnyClass] { get } var unmaskedViewClasses: [AnyClass] { get } + var excludedViewClasses: Set { get } + var includedViewClasses: Set { get } } @objcMembers @@ -14,4 +16,41 @@ public protocol SentryRedactOptions { public var maskAllImages: Bool = true public var maskedViewClasses: [AnyClass] = [] public var unmaskedViewClasses: [AnyClass] = [] + + /** + * A set of view type identifier strings that should be excluded from subtree traversal. + * + * 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). + * + * Matching uses partial string containment: if a view's class name (from `type(of: view).description()`) + * contains any of these strings, the subtree will be ignored. For example, "MyView" will match + * "MyApp.MyView", "MyViewSubclass", "Some.MyView.Container", etc. + * + * - Note: The final set of excluded view types is computed by `SentryUIRedactBuilder` using the formula: + * **Default View Classes + Excluded View Classes - Included View Classes** + * Default view classes are defined in `SentryUIRedactBuilder` (e.g., `CameraUI.ChromeSwiftUIView` on iOS 26+). + */ + public var excludedViewClasses: Set = [] + + /** + * A set of view type identifier strings that should be included in subtree traversal. + * + * View types exactly matching these strings will be removed from the excluded set, allowing their subtrees + * to be traversed even if they would otherwise be excluded by default or via `excludedViewClasses`. + * + * Matching uses exact string matching: the view's class name (from `type(of: view).description()`) + * must exactly equal one of these strings. For example, "MyApp.MyView" will only match exactly "MyApp.MyView", + * not "MyApp.MyViewSubclass". + * + * - Note: The final set of excluded view types is computed by `SentryUIRedactBuilder` using the formula: + * **Default View Classes + Excluded View Classes - Included View Classes** + * Default view classes are defined in `SentryUIRedactBuilder` (e.g., `CameraUI.ChromeSwiftUIView` on iOS 26+). + * For example, you can use this to re-enable traversal for `CameraUI.ChromeSwiftUIView` on iOS 26+. + * - Note: Included patterns use exact matching (not partial) to prevent accidental matches. For example, + * if "ChromeCameraUI" is excluded and "Camera" is included, "ChromeCameraUI" will still be excluded + * because "Camera" doesn't exactly match "ChromeCameraUI". + */ + public var includedViewClasses: Set = [] } diff --git a/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift b/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift index f4411453fa6..20fd963b344 100644 --- a/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift +++ b/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift @@ -102,10 +102,30 @@ final class SentryUIRedactBuilder { /// Optimized lookup: class IDs that should be redacted without layer constraints private var unconstrainedRedactClasses: Set = [] - + /// Optimized lookup: class IDs with layer constraints (includes both classId and layerId) private var constrainedRedactClasses: Set = [] + /// A set of view type identifier strings that should be excluded from subtree traversal. + /// + /// Views matching these patterns will have their subtrees skipped during redaction to avoid crashes + /// caused by traversing problematic view hierarchies. + /// + /// Matching is done using partial string matching: if the view's class name contains any of these + /// strings, the subtree will be ignored. For example, "MyView" will match "MyApp.MyView", + /// "MyViewSubclass", etc. + private var excludedViewClassPatterns: Set + + /// A set of view type identifier strings that should be included in subtree traversal. + /// + /// Views exactly matching these strings will be removed from the excluded set, allowing their subtrees + /// to be traversed even if they would otherwise be excluded. + /// + /// Matching is done using exact string matching: the view's class name must exactly equal one of these + /// strings. For example, "MyApp.MyView" will only match exactly "MyApp.MyView", not "MyApp.MyViewSubclass". + /// This prevents accidental matches where "ChromeCameraUI" is excluded but "Camera" is included. + private var includedViewClassPatterns: Set + /// Initializes a new instance of the redaction process with the specified options. /// /// This initializer populates allow/deny lists for view types using `ExtendedClassIdentifier`, @@ -145,7 +165,7 @@ final class SentryUIRedactBuilder { redactClasses.insert(ClassIdentifier(classId: "_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView")) } - + if options.maskAllImages { redactClasses.insert(ClassIdentifier(objcType: UIImageView.self)) @@ -163,7 +183,7 @@ final class SentryUIRedactBuilder { // Used by React Native to display images redactClasses.insert(ClassIdentifier(classId: "RCTImageView")) } - + #if os(iOS) redactClasses.insert(ClassIdentifier(objcType: PDFView.self)) redactClasses.insert(ClassIdentifier(objcType: WKWebView.self)) @@ -198,17 +218,38 @@ final class SentryUIRedactBuilder { #else ignoreClassesIdentifiers = [] #endif - + for type in options.unmaskedViewClasses { ignoreClassesIdentifiers.insert(ClassIdentifier(class: type)) } - + for type in options.maskedViewClasses { redactClasses.insert(ClassIdentifier(class: type)) } - + redactClassesIdentifiers = redactClasses + // Compile excluded and included patterns into separate sets for efficient lookup. + // The final decision is computed at runtime using the formula: + // + // Default View Classes + Excluded View Classes - Included View Classes + // + // SDK users can add exclusions via options.excludedViewClasses and remove defaults via options.includedViewClasses. + // Matching rules: + // - Excluded patterns use partial matching (contains): "MyView" matches "MyApp.MyView", "MyViewSubclass", etc. + // - Included patterns use exact matching (Set.contains): "MyViewSubclass" only matches exactly "MyViewSubclass" + // + // This prevents accidental matches where "ChromeCameraUI" is excluded but "Camera" is included from causing crashes. + var defaultExcluded: Set = [] + #if os(iOS) + if #available(iOS 26.0, *) { + defaultExcluded.insert(Self.cameraSwiftUIViewClassId.classId) + } + #endif + + excludedViewClassPatterns = defaultExcluded.union(options.excludedViewClasses) + includedViewClassPatterns = options.includedViewClasses + // didSet doesn't run during initialization, so we need to manually build the optimization structures rebuildOptimizedLookups() } @@ -224,7 +265,7 @@ final class SentryUIRedactBuilder { private func rebuildOptimizedLookups() { unconstrainedRedactClasses.removeAll() constrainedRedactClasses.removeAll() - + for identifier in redactClassesIdentifiers { if identifier.layerId == nil { // No layer constraint - add to unconstrained set @@ -283,40 +324,40 @@ final class SentryUIRedactBuilder { /// - `UIImageView` will match the class rule; the final decision is refined by `shouldRedact(imageView:)`. func containsRedactClass(viewClass: AnyClass, layerClass: AnyClass) -> Bool { var currentClass: AnyClass? = viewClass - + while let iteratorClass = currentClass { // Check if this class is in the unconstrained set (O(1) lookup) // This matches any layer type if unconstrainedRedactClasses.contains(ClassIdentifier(class: iteratorClass)) { return true } - + // Check if this class+layer combination is in the constrained set (O(1) lookup) // This only matches specific layer types if constrainedRedactClasses.contains(ClassIdentifier(class: iteratorClass, layer: layerClass)) { return true } - + currentClass = iteratorClass.superclass() } return false } - + /// Adds a class to the ignore list. func addIgnoreClass(_ ignoreClass: AnyClass) { ignoreClassesIdentifiers.insert(ClassIdentifier(class: ignoreClass)) } - + /// Adds a class to the redact list. func addRedactClass(_ redactClass: AnyClass) { redactClassesIdentifiers.insert(ClassIdentifier(class: redactClass)) } - + /// Adds multiple classes to the ignore list. func addIgnoreClasses(_ ignoreClasses: [AnyClass]) { ignoreClasses.forEach(addIgnoreClass(_:)) } - + /// Adds multiple classes to the redact list. func addRedactClasses(_ redactClasses: [AnyClass]) { redactClasses.forEach(addRedactClass(_:)) @@ -377,7 +418,7 @@ final class SentryUIRedactBuilder { var swiftUIRedact = [SentryRedactRegion]() var otherRegions = [SentryRedactRegion]() - + for region in redactingRegions { if region.type == .redactSwiftUI { swiftUIRedact.append(region) @@ -385,11 +426,11 @@ final class SentryUIRedactBuilder { otherRegions.append(region) } } - + //The swiftUI type needs to appear first in the list so it always get masked return (otherRegions + swiftUIRedact).reversed() } - + private func shouldIgnore(view: UIView) -> Bool { return SentryRedactViewHelper.shouldUnmask(view) || containsIgnoreClassId(ClassIdentifier(class: type(of: view))) || shouldIgnoreParentContainer(view) } @@ -437,7 +478,7 @@ final class SentryUIRedactBuilder { return true } - + /// Special handling for `UIImageView` to avoid masking tiny gradient strips and /// bundle‑provided assets (e.g. SF Symbols or app assets), which are unlikely to contain PII. private func shouldRedact(imageView: UIImageView) -> Bool { @@ -564,19 +605,28 @@ final class SentryUIRedactBuilder { // - 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 + // [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 the view type id is in the list of included view classes (exact matching). + // If yes we can exit early as this list overrules other matchings. + if includedViewClassPatterns.contains(viewTypeId) { + // Matches included pattern exactly, so don't ignore subtree + return false + } + + // Check excluded patterns using partial matching, with overruling using the included patterns with exact matching. + // + // For example, excluding "ChromeCameraUI" will match "MyApp.ChromeCameraUI", "ChromeCameraUISubclass", etc. + // + // However, if "ChromeCameraUI" is excluded and "Camera" is included, "ChromeCameraUI" will + // still be excluded because "Camera" doesn't exactly match "ChromeCameraUI". + for pattern in excludedViewClassPatterns where viewTypeId.contains(pattern) { + // Matches excluded but not exactly included, so ignore subtree return true } - #if os(iOS) +#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 @@ -584,8 +634,8 @@ final class SentryUIRedactBuilder { if viewTypeId == "UISwitch" && containsIgnoreClassId(ClassIdentifier(classId: viewTypeId)) { return true } - #endif // os(iOS) - +#endif // os(iOS) + return false } @@ -594,14 +644,14 @@ final class SentryUIRedactBuilder { let size = layer.bounds.size let anchorPoint = CGPoint(x: size.width * layer.anchorPoint.x, y: size.height * layer.anchorPoint.y) let position = parentLayer?.convert(layer.position, to: nil) ?? layer.position - + var newTransform = transform newTransform.tx = position.x newTransform.ty = position.y newTransform = CATransform3DGetAffineTransform(layer.transform).concatenating(newTransform) return newTransform.translatedBy(x: -anchorPoint.x, y: -anchorPoint.y) } - + /// Whether the transform does not contain rotation or skew. private func isAxisAligned(_ transform: CGAffineTransform) -> Bool { // Rotation exists if b or c are not zero @@ -616,7 +666,7 @@ final class SentryUIRedactBuilder { private func color(for view: UIView) -> UIColor? { return (view as? UILabel)?.textColor.withAlphaComponent(1) } - + /// Indicates whether the view is opaque and will block other views behind it. /// /// A view is considered opaque if it completely covers and hides any content behind it. diff --git a/Sources/Swift/Integrations/Screenshot/SentryScreenshotOptions.swift b/Sources/Swift/Integrations/Screenshot/SentryScreenshotOptions.swift index 47129d71b5e..a48b50d25a2 100644 --- a/Sources/Swift/Integrations/Screenshot/SentryScreenshotOptions.swift +++ b/Sources/Swift/Integrations/Screenshot/SentryScreenshotOptions.swift @@ -15,6 +15,8 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions { public static let maskAllImages: Bool = true public static let maskedViewClasses: [AnyClass] = [] public static let unmaskedViewClasses: [AnyClass] = [] + public static let excludedViewClasses: Set = [] + public static let includedViewClasses: Set = [] } // MARK: - Rendering @@ -93,7 +95,81 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions { * - Note: See ``SentryViewScreenshotOptions.init`` for the default value. */ public var unmaskedViewClasses: [AnyClass] + + /** + * A set of view type identifier strings that should be excluded from subtree traversal. + * + * 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). + * + * Matching uses partial string containment: if a view's class name (from `type(of: view).description()`) + * contains any of these strings, the subtree will be ignored. For example, "MyView" will match + * "MyApp.MyView", "MyViewSubclass", "Some.MyView.Container", etc. + * + * - Note: You should use the methods ``excludeViewTypeFromSubtreeTraversal(_:)`` and ``includeViewTypeInSubtreeTraversal(_:)`` + * to add and remove view types, so you do not accidentally remove our defaults. + * - Note: The final set of excluded view types is computed by `SentryUIRedactBuilder` using the formula: + * **Default View Classes + Excluded View Classes - Included View Classes** + * Default view classes are defined in `SentryUIRedactBuilder` (e.g., `CameraUI.ChromeSwiftUIView` on iOS 26+). + */ + public private(set) var excludedViewClasses: Set + + /** + * A set of view type identifier strings that should be included in subtree traversal. + * + * View types exactly matching these strings will be removed from the excluded set, allowing their subtrees + * to be traversed even if they would otherwise be excluded by default or via `excludedViewClasses`. + * + * Matching uses exact string matching: the view's class name (from `type(of: view).description()`) + * must exactly equal one of these strings. For example, "MyApp.MyView" will only match exactly "MyApp.MyView", + * not "MyApp.MyViewSubclass". + * + * - Note: You should use the methods ``excludeViewTypeFromSubtreeTraversal(_:)`` and ``includeViewTypeInSubtreeTraversal(_:)`` + * to add and remove view types, so you do not accidentally remove our defaults. + * - Note: The final set of excluded view types is computed by `SentryUIRedactBuilder` using the formula: + * **Default View Classes + Excluded View Classes - Included View Classes** + * Default view classes are defined in `SentryUIRedactBuilder` (e.g., `CameraUI.ChromeSwiftUIView` on iOS 26+). + * For example, you can use this to re-enable traversal for `CameraUI.ChromeSwiftUIView` on iOS 26+ + * by calling ``includeViewTypeInSubtreeTraversal("CameraUI.ChromeSwiftUIView")``. + * - Note: Included patterns use exact matching (not partial) to prevent accidental matches. For example, + * if "ChromeCameraUI" is excluded and "Camera" is included, "ChromeCameraUI" will still be excluded + * because "Camera" doesn't exactly match "ChromeCameraUI". + */ + public private(set) var includedViewClasses: Set + /** + * Adds a view type pattern to the excluded set, preventing matching views' subtrees from being traversed. + * + * - Parameter viewType: The view type identifier pattern (as a string) to exclude from subtree traversal. + * Matching uses partial string containment: if a view's class name contains this string, + * the subtree will be ignored. For example, "MyView" will match "MyApp.MyView", + * "MyViewSubclass", etc. + * + * - Note: This method adds the pattern to `excludedViewClasses`, which is then combined with + * default excluded types (defined in `SentryUIRedactBuilder`) and filtered by `includedViewClasses` + * to produce the final set. + */ + public func excludeViewTypeFromSubtreeTraversal(_ viewType: String) { + excludedViewClasses.insert(viewType) + } + + /** + * Adds a view type to the included set, allowing its subtree to be traversed. + * + * - Parameter viewType: The view type identifier (as a string) to include in subtree traversal. + * Must exactly match the result of `type(of: view).description()`. + * For example, "MyApp.MyView" will only match exactly "MyApp.MyView". + * + * - Note: This method adds the view type to `includedViewClasses`, which filters the combined set + * of default excluded types (defined in `SentryUIRedactBuilder`) and `excludedViewClasses`. + * For example, you can use this to re-enable traversal for `CameraUI.ChromeSwiftUIView` on iOS 26+. + * - Note: Included patterns use exact matching (not partial) to prevent accidental matches. + */ + public func includeViewTypeInSubtreeTraversal(_ viewType: String) { + includedViewClasses.insert(viewType) + } + /** * Initialize screenshot options disabled * @@ -109,7 +185,9 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions { maskAllText: DefaultValues.maskAllText, maskAllImages: DefaultValues.maskAllImages, maskedViewClasses: DefaultValues.maskedViewClasses, - unmaskedViewClasses: DefaultValues.unmaskedViewClasses + unmaskedViewClasses: DefaultValues.unmaskedViewClasses, + excludedViewClasses: DefaultValues.excludedViewClasses, + includedViewClasses: DefaultValues.includedViewClasses ) } @@ -133,7 +211,9 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions { }) ?? DefaultValues.maskedViewClasses, unmaskedViewClasses: (dictionary["unmaskedViewClasses"] as? NSArray)?.compactMap({ element in NSClassFromString((element as? String) ?? "") - }) ?? DefaultValues.unmaskedViewClasses + }) ?? DefaultValues.unmaskedViewClasses, + excludedViewClasses: (dictionary["excludedViewClasses"] as? [String]).map { Set($0) } ?? (dictionary["viewClassesExcludedFromSubtreeTraversal"] as? [String]).map { Set($0) } ?? DefaultValues.excludedViewClasses, + includedViewClasses: (dictionary["includedViewClasses"] as? [String]).map { Set($0) } ?? DefaultValues.includedViewClasses ) } @@ -147,6 +227,8 @@ 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. + * - excludedViewClasses: A set of view type identifiers that should be excluded from subtree traversal. + * - includedViewClasses: A set of view type identifiers that should be included in subtree traversal. * * - Note: See ``SentryViewScreenshotOptions.DefaultValues`` for the default values of each parameter. */ @@ -156,7 +238,9 @@ 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, + excludedViewClasses: Set = DefaultValues.excludedViewClasses, + includedViewClasses: Set = DefaultValues.includedViewClasses ) { // - 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. @@ -170,11 +254,13 @@ public class SentryViewScreenshotOptions: NSObject, SentryRedactOptions { self.maskAllImages = maskAllImages self.maskedViewClasses = maskedViewClasses self.unmaskedViewClasses = unmaskedViewClasses + self.excludedViewClasses = excludedViewClasses + self.includedViewClasses = includedViewClasses super.init() } public override var description: String { - return "SentryViewScreenshotOptions(enableViewRendererV2: \(enableViewRendererV2), enableFastViewRendering: \(enableFastViewRendering), maskAllText: \(maskAllText), maskAllImages: \(maskAllImages), maskedViewClasses: \(maskedViewClasses), unmaskedViewClasses: \(unmaskedViewClasses))" + return "SentryViewScreenshotOptions(enableViewRendererV2: \(enableViewRendererV2), enableFastViewRendering: \(enableFastViewRendering), maskAllText: \(maskAllText), maskAllImages: \(maskAllImages), maskedViewClasses: \(maskedViewClasses), unmaskedViewClasses: \(unmaskedViewClasses), excludedViewClasses: \(excludedViewClasses), includedViewClasses: \(includedViewClasses))" } } diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index 6520f49b1c2..d048ff4d780 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -22,6 +22,9 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { public static let maskedViewClasses: [AnyClass] = [] public static let unmaskedViewClasses: [AnyClass] = [] + public static let excludedViewClasses: Set = [] + public static let includedViewClasses: Set = [] + // The following properties are defaults which are not configurable by the user. fileprivate static let sdkInfo: [String: Any]? = nil @@ -162,6 +165,80 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { */ public var unmaskedViewClasses: [AnyClass] + /** + * A set of view type identifier strings that should be excluded from subtree traversal. + * + * 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). + * + * Matching uses partial string containment: if a view's class name (from `type(of: view).description()`) + * contains any of these strings, the subtree will be ignored. For example, "MyView" will match + * "MyApp.MyView", "MyViewSubclass", "Some.MyView.Container", etc. + * + * - Note: You should use the methods ``excludeViewTypeFromSubtreeTraversal(_:)`` and ``includeViewTypeInSubtreeTraversal(_:)`` + * to add and remove view types, so you do not accidentally remove our defaults. + * - Note: The final set of excluded view types is computed by `SentryUIRedactBuilder` using the formula: + * **Default View Classes + Excluded View Classes - Included View Classes** + * Default view classes are defined in `SentryUIRedactBuilder` (e.g., `CameraUI.ChromeSwiftUIView` on iOS 26+). + */ + public var excludedViewClasses: Set + + /** + * A set of view type identifier strings that should be included in subtree traversal. + * + * View types exactly matching these strings will be removed from the excluded set, allowing their subtrees + * to be traversed even if they would otherwise be excluded by default or via `excludedViewClasses`. + * + * Matching uses exact string matching: the view's class name (from `type(of: view).description()`) + * must exactly equal one of these strings. For example, "MyApp.MyView" will only match exactly "MyApp.MyView", + * not "MyApp.MyViewSubclass". + * + * - Note: You should use the methods ``excludeViewTypeFromSubtreeTraversal(_:)`` and ``includeViewTypeInSubtreeTraversal(_:)`` + * to add and remove view types, so you do not accidentally remove our defaults. + * - Note: The final set of excluded view types is computed by `SentryUIRedactBuilder` using the formula: + * **Default View Classes + Excluded View Classes - Included View Classes** + * Default view classes are defined in `SentryUIRedactBuilder` (e.g., `CameraUI.ChromeSwiftUIView` on iOS 26+). + * For example, you can use this to re-enable traversal for `CameraUI.ChromeSwiftUIView` on iOS 26+ + * by calling ``includeViewTypeInSubtreeTraversal("CameraUI.ChromeSwiftUIView")``. + * - Note: Included patterns use exact matching (not partial) to prevent accidental matches. For example, + * if "ChromeCameraUI" is excluded and "Camera" is included, "ChromeCameraUI" will still be excluded + * because "Camera" doesn't exactly match "ChromeCameraUI". + */ + public var includedViewClasses: Set + + /** + * Adds a view type pattern to the excluded set, preventing matching views' subtrees from being traversed. + * + * - Parameter viewType: The view type identifier pattern (as a string) to exclude from subtree traversal. + * Matching uses partial string containment: if a view's class name contains this string, + * the subtree will be ignored. For example, "MyView" will match "MyApp.MyView", + * "MyViewSubclass", etc. + * + * - Note: This method adds the pattern to `excludedViewClasses`, which is then combined with + * default excluded types (defined in `SentryUIRedactBuilder`) and filtered by `includedViewClasses` + * to produce the final set. + */ + public func excludeViewTypeFromSubtreeTraversal(_ viewType: String) { + excludedViewClasses.insert(viewType) + } + + /** + * Adds a view type to the included set, allowing its subtree to be traversed. + * + * - Parameter viewType: The view type identifier (as a string) to include in subtree traversal. + * Must exactly match the result of `type(of: view).description()`. + * For example, "MyApp.MyView" will only match exactly "MyApp.MyView". + * + * - Note: This method adds the view type to `includedViewClasses`, which filters the combined set + * of default excluded types (defined in `SentryUIRedactBuilder`) and `excludedViewClasses`. + * For example, you can use this to re-enable traversal for `CameraUI.ChromeSwiftUIView` on iOS 26+. + * - Note: Included patterns use exact matching (not partial) to prevent accidental matches. + */ + public func includeViewTypeInSubtreeTraversal(_ viewType: String) { + includedViewClasses.insert(viewType) + } + /** * Alias for ``enableViewRendererV2``. * @@ -330,7 +407,9 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { frameRate: (dictionary["frameRate"] as? NSNumber)?.uintValue, errorReplayDuration: (dictionary["errorReplayDuration"] as? NSNumber)?.doubleValue, sessionSegmentDuration: (dictionary["sessionSegmentDuration"] as? NSNumber)?.doubleValue, - maximumDuration: (dictionary["maximumDuration"] as? NSNumber)?.doubleValue + maximumDuration: (dictionary["maximumDuration"] as? NSNumber)?.doubleValue, + excludedViewClasses: (dictionary["excludedViewClasses"] as? [String]).map { Set($0) } ?? (dictionary["viewClassesExcludedFromSubtreeTraversal"] as? [String]).map { Set($0) }, + includedViewClasses: (dictionary["includedViewClasses"] as? [String]).map { Set($0) } ) } @@ -375,7 +454,9 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { frameRate: nil, errorReplayDuration: nil, sessionSegmentDuration: nil, - maximumDuration: nil + maximumDuration: nil, + excludedViewClasses: nil, + includedViewClasses: nil ) } @@ -394,7 +475,9 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { frameRate: UInt?, errorReplayDuration: TimeInterval?, sessionSegmentDuration: TimeInterval?, - maximumDuration: TimeInterval? + maximumDuration: TimeInterval?, + excludedViewClasses: Set? = nil, + includedViewClasses: Set? = nil ) { self.sessionSampleRate = sessionSampleRate ?? DefaultValues.sessionSampleRate self.onErrorSampleRate = onErrorSampleRate ?? DefaultValues.onErrorSampleRate @@ -410,7 +493,9 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { self.errorReplayDuration = errorReplayDuration ?? DefaultValues.errorReplayDuration self.sessionSegmentDuration = sessionSegmentDuration ?? DefaultValues.sessionSegmentDuration self.maximumDuration = maximumDuration ?? DefaultValues.maximumDuration - + self.excludedViewClasses = excludedViewClasses ?? DefaultValues.excludedViewClasses + self.includedViewClasses = includedViewClasses ?? DefaultValues.includedViewClasses + super.init() } } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift index 11404bf1f61..c205c79ce99 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift @@ -1,3 +1,4 @@ +// swiftlint:disable file_length import Foundation @_spi(Private) @testable import Sentry import XCTest @@ -227,6 +228,7 @@ class SentryReplayOptionsTests: XCTestCase { "enableFastViewRendering": false, "maskedViewClasses": ["NSString"], "unmaskedViewClasses": ["NSNumber"], + "excludedViewClasses": ["MyCustomView", "AnotherView"], "quality": 0, "frameRate": 2, "errorReplayDuration": 300, @@ -250,6 +252,10 @@ class SentryReplayOptionsTests: XCTestCase { let unmaskedViewClass: AnyClass = try XCTUnwrap(options.unmaskedViewClasses.first) XCTAssertEqual(ObjectIdentifier(unmaskedViewClass), ObjectIdentifier(NSNumber.self)) + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView")) + XCTAssertTrue(options.excludedViewClasses.contains("AnotherView")) + XCTAssertEqual(options.excludedViewClasses.count, 2) + XCTAssertEqual(options.quality, .low) XCTAssertEqual(options.frameRate, 2) XCTAssertEqual(options.errorReplayDuration, 300) @@ -850,4 +856,137 @@ class SentryReplayOptionsTests: XCTestCase { // -- Assert -- XCTAssertNil(options.sdkInfo) } + + // MARK: excludedViewClasses and includedViewClasses + + func testInitFromDict_excludedViewClasses_whenValidValue_shouldSetValue() { + // -- Act -- + let options = SentryReplayOptions(dictionary: [ + "excludedViewClasses": ["MyCustomView"] + ]) + + // -- Assert -- + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView")) + XCTAssertEqual(options.excludedViewClasses.count, 1) + } + + func testInitFromDict_excludedViewClasses_whenMultipleValidValues_shouldKeepAll() { + // -- Act -- + let options = SentryReplayOptions(dictionary: [ + "excludedViewClasses": ["MyCustomView1", "MyCustomView2", "MyCustomView3"] + ]) + + // -- Assert -- + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView1")) + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView2")) + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView3")) + XCTAssertEqual(options.excludedViewClasses.count, 3) + } + + func testInitFromDict_excludedViewClasses_whenLegacyKey_shouldSetValue() { + // -- Act -- + let options = SentryReplayOptions(dictionary: [ + "viewClassesExcludedFromSubtreeTraversal": ["MyCustomView"] + ]) + + // -- Assert -- + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView")) + XCTAssertEqual(options.excludedViewClasses.count, 1) + } + + func testInitFromDict_excludedViewClasses_whenInvalidValue_shouldUseDefaultValue() { + // -- Act -- + let options = SentryReplayOptions(dictionary: [ + "excludedViewClasses": "invalid_value" + ]) + + // -- Assert -- + XCTAssertEqual(options.excludedViewClasses.count, 0) + } + + func testInitFromDict_excludedViewClasses_whenKeyOmitted_shouldUseDefaultValue() { + // -- Act -- + let options = SentryReplayOptions(dictionary: [:]) + + // -- Assert -- + XCTAssertEqual(options.excludedViewClasses.count, 0) + XCTAssertEqual(options.includedViewClasses.count, 0) + } + + func testExcludedViewClasses_defaultInitialization_shouldBeEmpty() { + // -- Act -- + let options = SentryReplayOptions() + + // -- Assert -- + XCTAssertEqual(options.excludedViewClasses.count, 0) + XCTAssertEqual(options.includedViewClasses.count, 0) + } + + func testExcludedViewClasses_excludeViewTypeFromSubtreeTraversal_shouldAddViewType() { + // -- Arrange -- + let options = SentryReplayOptions() + + // -- Act -- + options.excludeViewTypeFromSubtreeTraversal("MyCustomView") + + // -- Assert -- + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView")) + XCTAssertEqual(options.excludedViewClasses.count, 1) + } + + func testExcludedViewClasses_excludeViewTypeFromSubtreeTraversal_multipleCalls_shouldAddAllViewTypes() { + // -- Arrange -- + let options = SentryReplayOptions() + + // -- Act -- + options.excludeViewTypeFromSubtreeTraversal("MyCustomView1") + options.excludeViewTypeFromSubtreeTraversal("MyCustomView2") + options.excludeViewTypeFromSubtreeTraversal("MyCustomView3") + + // -- Assert -- + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView1")) + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView2")) + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView3")) + XCTAssertEqual(options.excludedViewClasses.count, 3) + } + + func testIncludedViewClasses_includeViewTypeInSubtreeTraversal_shouldAddViewType() { + // -- Arrange -- + let options = SentryReplayOptions() + options.excludeViewTypeFromSubtreeTraversal("MyCustomView") + XCTAssertTrue(options.excludedViewClasses.contains("MyCustomView")) + + // -- Act -- + options.includeViewTypeInSubtreeTraversal("MyCustomView") + + // -- Assert -- + XCTAssertTrue(options.includedViewClasses.contains("MyCustomView")) + XCTAssertEqual(options.includedViewClasses.count, 1) + } + + func testIncludedViewClasses_includeViewTypeInSubtreeTraversal_shouldAllowRemovingDefaultCameraUIView() { + // -- Arrange -- + let options = SentryReplayOptions() + + // -- Act -- + options.includeViewTypeInSubtreeTraversal("CameraUI.ChromeSwiftUIView") + + // -- Assert -- + XCTAssertTrue(options.includedViewClasses.contains("CameraUI.ChromeSwiftUIView")) + XCTAssertEqual(options.includedViewClasses.count, 1) + } + + func testIncludedViewClasses_includeViewTypeInSubtreeTraversal_nonExistentViewType_shouldNotCrash() { + // -- Arrange -- + let options = SentryReplayOptions() + let initialCount = options.includedViewClasses.count + + // -- Act -- + options.includeViewTypeInSubtreeTraversal("NonExistentView") + + // -- Assert -- + XCTAssertEqual(options.includedViewClasses.count, initialCount + 1) + XCTAssertTrue(options.includedViewClasses.contains("NonExistentView")) + } } +// swiftlint:enable file_length diff --git a/Tests/SentryTests/Protocol/SentryRedactDefaultOptionsTests.swift b/Tests/SentryTests/Protocol/SentryRedactDefaultOptionsTests.swift index 0c9eb1e77d2..7cf227101bf 100644 --- a/Tests/SentryTests/Protocol/SentryRedactDefaultOptionsTests.swift +++ b/Tests/SentryTests/Protocol/SentryRedactDefaultOptionsTests.swift @@ -12,5 +12,7 @@ class SentryRedactDefaultOptionsTests: XCTestCase { XCTAssertTrue(options.maskAllImages) XCTAssertEqual(options.maskedViewClasses.count, 0) XCTAssertEqual(options.unmaskedViewClasses.count, 0) + XCTAssertEqual(options.excludedViewClasses.count, 0) + XCTAssertEqual(options.includedViewClasses.count, 0) } } diff --git a/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+Common.swift b/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+Common.swift index 4d45a85959c..ec3b212a43b 100644 --- a/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+Common.swift +++ b/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+Common.swift @@ -13,12 +13,14 @@ import XCTest /// See `SentryUIRedactBuilderTests.swift` for more information on how to print the internal view hierarchy of a view. class SentryUIRedactBuilderTests_Common: SentryUIRedactBuilderTests { // swiftlint:disable:this type_name - private func getSut(maskAllText: Bool, maskAllImages: Bool, maskedViewClasses: [AnyClass] = [], unmaskedViewClasses: [AnyClass] = []) -> SentryUIRedactBuilder { + private func getSut(maskAllText: Bool, maskAllImages: Bool, maskedViewClasses: [AnyClass] = [], unmaskedViewClasses: [AnyClass] = [], viewClassesExcludedFromSubtreeTraversal: Set = []) -> SentryUIRedactBuilder { return SentryUIRedactBuilder(options: TestRedactOptions( maskAllText: maskAllText, maskAllImages: maskAllImages, maskedViewClasses: maskedViewClasses, - unmaskedViewClasses: unmaskedViewClasses + unmaskedViewClasses: unmaskedViewClasses, + excludedViewClasses: viewClassesExcludedFromSubtreeTraversal, + includedViewClasses: [] )) } @@ -1297,10 +1299,309 @@ class SentryUIRedactBuilderTests_Common: SentryUIRedactBuilderTests { // swiftli // Label should be ignored because UILabel is in the ignore list XCTAssertEqual(result.count, 0) } + + // MARK: - Subtree Traversal Ignoring + + func testSubtreeTraversalIgnored_withProblematicView_shouldNotAccessSublayers() throws { + // -- Arrange -- + // Create a view with a problematic layer that crashes when sublayers is accessed + // This simulates the real-world issue with CameraUI.ChromeSwiftUIView and CameraUI.ModeLoupeLayer + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalView = NormalView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalViewLayer = try XCTUnwrap(normalView.layer as? NormalLayer) + rootView.addSubview(normalView) + + let problematicView = ProblematicView(frame: CGRect(x: 10, y: 10, width: 80, height: 80)) + let problematicViewLayer = try XCTUnwrap(problematicView.layer as? ProblematicLayer) + normalView.addSubview(problematicView) + + // -- Act -- + let viewTypeId = type(of: problematicView).description() + let sut = getSut( + maskAllText: true, + maskAllImages: true, + viewClassesExcludedFromSubtreeTraversal: [viewTypeId] + ) + + // Reset the sublayers before redaction in case it was called by UIKit internals + normalViewLayer.sublayerInvocations.removeAll() + problematicViewLayer.sublayerInvocations.removeAll() + + // This should not crash because the problematic view's subtree is ignored + let _ = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + XCTAssertEqual(normalViewLayer.sublayerInvocations.count, 1) + XCTAssertEqual(problematicViewLayer.sublayerInvocations.count, 0) + } + + func testSubtreeTraversalIgnored_withoutIgnoredViewType_shouldTraverseChildrenNormally() throws { + // -- Arrange -- + // When no view types are ignored, sublayers should be accessed normally + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalView = NormalView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalViewLayer = try XCTUnwrap(normalView.layer as? NormalLayer) + rootView.addSubview(normalView) + + let problematicView = ProblematicView(frame: CGRect(x: 10, y: 10, width: 80, height: 80)) + let problematicViewLayer = try XCTUnwrap(problematicView.layer as? ProblematicLayer) + normalView.addSubview(problematicView) + + // -- Act -- + let sut = getSut( + maskAllText: true, + maskAllImages: true, + viewClassesExcludedFromSubtreeTraversal: [] // Empty set - no ignored types + ) + + // Reset the sublayers before redaction in case it was called by UIKit internals + normalViewLayer.sublayerInvocations.removeAll() + problematicViewLayer.sublayerInvocations.removeAll() + + let _ = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + // When not ignored, sublayers should be accessed (which would crash with ProblematicLayer in real scenario) + // normalViewLayer.sublayers is accessed to traverse its children + XCTAssertGreaterThanOrEqual(normalViewLayer.sublayerInvocations.count, 1, "Normal view's sublayers should be accessed when not ignored") + // problematicViewLayer.sublayers is accessed because it's a child that needs to be traversed + XCTAssertGreaterThanOrEqual(problematicViewLayer.sublayerInvocations.count, 1, "Problematic view's sublayers should be accessed when not ignored (would crash in real scenario)") + } + + func testSubtreeTraversalIgnored_withMultipleIgnoredViewTypes_shouldIgnoreAll() throws { + // -- Arrange -- + // Create two different problematic view types + class ProblematicView1: ProblematicView {} + class ProblematicView2: ProblematicView {} + + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalView = NormalView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalViewLayer = try XCTUnwrap(normalView.layer as? NormalLayer) + rootView.addSubview(normalView) + + let problematicView1 = ProblematicView1(frame: CGRect(x: 10, y: 10, width: 30, height: 30)) + let problematicView1Layer = try XCTUnwrap(problematicView1.layer as? ProblematicLayer) + normalView.addSubview(problematicView1) + + let problematicView2 = ProblematicView2(frame: CGRect(x: 50, y: 50, width: 30, height: 30)) + let problematicView2Layer = try XCTUnwrap(problematicView2.layer as? ProblematicLayer) + normalView.addSubview(problematicView2) + + // -- Act -- + let viewTypeId1 = type(of: problematicView1).description() + let viewTypeId2 = type(of: problematicView2).description() + let sut = getSut( + maskAllText: true, + maskAllImages: true, + viewClassesExcludedFromSubtreeTraversal: [viewTypeId1, viewTypeId2] + ) + + // Reset the sublayers before redaction in case it was called by UIKit internals + normalViewLayer.sublayerInvocations.removeAll() + problematicView1Layer.sublayerInvocations.removeAll() + problematicView2Layer.sublayerInvocations.removeAll() + + let _ = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + // Normal view's sublayers should be accessed to traverse children + XCTAssertGreaterThanOrEqual(normalViewLayer.sublayerInvocations.count, 1, "Normal view's sublayers should be accessed") + // Both problematic views' sublayers should NOT be accessed because they're ignored + XCTAssertEqual(problematicView1Layer.sublayerInvocations.count, 0, "First problematic view's sublayers should not be accessed when ignored") + XCTAssertEqual(problematicView2Layer.sublayerInvocations.count, 0, "Second problematic view's sublayers should not be accessed when ignored") + } + + func testSubtreeTraversalIgnored_withPartialMatching_shouldMatchSubstrings() throws { + // -- Arrange -- + // Test that partial matching works: "Problematic" should match "MyApp.ProblematicView", "ProblematicViewSubclass", etc. + class MyAppProblematicView: ProblematicView {} + class ProblematicViewSubclass: ProblematicView {} + class SomeOtherView: UIView { + override class var layerClass: AnyClass { + return NormalLayer.self + } + } + + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalView = NormalView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalViewLayer = try XCTUnwrap(normalView.layer as? NormalLayer) + rootView.addSubview(normalView) + + let problematicView1 = MyAppProblematicView(frame: CGRect(x: 10, y: 10, width: 30, height: 30)) + let problematicView1Layer = try XCTUnwrap(problematicView1.layer as? ProblematicLayer) + normalView.addSubview(problematicView1) + + let problematicView2 = ProblematicViewSubclass(frame: CGRect(x: 50, y: 50, width: 30, height: 30)) + let problematicView2Layer = try XCTUnwrap(problematicView2.layer as? ProblematicLayer) + normalView.addSubview(problematicView2) + + let otherView = SomeOtherView(frame: CGRect(x: 20, y: 20, width: 20, height: 20)) + let otherViewLayer = try XCTUnwrap(otherView.layer as? NormalLayer) + normalView.addSubview(otherView) + + // -- Act -- + // Use partial match "Problematic" which should match both MyAppProblematicView and ProblematicViewSubclass + let sut = getSut( + maskAllText: true, + maskAllImages: true, + viewClassesExcludedFromSubtreeTraversal: ["Problematic"] // Partial match + ) + + // Reset the sublayers before redaction + normalViewLayer.sublayerInvocations.removeAll() + problematicView1Layer.sublayerInvocations.removeAll() + problematicView2Layer.sublayerInvocations.removeAll() + otherViewLayer.sublayerInvocations.removeAll() + + let _ = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + // Normal view's sublayers should be accessed + XCTAssertGreaterThanOrEqual(normalViewLayer.sublayerInvocations.count, 1, "Normal view's sublayers should be accessed") + // Both problematic views' sublayers should NOT be accessed because "Problematic" matches their class names + XCTAssertEqual(problematicView1Layer.sublayerInvocations.count, 0, "MyAppProblematicView's sublayers should not be accessed when 'Problematic' is excluded") + XCTAssertEqual(problematicView2Layer.sublayerInvocations.count, 0, "ProblematicViewSubclass's sublayers should not be accessed when 'Problematic' is excluded") + // Other view's sublayers should be accessed normally (doesn't contain "Problematic") + XCTAssertGreaterThanOrEqual(otherViewLayer.sublayerInvocations.count, 1, "SomeOtherView's sublayers should be accessed normally") + } + + func testSubtreeTraversalIgnored_withExactMatchingIncluded_shouldRemoveFromExcluded() throws { + // -- Arrange -- + // Test that included patterns use exact matching to remove views from excluded set + class MyAppProblematicView: ProblematicView {} + class ProblematicViewSubclass: ProblematicView {} + + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalView = NormalView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalViewLayer = try XCTUnwrap(normalView.layer as? NormalLayer) + rootView.addSubview(normalView) + + let problematicView1 = MyAppProblematicView(frame: CGRect(x: 10, y: 10, width: 30, height: 30)) + let problematicView1Layer = try XCTUnwrap(problematicView1.layer as? ProblematicLayer) + normalView.addSubview(problematicView1) + + let problematicView2 = ProblematicViewSubclass(frame: CGRect(x: 50, y: 50, width: 30, height: 30)) + let problematicView2Layer = try XCTUnwrap(problematicView2.layer as? ProblematicLayer) + normalView.addSubview(problematicView2) + + // -- Act -- + // Exclude "Problematic" (partial match) but include exact class name "ProblematicViewSubclass" + // So ProblematicViewSubclass should be traversed, but MyAppProblematicView should not + let problematicView2TypeId = type(of: problematicView2).description() + let options = TestRedactOptions( + maskAllText: true, + maskAllImages: true, + excludedViewClasses: ["Problematic"], + includedViewClasses: [problematicView2TypeId] // Exact match required + ) + let sut = SentryUIRedactBuilder(options: options) + + // Reset the sublayers before redaction + normalViewLayer.sublayerInvocations.removeAll() + problematicView1Layer.sublayerInvocations.removeAll() + problematicView2Layer.sublayerInvocations.removeAll() + + let _ = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + // Normal view's sublayers should be accessed + XCTAssertGreaterThanOrEqual(normalViewLayer.sublayerInvocations.count, 1, "Normal view's sublayers should be accessed") + // MyAppProblematicView's sublayers should NOT be accessed (matches "Problematic" but doesn't exactly match included) + XCTAssertEqual(problematicView1Layer.sublayerInvocations.count, 0, "MyAppProblematicView's sublayers should not be accessed") + // ProblematicViewSubclass's sublayers SHOULD be accessed (matches "Problematic" but exactly matches included pattern) + XCTAssertGreaterThanOrEqual(problematicView2Layer.sublayerInvocations.count, 1, "ProblematicViewSubclass's sublayers should be accessed because it exactly matches the included pattern") + } + + func testSubtreeTraversalIgnored_withIncludedPartialMatch_shouldNotRemoveFromExcluded() throws { + // -- Arrange -- + // Test that included patterns require exact matching, so partial matches don't accidentally remove exclusions + // This prevents cases where "ChromeCameraUI" is excluded but "Camera" is included from causing crashes + class ChromeCameraUIView: ProblematicView {} + + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalView = NormalView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let normalViewLayer = try XCTUnwrap(normalView.layer as? NormalLayer) + rootView.addSubview(normalView) + + let chromeCameraView = ChromeCameraUIView(frame: CGRect(x: 10, y: 10, width: 30, height: 30)) + let chromeCameraViewLayer = try XCTUnwrap(chromeCameraView.layer as? ProblematicLayer) + normalView.addSubview(chromeCameraView) + + // -- Act -- + // Exclude "ChromeCameraUI" but include "Camera" - ChromeCameraUIView should still be excluded + // because "Camera" doesn't exactly match "ChromeCameraUIView" + let chromeCameraViewTypeId = type(of: chromeCameraView).description() + let options = TestRedactOptions( + maskAllText: true, + maskAllImages: true, + excludedViewClasses: [chromeCameraViewTypeId], + includedViewClasses: ["Camera"] // Partial match should NOT work + ) + let sut = SentryUIRedactBuilder(options: options) + + // Reset the sublayers before redaction + normalViewLayer.sublayerInvocations.removeAll() + chromeCameraViewLayer.sublayerInvocations.removeAll() + + let _ = sut.redactRegionsFor(view: rootView) + + // -- Assert -- + // Normal view's sublayers should be accessed + XCTAssertGreaterThanOrEqual(normalViewLayer.sublayerInvocations.count, 1, "Normal view's sublayers should be accessed") + // ChromeCameraUIView's sublayers should NOT be accessed because "Camera" doesn't exactly match the class name + XCTAssertEqual(chromeCameraViewLayer.sublayerInvocations.count, 0, "ChromeCameraUIView's sublayers should not be accessed because included pattern requires exact match") + } } // MARK: - Test Views +/// A layer that does not crash when `sublayers` is accessed, simulating views like UIView that do not crash when their sublayers are accessed. +private class NormalLayer: CALayer { + var sublayerInvocations: Invocations = Invocations() + override var sublayers: [CALayer]? { + get { + sublayerInvocations.record(Void()) + return super.sublayers + } + set { + sublayerInvocations.record(Void()) + super.sublayers = newValue + } + } +} + +/// A view that uses a normal layer, simulating views like UIView that do not crash when their sublayers are accessed. +private class NormalView: UIView { + override class var layerClass: AnyClass { + return NormalLayer.self + } +} + +/// A layer that crashes when `sublayers` is accessed, simulating the issue with CameraUI.ModeLoupeLayer +/// and other problematic layers that cause crashes during view traversal. +private class ProblematicLayer: CALayer { + var sublayerInvocations: Invocations = Invocations() + + override var sublayers: [CALayer]? { + get { + sublayerInvocations.record(Void()) + return super.sublayers + } + set { + sublayerInvocations.record(Void()) + super.sublayers = newValue + } + } +} + +/// A view that uses ProblematicLayer, simulating views like CameraUI.ChromeSwiftUIView +/// that contain layers which crash when their sublayers are accessed. +private class ProblematicView: UIView { + override class var layerClass: AnyClass { + return ProblematicLayer.self + } +} + private class TestCustomVisibilityView: UIView { class CustomLayer: CALayer { override var opacity: Float { @@ -1342,7 +1643,6 @@ private class TestGridView: UIView { ctx.setFillColor(UIColor.orange.cgColor) ctx.fill(CGRect(x: midX, y: midY, width: bounds.width - midX, height: bounds.height - midY)) } - } private func isBuiltWithSDK26() -> Bool { diff --git a/Tests/SentryTests/ViewCapture/TestRedactOptions.swift b/Tests/SentryTests/ViewCapture/TestRedactOptions.swift index 546980acc26..d72acdaf947 100644 --- a/Tests/SentryTests/ViewCapture/TestRedactOptions.swift +++ b/Tests/SentryTests/ViewCapture/TestRedactOptions.swift @@ -3,16 +3,22 @@ class TestRedactOptions: SentryRedactOptions { var unmaskedViewClasses: [AnyClass] var maskAllText: Bool var maskAllImages: Bool + var excludedViewClasses: Set + var includedViewClasses: Set init( maskAllText: Bool = true, maskAllImages: Bool = true, maskedViewClasses: [AnyClass] = [], - unmaskedViewClasses: [AnyClass] = [] + unmaskedViewClasses: [AnyClass] = [], + excludedViewClasses: Set = [], + includedViewClasses: Set = [] ) { self.maskAllText = maskAllText self.maskAllImages = maskAllImages self.maskedViewClasses = maskedViewClasses self.unmaskedViewClasses = unmaskedViewClasses + self.excludedViewClasses = excludedViewClasses + self.includedViewClasses = includedViewClasses } } diff --git a/sdk_api.json b/sdk_api.json index d8254369f76..2e8d6803ba4 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -43787,6 +43787,136 @@ "accessorKind": "get" } ] + }, + { + "kind": "Var", + "name": "excludedViewClasses", + "printedName": "excludedViewClasses", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Var", + "usr": "s:6Sentry0A21ViewScreenshotOptionsC13DefaultValuesC08excludedB7ClassesShySSGvpZ", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC13DefaultValuesC08excludedB7ClassesShySSGvpZ", + "moduleName": "Sentry", + "static": true, + "declAttributes": [ + "Final", + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Accessor", + "usr": "s:6Sentry0A21ViewScreenshotOptionsC13DefaultValuesC08excludedB7ClassesShySSGvgZ", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC13DefaultValuesC08excludedB7ClassesShySSGvgZ", + "moduleName": "Sentry", + "static": true, + "implicit": true, + "declAttributes": [ + "Final" + ], + "accessorKind": "get" + } + ] + }, + { + "kind": "Var", + "name": "includedViewClasses", + "printedName": "includedViewClasses", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Var", + "usr": "s:6Sentry0A21ViewScreenshotOptionsC13DefaultValuesC08includedB7ClassesShySSGvpZ", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC13DefaultValuesC08includedB7ClassesShySSGvpZ", + "moduleName": "Sentry", + "static": true, + "declAttributes": [ + "Final", + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Accessor", + "usr": "s:6Sentry0A21ViewScreenshotOptionsC13DefaultValuesC08includedB7ClassesShySSGvgZ", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC13DefaultValuesC08includedB7ClassesShySSGvgZ", + "moduleName": "Sentry", + "static": true, + "implicit": true, + "declAttributes": [ + "Final" + ], + "accessorKind": "get" + } + ] } ], "declKind": "Class", @@ -44467,6 +44597,180 @@ } ] }, + { + "kind": "Var", + "name": "excludedViewClasses", + "printedName": "excludedViewClasses", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryViewScreenshotOptions(py)excludedViewClasses", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC08excludedB7ClassesShySSGvp", + "moduleName": "Sentry", + "objc_name": "excludedViewClasses", + "declAttributes": [ + "ObjC" + ], + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryViewScreenshotOptions(im)excludedViewClasses", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC08excludedB7ClassesShySSGvg", + "moduleName": "Sentry", + "objc_name": "excludedViewClasses", + "declAttributes": [ + "ObjC" + ], + "accessorKind": "get" + } + ] + }, + { + "kind": "Var", + "name": "includedViewClasses", + "printedName": "includedViewClasses", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryViewScreenshotOptions(py)includedViewClasses", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC08includedB7ClassesShySSGvp", + "moduleName": "Sentry", + "objc_name": "includedViewClasses", + "declAttributes": [ + "ObjC" + ], + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryViewScreenshotOptions(im)includedViewClasses", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC08includedB7ClassesShySSGvg", + "moduleName": "Sentry", + "objc_name": "includedViewClasses", + "declAttributes": [ + "ObjC" + ], + "accessorKind": "get" + } + ] + }, + { + "kind": "Function", + "name": "excludeViewTypeFromSubtreeTraversal", + "printedName": "excludeViewTypeFromSubtreeTraversal(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "c:@M@Sentry@objc(cs)SentryViewScreenshotOptions(im)excludeViewTypeFromSubtreeTraversal:", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC07excludeB24TypeFromSubtreeTraversalyySSF", + "moduleName": "Sentry", + "declAttributes": [ + "ObjC" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "includeViewTypeInSubtreeTraversal", + "printedName": "includeViewTypeInSubtreeTraversal(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "c:@M@Sentry@objc(cs)SentryViewScreenshotOptions(im)includeViewTypeInSubtreeTraversal:", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC07includeB22TypeInSubtreeTraversalyySSF", + "moduleName": "Sentry", + "declAttributes": [ + "ObjC" + ], + "funcSelfKind": "NonMutating" + }, { "kind": "Constructor", "name": "init", @@ -44495,7 +44799,7 @@ { "kind": "Constructor", "name": "init", - "printedName": "init(enableViewRendererV2:enableFastViewRendering:maskAllText:maskAllImages:maskedViewClasses:unmaskedViewClasses:)", + "printedName": "init(enableViewRendererV2:enableFastViewRendering:maskAllText:maskAllImages:maskedViewClasses:unmaskedViewClasses:excludedViewClasses:includedViewClasses:)", "children": [ { "kind": "TypeNominal", @@ -44614,13 +44918,43 @@ ], "hasDefaultArg": true, "usr": "s:Sa" + }, + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "hasDefaultArg": true, + "usr": "s:Sh" + }, + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "hasDefaultArg": true, + "usr": "s:Sh" } ], "declKind": "Constructor", - "usr": "c:@M@Sentry@objc(cs)SentryViewScreenshotOptions(im)initWithEnableViewRendererV2:enableFastViewRendering:maskAllText:maskAllImages:maskedViewClasses:unmaskedViewClasses:", - "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC06enableB10RendererV20e4FastB9Rendering11maskAllText0jK6Images06maskedB7Classes08unmaskedbO0ACSb_S3bSayyXlXpGAJtcfc", + "usr": "c:@M@Sentry@objc(cs)SentryViewScreenshotOptions(im)initWithEnableViewRendererV2:enableFastViewRendering:maskAllText:maskAllImages:maskedViewClasses:unmaskedViewClasses:excludedViewClasses:includedViewClasses:", + "mangledName": "$s6Sentry0A21ViewScreenshotOptionsC06enableB10RendererV20e4FastB9Rendering11maskAllText0jK6Images06maskedB7Classes08unmaskedbO008excludedbO008includedbO0ACSb_S3bSayyXlXpGALShySSGAMtcfc", "moduleName": "Sentry", - "objc_name": "initWithEnableViewRendererV2:enableFastViewRendering:maskAllText:maskAllImages:maskedViewClasses:unmaskedViewClasses:", + "objc_name": "initWithEnableViewRendererV2:enableFastViewRendering:maskAllText:maskAllImages:maskedViewClasses:unmaskedViewClasses:excludedViewClasses:includedViewClasses:", "declAttributes": [ "ObjC" ], @@ -49168,12 +49502,142 @@ ] } ], - "usr": "s:Sa" + "usr": "s:Sa" + } + ], + "declKind": "Accessor", + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC19unmaskedViewClassesSayyXlXpGvgZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC19unmaskedViewClassesSayyXlXpGvgZ", + "moduleName": "Sentry", + "static": true, + "implicit": true, + "declAttributes": [ + "Final" + ], + "accessorKind": "get" + } + ] + }, + { + "kind": "Var", + "name": "excludedViewClasses", + "printedName": "excludedViewClasses", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Var", + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC19excludedViewClassesShySSGvpZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC19excludedViewClassesShySSGvpZ", + "moduleName": "Sentry", + "static": true, + "declAttributes": [ + "Final", + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Accessor", + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC19excludedViewClassesShySSGvgZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC19excludedViewClassesShySSGvgZ", + "moduleName": "Sentry", + "static": true, + "implicit": true, + "declAttributes": [ + "Final" + ], + "accessorKind": "get" + } + ] + }, + { + "kind": "Var", + "name": "includedViewClasses", + "printedName": "includedViewClasses", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Var", + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC19includedViewClassesShySSGvpZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC19includedViewClassesShySSGvpZ", + "moduleName": "Sentry", + "static": true, + "declAttributes": [ + "Final", + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" } ], "declKind": "Accessor", - "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC19unmaskedViewClassesSayyXlXpGvgZ", - "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC19unmaskedViewClassesSayyXlXpGvgZ", + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC19includedViewClassesShySSGvgZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC19includedViewClassesShySSGvgZ", "moduleName": "Sentry", "static": true, "implicit": true, @@ -50124,14 +50588,266 @@ ] } ], - "usr": "s:Sa" + "usr": "s:Sa" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(py)unmaskedViewClasses", + "mangledName": "$s6Sentry0A13ReplayOptionsC19unmaskedViewClassesSayyXlXpGvp", + "moduleName": "Sentry", + "objc_name": "unmaskedViewClasses", + "declAttributes": [ + "ObjC", + "HasStorage" + ], + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Array", + "printedName": "[Swift.AnyClass]", + "children": [ + { + "kind": "TypeNameAlias", + "name": "AnyClass", + "printedName": "Swift.AnyClass", + "children": [ + { + "kind": "TypeNominal", + "name": "ExistentialMetatype", + "printedName": "any Swift.AnyObject.Type", + "children": [ + { + "kind": "TypeNameAlias", + "name": "AnyObject", + "printedName": "Swift.AnyObject", + "children": [ + { + "kind": "TypeNameAlias", + "name": "AnyObject", + "printedName": "Builtin.AnyObject", + "children": [ + { + "kind": "TypeNominal", + "name": "ProtocolComposition", + "printedName": "AnyObject" + } + ] + } + ] + } + ] + } + ] + } + ], + "usr": "s:Sa" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)unmaskedViewClasses", + "mangledName": "$s6Sentry0A13ReplayOptionsC19unmaskedViewClassesSayyXlXpGvg", + "moduleName": "Sentry", + "implicit": true, + "objc_name": "unmaskedViewClasses", + "declAttributes": [ + "ObjC" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "Array", + "printedName": "[Swift.AnyClass]", + "children": [ + { + "kind": "TypeNameAlias", + "name": "AnyClass", + "printedName": "Swift.AnyClass", + "children": [ + { + "kind": "TypeNominal", + "name": "ExistentialMetatype", + "printedName": "any Swift.AnyObject.Type", + "children": [ + { + "kind": "TypeNameAlias", + "name": "AnyObject", + "printedName": "Swift.AnyObject", + "children": [ + { + "kind": "TypeNameAlias", + "name": "AnyObject", + "printedName": "Builtin.AnyObject", + "children": [ + { + "kind": "TypeNominal", + "name": "ProtocolComposition", + "printedName": "AnyObject" + } + ] + } + ] + } + ] + } + ] + } + ], + "usr": "s:Sa" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)setUnmaskedViewClasses:", + "mangledName": "$s6Sentry0A13ReplayOptionsC19unmaskedViewClassesSayyXlXpGvs", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "ObjC" + ], + "accessorKind": "set" + } + ] + }, + { + "kind": "Var", + "name": "excludedViewClasses", + "printedName": "excludedViewClasses", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(py)excludedViewClasses", + "mangledName": "$s6Sentry0A13ReplayOptionsC19excludedViewClassesShySSGvp", + "moduleName": "Sentry", + "objc_name": "excludedViewClasses", + "declAttributes": [ + "ObjC", + "HasStorage" + ], + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)excludedViewClasses", + "mangledName": "$s6Sentry0A13ReplayOptionsC19excludedViewClassesShySSGvg", + "moduleName": "Sentry", + "implicit": true, + "objc_name": "excludedViewClasses", + "declAttributes": [ + "ObjC" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)setExcludedViewClasses:", + "mangledName": "$s6Sentry0A13ReplayOptionsC19excludedViewClassesShySSGvs", + "moduleName": "Sentry", + "implicit": true, + "declAttributes": [ + "ObjC" + ], + "accessorKind": "set" + } + ] + }, + { + "kind": "Var", + "name": "includedViewClasses", + "printedName": "includedViewClasses", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" } ], "declKind": "Var", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(py)unmaskedViewClasses", - "mangledName": "$s6Sentry0A13ReplayOptionsC19unmaskedViewClassesSayyXlXpGvp", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(py)includedViewClasses", + "mangledName": "$s6Sentry0A13ReplayOptionsC19includedViewClassesShySSGvp", "moduleName": "Sentry", - "objc_name": "unmaskedViewClasses", + "objc_name": "includedViewClasses", "declAttributes": [ "ObjC", "HasStorage" @@ -50145,52 +50861,25 @@ "children": [ { "kind": "TypeNominal", - "name": "Array", - "printedName": "[Swift.AnyClass]", + "name": "Set", + "printedName": "Swift.Set", "children": [ { - "kind": "TypeNameAlias", - "name": "AnyClass", - "printedName": "Swift.AnyClass", - "children": [ - { - "kind": "TypeNominal", - "name": "ExistentialMetatype", - "printedName": "any Swift.AnyObject.Type", - "children": [ - { - "kind": "TypeNameAlias", - "name": "AnyObject", - "printedName": "Swift.AnyObject", - "children": [ - { - "kind": "TypeNameAlias", - "name": "AnyObject", - "printedName": "Builtin.AnyObject", - "children": [ - { - "kind": "TypeNominal", - "name": "ProtocolComposition", - "printedName": "AnyObject" - } - ] - } - ] - } - ] - } - ] + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" } ], - "usr": "s:Sa" + "usr": "s:Sh" } ], "declKind": "Accessor", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)unmaskedViewClasses", - "mangledName": "$s6Sentry0A13ReplayOptionsC19unmaskedViewClassesSayyXlXpGvg", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)includedViewClasses", + "mangledName": "$s6Sentry0A13ReplayOptionsC19includedViewClassesShySSGvg", "moduleName": "Sentry", "implicit": true, - "objc_name": "unmaskedViewClasses", + "objc_name": "includedViewClasses", "declAttributes": [ "ObjC" ], @@ -50208,49 +50897,22 @@ }, { "kind": "TypeNominal", - "name": "Array", - "printedName": "[Swift.AnyClass]", + "name": "Set", + "printedName": "Swift.Set", "children": [ { - "kind": "TypeNameAlias", - "name": "AnyClass", - "printedName": "Swift.AnyClass", - "children": [ - { - "kind": "TypeNominal", - "name": "ExistentialMetatype", - "printedName": "any Swift.AnyObject.Type", - "children": [ - { - "kind": "TypeNameAlias", - "name": "AnyObject", - "printedName": "Swift.AnyObject", - "children": [ - { - "kind": "TypeNameAlias", - "name": "AnyObject", - "printedName": "Builtin.AnyObject", - "children": [ - { - "kind": "TypeNominal", - "name": "ProtocolComposition", - "printedName": "AnyObject" - } - ] - } - ] - } - ] - } - ] + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" } ], - "usr": "s:Sa" + "usr": "s:Sh" } ], "declKind": "Accessor", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)setUnmaskedViewClasses:", - "mangledName": "$s6Sentry0A13ReplayOptionsC19unmaskedViewClassesSayyXlXpGvs", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)setIncludedViewClasses:", + "mangledName": "$s6Sentry0A13ReplayOptionsC19includedViewClassesShySSGvs", "moduleName": "Sentry", "implicit": true, "declAttributes": [ @@ -50260,6 +50922,58 @@ } ] }, + { + "kind": "Function", + "name": "excludeViewTypeFromSubtreeTraversal", + "printedName": "excludeViewTypeFromSubtreeTraversal(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)excludeViewTypeFromSubtreeTraversal:", + "mangledName": "$s6Sentry0A13ReplayOptionsC35excludeViewTypeFromSubtreeTraversalyySSF", + "moduleName": "Sentry", + "declAttributes": [ + "ObjC" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "includeViewTypeInSubtreeTraversal", + "printedName": "includeViewTypeInSubtreeTraversal(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "declKind": "Func", + "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)includeViewTypeInSubtreeTraversal:", + "mangledName": "$s6Sentry0A13ReplayOptionsC33includeViewTypeInSubtreeTraversalyySSF", + "moduleName": "Sentry", + "declAttributes": [ + "ObjC" + ], + "funcSelfKind": "NonMutating" + }, { "kind": "Var", "name": "enableExperimentalViewRenderer", @@ -56103,6 +56817,132 @@ "accessorKind": "get" } ] + }, + { + "kind": "Var", + "name": "excludedViewClasses", + "printedName": "excludedViewClasses", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(pl)SentryRedactOptions(py)excludedViewClasses", + "mangledName": "$s6Sentry0A13RedactOptionsP19excludedViewClassesShySSGvp", + "moduleName": "Sentry", + "protocolReq": true, + "declAttributes": [ + "ObjC" + ], + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(pl)SentryRedactOptions(im)excludedViewClasses", + "mangledName": "$s6Sentry0A13RedactOptionsP19excludedViewClassesShySSGvg", + "moduleName": "Sentry", + "genericSig": "", + "protocolReq": true, + "declAttributes": [ + "ObjC" + ], + "reqNewWitnessTableEntry": true, + "accessorKind": "get" + } + ] + }, + { + "kind": "Var", + "name": "includedViewClasses", + "printedName": "includedViewClasses", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Var", + "usr": "c:@M@Sentry@objc(pl)SentryRedactOptions(py)includedViewClasses", + "mangledName": "$s6Sentry0A13RedactOptionsP19includedViewClassesShySSGvp", + "moduleName": "Sentry", + "protocolReq": true, + "declAttributes": [ + "ObjC" + ], + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Set", + "printedName": "Swift.Set", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:Sh" + } + ], + "declKind": "Accessor", + "usr": "c:@M@Sentry@objc(pl)SentryRedactOptions(im)includedViewClasses", + "mangledName": "$s6Sentry0A13RedactOptionsP19includedViewClassesShySSGvg", + "moduleName": "Sentry", + "genericSig": "", + "protocolReq": true, + "declAttributes": [ + "ObjC" + ], + "reqNewWitnessTableEntry": true, + "accessorKind": "get" + } + ] } ], "declKind": "Protocol",