From c57613f86641891f256b9a9e188812dc2146c3c4 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 29 Aug 2024 09:26:46 +0200 Subject: [PATCH 01/19] wip --- .../iOS-Swift.xcodeproj/project.pbxproj | 4 + .../iOS-Swift/Base.lproj/Main.storyboard | 84 ++++++++++++++++--- .../UITestViewController.swift | 21 +++++ Sources/Swift/Tools/UIRedactBuilder.swift | 6 +- 4 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj index 63a03cd9d3b..659fc733e68 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj @@ -98,6 +98,7 @@ D8D7BB4A2750067900044146 /* UIAssert.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D7BB492750067900044146 /* UIAssert.swift */; }; D8D7BB4C2750095800044146 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D7BB4B2750095800044146 /* UIViewExtension.swift */; }; D8D7BB4E27501B9400044146 /* SpanObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D7BB4D27501B9400044146 /* SpanObserver.swift */; }; + D8DA29042C7F2199008BC825 /* UITestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DA29032C7F2199008BC825 /* UITestViewController.swift */; }; D8DBDA76274D591F00007380 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DBDA75274D591F00007380 /* TableViewController.swift */; }; D8DBDA78274D5FC400007380 /* SplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DBDA77274D5FC400007380 /* SplitViewController.swift */; }; D8F01DEA2A1376B5008F4996 /* InfoForBreadcrumbController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F01DE92A1376B5008F4996 /* InfoForBreadcrumbController.swift */; }; @@ -354,6 +355,7 @@ D8D7BB492750067900044146 /* UIAssert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAssert.swift; sourceTree = ""; }; D8D7BB4B2750095800044146 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; D8D7BB4D27501B9400044146 /* SpanObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanObserver.swift; sourceTree = ""; }; + D8DA29032C7F2199008BC825 /* UITestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestViewController.swift; sourceTree = ""; }; D8DBDA75274D591F00007380 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; D8DBDA77274D5FC400007380 /* SplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewController.swift; sourceTree = ""; }; D8F01DE92A1376B5008F4996 /* InfoForBreadcrumbController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoForBreadcrumbController.swift; sourceTree = ""; }; @@ -588,6 +590,7 @@ D8832B1D2AF52D0500C522B0 /* PageViewController.swift */, B70038842BB33E7700065A38 /* ReplaceContentViewController.swift */, D8AE48C82C57DC2F0092A2A6 /* WebViewController.swift */, + D8DA29032C7F2199008BC825 /* UITestViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -932,6 +935,7 @@ D8444E4C275E38090042F4DE /* UIViewControllerExtension.swift in Sources */, 637AFDAE243B02760034958B /* TransactionsViewController.swift in Sources */, D8832B132AF4F7FE00C522B0 /* TopViewControllerInspector.swift in Sources */, + D8DA29042C7F2199008BC825 /* UITestViewController.swift in Sources */, 0AABE2EA28855FF80057ED69 /* PermissionsViewController.swift in Sources */, 7B5525B32938B5B5006A2932 /* DiskWriteException.swift in Sources */, D8F3D062274EBD4800B56F8C /* SpanExtension.swift in Sources */, diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index c3745fcac53..7f2b000834d 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -172,7 +172,7 @@ - + @@ -445,7 +445,7 @@ - + @@ -691,7 +691,7 @@ - + @@ -952,7 +952,7 @@ + @@ -1092,6 +1102,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift new file mode 100644 index 00000000000..f3d127ef48a --- /dev/null +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift @@ -0,0 +1,21 @@ +import Foundation + + +class UITestViewController : UIViewController { + + @IBOutlet var transparentView : UIView! + + @IBOutlet var label : UILabel! + + override func viewDidLoad() { + super.viewDidLoad() + + transparentView.backgroundColor = .green + transparentView.transform = CGAffineTransform(rotationAngle: 45 * .pi / 180.0) + } + + @IBAction func showAlert(_ sender: UIButton) { + + } + +} diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 8b1d8d8a852..0fe50528696 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -9,10 +9,12 @@ import WebKit struct RedactRegion { let rect: CGRect + let transform: CGAffineTransform let color: UIColor? - - init(rect: CGRect, color: UIColor? = nil) { + + init(rect: CGRect, transform: CGAffineTransform, color: UIColor?) { self.rect = rect + self.transform = transform self.color = color } From 8459cc9b0ebb628cc7cc445a6735b4c957e41c7c Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 2 Sep 2024 16:22:35 +0200 Subject: [PATCH 02/19] test in progress --- .../iOS-Swift/Base.lproj/Main.storyboard | 17 ++- .../UITestViewController.swift | 13 +- Sentry.xcodeproj/project.pbxproj | 8 +- .../Swift/Tools/SentryViewPhotographer.swift | 24 ++- Sources/Swift/Tools/UIRedactBuilder.swift | 84 +++++------ Tests/SentryTests/RedactRegionTests.swift | 142 ------------------ Tests/SentryTests/SentryScreenShotTests.swift | 2 +- .../SentryViewPhotographerTests.swift | 70 +++++++++ Tests/SentryTests/UIRedactBuilderTests.swift | 22 +-- 9 files changed, 169 insertions(+), 213 deletions(-) delete mode 100644 Tests/SentryTests/RedactRegionTests.swift create mode 100644 Tests/SentryTests/SentryViewPhotographerTests.swift diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index 7f2b000834d..65db895fcba 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -691,7 +691,7 @@ - + @@ -1106,7 +1106,7 @@ - + @@ -1127,8 +1127,17 @@ - + + + + @@ -1234,7 +1243,7 @@ - + diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift index f3d127ef48a..a52a782e440 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift @@ -1,18 +1,23 @@ import Foundation +class TargetView: UIView { + +} -class UITestViewController : UIViewController { +class UITestViewController: UIViewController { - @IBOutlet var transparentView : UIView! + @IBOutlet var transparentView: UIView! - @IBOutlet var label : UILabel! + @IBOutlet var label: UILabel! override func viewDidLoad() { super.viewDidLoad() transparentView.backgroundColor = .green transparentView.transform = CGAffineTransform(rotationAngle: 45 * .pi / 180.0) - } + + SentrySDK.replayIgnore(transparentView) + } @IBAction func showAlert(_ sender: UIButton) { diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 0a6438ab07d..37f076f5385 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -808,6 +808,7 @@ D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */; }; D82859432C3E753C009A28AA /* SentrySessionReplaySyncC.c in Sources */ = {isa = PBXBuildFile; fileRef = D82859422C3E753C009A28AA /* SentrySessionReplaySyncC.c */; }; D82859442C3E753C009A28AA /* SentrySessionReplaySyncC.h in Headers */ = {isa = PBXBuildFile; fileRef = D82859412C3E753C009A28AA /* SentrySessionReplaySyncC.h */; }; + D82915632C85EF0C00A6CDD4 /* SentryViewPhotographerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82915622C85EF0C00A6CDD4 /* SentryViewPhotographerTests.swift */; }; D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D82DD1CD2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82DD1CC2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; @@ -883,7 +884,6 @@ D8AFC01A2BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */; }; D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */; }; D8AFC0572BDA895400118BE1 /* UIRedactBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0562BDA895400118BE1 /* UIRedactBuilder.swift */; }; - D8AFC05A2BDA89C100118BE1 /* RedactRegionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */; }; D8B0542E2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */; }; D8B088B629C9E3FF00213258 /* SentryTracerConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8B088B429C9E3FF00213258 /* SentryTracerConfiguration.h */; }; D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = D8B088B529C9E3FF00213258 /* SentryTracerConfiguration.m */; }; @@ -1871,6 +1871,7 @@ D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; D82859412C3E753C009A28AA /* SentrySessionReplaySyncC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplaySyncC.h; path = include/SentrySessionReplaySyncC.h; sourceTree = ""; }; D82859422C3E753C009A28AA /* SentrySessionReplaySyncC.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SentrySessionReplaySyncC.c; sourceTree = ""; }; + D82915622C85EF0C00A6CDD4 /* SentryViewPhotographerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewPhotographerTests.swift; sourceTree = ""; }; D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderSanitizer.swift; sourceTree = ""; }; D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D82DD1CC2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySRDefaultBreadcrumbConverterTests.swift; sourceTree = ""; }; @@ -1953,7 +1954,6 @@ D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewScreenshotProvider.swift; sourceTree = ""; }; D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayVideoMaker.swift; sourceTree = ""; }; D8AFC0562BDA895400118BE1 /* UIRedactBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIRedactBuilder.swift; sourceTree = ""; }; - D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactRegionTests.swift; sourceTree = ""; }; D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySessionReplayIntegration+Private.h"; path = "include/SentrySessionReplayIntegration+Private.h"; sourceTree = ""; }; D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; D8B088B429C9E3FF00213258 /* SentryTracerConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTracerConfiguration.h; path = include/SentryTracerConfiguration.h; sourceTree = ""; }; @@ -3696,10 +3696,10 @@ D84541192A2DC55100E2B11C /* SentryBinaryImageCache+Private.h */, D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */, D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */, - D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */, D8F67AEF2BE0D31A00C9197B /* UIImageHelperTests.swift */, D8F67AF22BE10F7600C9197B /* UIRedactBuilderTests.swift */, 51B15F7F2BE88D510026A2F2 /* URLSessionTaskHelperTests.swift */, + D82915622C85EF0C00A6CDD4 /* SentryViewPhotographerTests.swift */, ); name = Tools; sourceTree = ""; @@ -4822,6 +4822,7 @@ 63FE722420DA66EC00CDBAE8 /* SentryCrashMonitor_NSException_Tests.m in Sources */, 7B5AB65D27E48E5200F1D1BA /* TestThreadInspector.swift in Sources */, 7BF9EF742722A85B00B5BBEF /* SentryClassRegistrator.m in Sources */, + D82915632C85EF0C00A6CDD4 /* SentryViewPhotographerTests.swift in Sources */, D8DBE0CA2C0E093000FAB1FD /* SentryTouchTrackerTests.swift in Sources */, D8F67AF42BE10F9600C9197B /* UIRedactBuilderTests.swift in Sources */, 63B819141EC352A7002FDF4C /* SentryInterfacesTests.m in Sources */, @@ -4950,7 +4951,6 @@ 62BAD74E2BA1C58D00EBAAFC /* EncodeMetricTests.swift in Sources */, 7BE0DC29272A9E1C004FA8B7 /* SentryBreadcrumbTrackerTests.swift in Sources */, 63FE722520DA66EC00CDBAE8 /* SentryCrashFileUtils_Tests.m in Sources */, - D8AFC05A2BDA89C100118BE1 /* RedactRegionTests.swift in Sources */, D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */, 7BFC16BA2524D4AF00FF6266 /* SentryMessage+Equality.m in Sources */, 7B4260342630315C00B36EDD /* SampleError.swift in Sources */, diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift index 0db478d5317..3b62ae5dee8 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -17,16 +17,34 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { view.drawHierarchy(in: view.bounds, afterScreenUpdates: false) } - let redact = redactBuilder.redactRegionsFor(view: view, options: options) + let redact = redactBuilder.redactRegionsFor(view: view, options: options).reversed() let imageSize = view.bounds.size DispatchQueue.global().async { let screenshot = UIGraphicsImageRenderer(size: imageSize, format: .init(for: .init(displayScale: 1))).image { context in + + context.cgContext.addRect(CGRect(origin: CGPoint.zero, size: imageSize)) + context.cgContext.clip(using: .evenOdd) + context.cgContext.interpolationQuality = .none image.draw(at: .zero) for region in redact { - (region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: region.rect)).setFill() - context.fill(region.rect) + context.cgContext.saveGState() + context.cgContext.concatenate(region.transform) + + let rect = CGRect(origin: CGPoint.zero, size: region.size) + switch region.type { + case .redact: + (region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: rect)).setFill() + context.fill(rect) + context.cgContext.restoreGState() + case .clip: + context.cgContext.addRect(context.cgContext.boundingBoxOfClipPath) + context.cgContext.addRect(rect) + context.cgContext.restoreGState() + context.cgContext.clip(using: .evenOdd) + } + } } onComplete(screenshot) diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 0fe50528696..a9df0adbff2 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -7,50 +7,23 @@ import UIKit import WebKit #endif +enum RedactRegionType { + case clip + case redact +} + struct RedactRegion { - let rect: CGRect + let size: CGSize let transform: CGAffineTransform + let type: RedactRegionType let color: UIColor? - init(rect: CGRect, transform: CGAffineTransform, color: UIColor?) { - self.rect = rect + init(size: CGSize, transform: CGAffineTransform, type: RedactRegionType, color: UIColor? = nil) { + self.size = size self.transform = transform + self.type = type self.color = color } - - func splitBySubtracting(region: CGRect) -> [RedactRegion] { - guard rect.intersects(region) else { return [self] } - guard !region.contains(rect) else { return [] } - - let intersectionRect = rect.intersection(region) - var resultRegions: [CGRect] = [] - - // Calculate the top region. - resultRegions.append(CGRect(x: rect.minX, - y: rect.minY, - width: rect.width, - height: intersectionRect.minY - rect.minY)) - - // Calculate the bottom region. - resultRegions.append(CGRect(x: rect.minX, - y: intersectionRect.maxY, - width: rect.width, - height: rect.maxY - intersectionRect.maxY)) - - // Calculate the left region. - resultRegions.append(CGRect(x: rect.minX, - y: max(rect.minY, intersectionRect.minY), - width: intersectionRect.minX - rect.minX, - height: min(intersectionRect.maxY, rect.maxY) - max(rect.minY, intersectionRect.minY))) - - // Calculate the right region. - resultRegions.append(CGRect(x: intersectionRect.maxX, - y: max(rect.minY, intersectionRect.minY), - width: rect.maxX - intersectionRect.maxX, - height: min(intersectionRect.maxY, rect.maxY) - max(rect.minY, intersectionRect.minY))) - - return resultRegions.filter { !$0.isEmpty }.map { RedactRegion(rect: $0, color: color) } - } } class UIRedactBuilder { @@ -117,11 +90,12 @@ class UIRedactBuilder { redacting: &redactingRegions, area: view.frame, redactText: options?.redactAllText ?? true, - redactImage: options?.redactAllImages ?? true) + redactImage: options?.redactAllImages ?? true, + transform: CGAffineTransform.identity) return redactingRegions } - + private func shouldIgnore(view: UIView) -> Bool { return SentryRedactViewHelper.shouldIgnoreView(view) || containsIgnoreClass(type(of: view)) } @@ -143,31 +117,49 @@ class UIRedactBuilder { return image.imageAsset?.value(forKey: "_containingBundle") == nil } - private func mapRedactRegion(fromView view: UIView, to: CALayer, redacting: inout [RedactRegion], area: CGRect, redactText: Bool, redactImage: Bool) { - let rectInWindow = (view.layer.presentation() ?? view.layer).convert(view.bounds, to: to) - guard (redactImage || redactText) && area.intersects(rectInWindow) && !view.isHidden && view.alpha != 0 else { return } + private func mapRedactRegion(fromView view: UIView, to: CALayer, redacting: inout [RedactRegion], area: CGRect, redactText: Bool, redactImage: Bool, transform: CGAffineTransform) { + guard (redactImage || redactText) && !view.isHidden && view.alpha != 0 else { return } + let layer = view.layer.presentation() ?? view.layer + let size = layer.bounds.size + let layerMiddle = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + + var newTransform = transform.translatedBy(x: layer.position.x, y: layer.position.y) + newTransform = view.transform.concatenating(newTransform) + newTransform = newTransform.translatedBy(x: -layerMiddle.x, y: -layerMiddle.y) let ignore = shouldIgnore(view: view) let redact = shouldRedact(view: view, redactText: redactText, redactImage: redactImage) if !ignore && redact { - redacting.append(RedactRegion(rect: rectInWindow, color: self.color(for: view))) + redacting.append(RedactRegion(size: size, transform: newTransform, type: .redact, color: self.color(for: view))) return } else if hasBackground(view) { - if rectInWindow == area { + if layerOriginalFrame(layer: layer) == area { redacting.removeAll() } else { - redacting = redacting.flatMap { $0.splitBySubtracting(region: rectInWindow) } + redacting.append(RedactRegion(size: size, transform: newTransform, type: .clip)) } } if !ignore { for subview in view.subviews { - mapRedactRegion(fromView: subview, to: to, redacting: &redacting, area: area, redactText: redactText, redactImage: redactImage) + mapRedactRegion(fromView: subview, to: to, redacting: &redacting, area: area, redactText: redactText, redactImage: redactImage, transform: newTransform) } } } + private func layerOriginalFrame(layer: CALayer) -> CGRect { + let originalCenter = layer.position + let originalBounds = layer.bounds + + return CGRect( + x: originalCenter.x - originalBounds.width / 2, + y: originalCenter.y - originalBounds.height / 2, + width: originalBounds.width, + height: originalBounds.height + ) + } + private func color(for view: UIView) -> UIColor? { return (view as? UILabel)?.textColor } diff --git a/Tests/SentryTests/RedactRegionTests.swift b/Tests/SentryTests/RedactRegionTests.swift deleted file mode 100644 index 63a37e4297d..00000000000 --- a/Tests/SentryTests/RedactRegionTests.swift +++ /dev/null @@ -1,142 +0,0 @@ -import Foundation -@testable import Sentry -import XCTest -#if os(iOS) || os(tvOS) -class RedactRegionTests: XCTestCase { - - func testSplitBySubtractingBottom() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) - - let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 50, width: 100, height: 50)) - - XCTAssertEqual(result.count, 1) - XCTAssertEqual(result.first?.rect, CGRect(x: 0, y: 0, width: 100, height: 50)) - XCTAssertEqual(result.first?.color, .red) - } - - func testSplitBySubtractingTop() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) - - let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 100, height: 50)) - - XCTAssertEqual(result.count, 1) - XCTAssertEqual(result.first?.rect, CGRect(x: 0, y: 50, width: 100, height: 50)) - } - - func testSplitBySubtractingTopRight() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) - - let result = sut.splitBySubtracting(region: CGRect(x: 50, y: 0, width: 50, height: 50)) - - XCTAssertEqual(result.count, 2) - XCTAssertEqual(result.first?.rect, CGRect(x: 0, y: 50, width: 100, height: 50)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 1)).rect, CGRect(x: 0, y: 0, width: 50, height: 50)) - } - - func testSplitBySubtractingBottomLeft() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) - - let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 50, width: 50, height: 50)) - - XCTAssertEqual(result.count, 2) - XCTAssertEqual(result.first?.rect, CGRect(x: 0, y: 0, width: 100, height: 50)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 1)).rect, CGRect(x: 50, y: 50, width: 50, height: 50)) - } - - func testSplitBySubtractingMiddle() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) - - let result = sut.splitBySubtracting(region: CGRect(x: 25, y: 25, width: 50, height: 50)) - - XCTAssertEqual(result.count, 4) - XCTAssertEqual(try XCTUnwrap(result.first).rect, CGRect(x: 0, y: 0, width: 100, height: 25)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 1)).rect, CGRect(x: 0, y: 75, width: 100, height: 25)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 2)).rect, CGRect(x: 0, y: 25, width: 25, height: 50)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 3)).rect, CGRect(x: 75, y: 25, width: 25, height: 50)) - } - - func testSplitBySubtractingInHalfHorizontally() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) - - let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 25, width: 100, height: 50)) - - XCTAssertEqual(result.count, 2) - XCTAssertEqual(try XCTUnwrap(result.first).rect, CGRect(x: 0, y: 0, width: 100, height: 25)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 1)).rect, CGRect(x: 0, y: 75, width: 100, height: 25)) - } - - func testSplitBySubtractingInHalfVertically() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) - - let result = sut.splitBySubtracting(region: CGRect(x: 25, y: 0, width: 50, height: 100)) - - XCTAssertEqual(result.count, 2) - XCTAssertEqual(try XCTUnwrap(result.first).rect, CGRect(x: 0, y: 0, width: 25, height: 100)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 1)).rect, CGRect(x: 75, y: 0, width: 25, height: 100)) - } - - func testSplitBySubtractingMiddleRight() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100)) - - let result = sut.splitBySubtracting(region: CGRect(x: 25, y: 25, width: 100, height: 50)) - - XCTAssertEqual(result.count, 3) - XCTAssertEqual(try XCTUnwrap(result.first).rect, CGRect(x: 0, y: 0, width: 100, height: 25)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 1)).rect, CGRect(x: 0, y: 75, width: 100, height: 25)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 2)).rect, CGRect(x: 0, y: 25, width: 25, height: 50)) - } - - func testSplitBySubtractingMiddleLeft() { - let sut = RedactRegion(rect: CGRect(x: 50, y: 0, width: 100, height: 100)) - - let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 25, width: 100, height: 50)) - - XCTAssertEqual(result.count, 3) - XCTAssertEqual(try XCTUnwrap(result.first).rect, CGRect(x: 50, y: 0, width: 100, height: 25)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 1)).rect, CGRect(x: 50, y: 75, width: 100, height: 25)) - XCTAssertEqual(try XCTUnwrap(result.element(at: 2)).rect, CGRect(x: 100, y: 25, width: 50, height: 50)) - } - - func testSplitBySubtracting_TopIsWider() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) - let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 150, height: 50)) - - XCTAssertEqual(result.count, 1) - XCTAssertEqual(result.first?.rect, CGRect(x: 0, y: 50, width: 100, height: 50)) - XCTAssertEqual(result.first?.color, .red) - } - - func testSplitBySubtracting_BottomIsWider() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) - let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 50, width: 150, height: 50)) - - XCTAssertEqual(result.count, 1) - XCTAssertEqual(result.first?.rect, CGRect(x: 0, y: 0, width: 100, height: 50)) - XCTAssertEqual(result.first?.color, .red) - } - - func testNoResultForEqualRegion() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) - let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 100, height: 100)) - - XCTAssertEqual(result.count, 0) - } - - func testNoResultForLargerRegion() { - let sut = RedactRegion(rect: CGRect(x: 50, y: 50, width: 100, height: 100), color: .red) - let result = sut.splitBySubtracting(region: CGRect(x: 0, y: 0, width: 200, height: 200)) - - XCTAssertEqual(result.count, 0) - } - - func testSameRegionForOutsideOfBounds() { - let sut = RedactRegion(rect: CGRect(x: 0, y: 0, width: 100, height: 100), color: .red) - let result = sut.splitBySubtracting(region: CGRect(x: 110, y: 110, width: 200, height: 200)) - - XCTAssertEqual(result.count, 1) - XCTAssertEqual(result.first?.rect, sut.rect) - XCTAssertEqual(result.first?.color, .red) - } - -} -#endif diff --git a/Tests/SentryTests/SentryScreenShotTests.swift b/Tests/SentryTests/SentryScreenShotTests.swift index 924da3f2b04..211bd6b3535 100644 --- a/Tests/SentryTests/SentryScreenShotTests.swift +++ b/Tests/SentryTests/SentryScreenShotTests.swift @@ -122,7 +122,7 @@ class SentryScreenShotTests: XCTestCase { class TestWindow: UIWindow { var onDrawHierarchy: (() -> Void)? - override func drawHierarchy(in rect: CGRect, afterScreenUpdates afterUpdates: Bool) -> Bool { + override func drawHierarchy(in size: CGRect, afterScreenUpdates afterUpdates: Bool) -> Bool { onDrawHierarchy?() return true } diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift new file mode 100644 index 00000000000..b01c3491bfa --- /dev/null +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -0,0 +1,70 @@ +#if os(iOS) +import Foundation +@testable import Sentry +import SentryTestUtils +import UIKit +import XCTest + +class SentryViewPhotographerTests : XCTestCase { + + private class RedactOptions : SentryRedactOptions { + var redactAllText: Bool + var redactAllImages: Bool + + init(redactAllText: Bool = true, redactAllImages: Bool = true) { + self.redactAllText = redactAllText + self.redactAllImages = redactAllImages + } + } + + private func prepare(views: [UIView], options: any SentryRedactOptions = RedactOptions()) -> UIImage? { + let rootView = UIView(frame: CGRect(x:0, y: 0, width: 50, height: 50)) + rootView.backgroundColor = .white + views.forEach(rootView.addSubview(_:)) + + let sut = SentryViewPhotographer() + let expect = expectation(description: "Image rendered") + var result : UIImage? = nil + + sut.image(view: rootView, options: options) { image in + result = image + expect.fulfill() + } + + wait(for: [expect], timeout: 1) + return result + } + + + func testLabelRedacted() throws { + let image = try XCTUnwrap(prepare(views: [UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30))])) + let pixel = color(at: CGPoint(x: 10, y: 10), in: image) + + XCTAssertEqual(pixel, UIColor.black) + } + + + + private func color(at point: CGPoint, in image: UIImage) -> UIColor? { + guard let cgImage = image.cgImage, + let dataProvider = cgImage.dataProvider, + let pixelData = dataProvider.data else { + return nil + } + + let data: UnsafePointer = CFDataGetBytePtr(pixelData) + + let bytesPerPixel = 4 + let bytesPerRow = cgImage.bytesPerRow + let pixelOffset = Int(point.y) * bytesPerRow + Int(point.x) * bytesPerPixel + + let red = CGFloat(data[pixelOffset]) / 255.0 + let green = CGFloat(data[pixelOffset + 1]) / 255.0 + let blue = CGFloat(data[pixelOffset + 2]) / 255.0 + let alpha = CGFloat(data[pixelOffset + 3]) / 255.0 + + return UIColor(red: red, green: green, blue: blue, alpha: alpha) + } +} + +#endif diff --git a/Tests/SentryTests/UIRedactBuilderTests.swift b/Tests/SentryTests/UIRedactBuilderTests.swift index ca3812a64d0..903b8c71fa9 100644 --- a/Tests/SentryTests/UIRedactBuilderTests.swift +++ b/Tests/SentryTests/UIRedactBuilderTests.swift @@ -38,7 +38,9 @@ class UIRedactBuilderTests: XCTestCase { XCTAssertEqual(result.count, 1) XCTAssertEqual(result.first?.color, .purple) - XCTAssertEqual(result.first?.rect, CGRect(x: 20, y: 20, width: 40, height: 40)) + XCTAssertEqual(result.first?.size, CGSize(width: 40, height: 40)) + XCTAssertEqual(result.first?.type, .redact) + XCTAssertEqual(result.first?.transform, CGAffineTransform(1, 0, 0, 1, 20, 20)) } func testDontRedactALabelOptionDisabled() { @@ -67,7 +69,7 @@ class UIRedactBuilderTests: XCTestCase { XCTAssertEqual(result.count, 1) XCTAssertNil(result.first?.color) - XCTAssertEqual(result.first?.rect, CGRect(x: 20, y: 20, width: 40, height: 40)) + XCTAssertEqual(result.first?.size, CGSize(width: 40, height: 40)) } func testDontRedactAImageOptionDisabled() { @@ -123,15 +125,17 @@ class UIRedactBuilderTests: XCTestCase { XCTAssertEqual(result.count, 0) } - func testDontRedactALabelBehindAOpaqueView() { + func testClipForOpaqueView() { + let opaqueView = UIView(frame: CGRect(x: 10, y: 10, width: 60, height: 60)) + opaqueView.backgroundColor = .white + rootView.addSubview(opaqueView) + let sut = UIRedactBuilder() - let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) - rootView.addSubview(label) - let topView = UIView(frame: CGRect(x: 10, y: 10, width: 60, height: 60)) - topView.backgroundColor = .white - rootView.addSubview(topView) let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) - XCTAssertEqual(result.count, 0) + + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.type, .clip) + XCTAssertEqual(result.first?.transform, CGAffineTransform(1, 0, 0, 1, 10, 10)) } func testRedactALabelBehindATransparentView() { From 364cb778b67f4011fd49997090650f261a411836 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 3 Sep 2024 14:18:47 +0200 Subject: [PATCH 03/19] fix: Session Replay redact for transformed views --- .../Swift/Tools/SentryViewPhotographer.swift | 31 ++++- Sources/Swift/Tools/UIRedactBuilder.swift | 2 +- .../SentryViewPhotographerTests.swift | 130 ++++++++++++++++-- 3 files changed, 142 insertions(+), 21 deletions(-) diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift index 3b62ae5dee8..ea3b279749a 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -5,17 +5,37 @@ import CoreGraphics import Foundation import UIKit +protocol ViewRenderer { + func render(view: UIView) -> UIImage +} + +class DefaultViewRenderer: ViewRenderer { + func render(view: UIView) -> UIImage { + let image = UIGraphicsImageRenderer(size: view.bounds.size).image { _ in + view.drawHierarchy(in: view.bounds, afterScreenUpdates: false) + } + return image + } +} + @objcMembers class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { - static let shared = SentryViewPhotographer() - private let redactBuilder = UIRedactBuilder() + + var renderer: ViewRenderer + + init(renderer: ViewRenderer) { + self.renderer = renderer + super.init() + } + + private convenience override init() { + self.init(renderer: DefaultViewRenderer()) + } func image(view: UIView, options: SentryRedactOptions, onComplete: @escaping ScreenshotCallback ) { - let image = UIGraphicsImageRenderer(size: view.bounds.size).image { _ in - view.drawHierarchy(in: view.bounds, afterScreenUpdates: false) - } + let image = renderer.render(view: view) let redact = redactBuilder.redactRegionsFor(view: view, options: options).reversed() let imageSize = view.bounds.size @@ -44,7 +64,6 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { context.cgContext.restoreGState() context.cgContext.clip(using: .evenOdd) } - } } onComplete(screenshot) diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index a9df0adbff2..ee809aaf3eb 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -166,7 +166,7 @@ class UIRedactBuilder { private func hasBackground(_ view: UIView) -> Bool { //Anything with an alpha greater than 0.9 is opaque enough that it's impossible to see anything behind it. - return view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9 + return view.backgroundColor != nil && view.alpha > 0.9 && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9 } } diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift index b01c3491bfa..a324d214172 100644 --- a/Tests/SentryTests/SentryViewPhotographerTests.swift +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -5,9 +5,17 @@ import SentryTestUtils import UIKit import XCTest -class SentryViewPhotographerTests : XCTestCase { +class SentryViewPhotographerTests: XCTestCase { - private class RedactOptions : SentryRedactOptions { + private class TestViewRenderer: ViewRenderer { + func render(view: UIView) -> UIImage { + UIGraphicsImageRenderer(size: view.bounds.size).image { context in + view.layer.render(in: context.cgContext) + } + } + } + + private class RedactOptions: SentryRedactOptions { var redactAllText: Bool var redactAllImages: Bool @@ -17,15 +25,19 @@ class SentryViewPhotographerTests : XCTestCase { } } + func sut() -> SentryViewPhotographer { + return SentryViewPhotographer(renderer: TestViewRenderer()) + } + private func prepare(views: [UIView], options: any SentryRedactOptions = RedactOptions()) -> UIImage? { - let rootView = UIView(frame: CGRect(x:0, y: 0, width: 50, height: 50)) + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) rootView.backgroundColor = .white views.forEach(rootView.addSubview(_:)) - let sut = SentryViewPhotographer() + let sut = sut() let expect = expectation(description: "Image rendered") - var result : UIImage? = nil - + var result: UIImage? + sut.image(view: rootView, options: options) { image in result = image expect.fulfill() @@ -35,21 +47,111 @@ class SentryViewPhotographerTests : XCTestCase { return result } - func testLabelRedacted() throws { - let image = try XCTUnwrap(prepare(views: [UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30))])) + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) + label.text = "Test" + + let image = try XCTUnwrap(prepare(views: [label])) let pixel = color(at: CGPoint(x: 10, y: 10), in: image) - XCTAssertEqual(pixel, UIColor.black) + assertColor(pixel, .black) } - + func testLabelNotRedactedWithOpaqueViewOnTop() throws { + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) + label.text = "Test" + let viewOnTop = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) + viewOnTop.backgroundColor = .red + + let image = try XCTUnwrap(prepare(views: [label, viewOnTop])) + let pixel = color(at: CGPoint(x: 10, y: 10), in: image) + + assertColor(pixel, .red) + } + + func testLabelNotRedactedWithTwoOpaqueViewsOnTop() throws { + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) + label.text = "Test" + let viewOnTop1 = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) + viewOnTop1.backgroundColor = .red + + let viewOnTop2 = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) + viewOnTop2.backgroundColor = .blue + + let image = try XCTUnwrap(prepare(views: [label, viewOnTop1, viewOnTop2])) + let pixel = color(at: CGPoint(x: 10, y: 10), in: image) + + assertColor(pixel, .blue) + } + + func testLabelRedactedWithNonOpaqueViewOnTop() throws { + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) + label.text = "Test" + let viewOnTop = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) + viewOnTop.backgroundColor = .red + viewOnTop.alpha = 0.5 + + let image = try XCTUnwrap(prepare(views: [label, viewOnTop])) + let pixel = color(at: CGPoint(x: 10, y: 10), in: image) + + assertColor(pixel, .black) + } + + func testLabelRedactedWithViewOnTopTransparentBackground() throws { + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) + label.text = "Test" + let viewOnTop = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) + viewOnTop.backgroundColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 0.8) + + let image = try XCTUnwrap(prepare(views: [label, viewOnTop])) + let pixel = color(at: CGPoint(x: 10, y: 10), in: image) + + assertColor(pixel, .black) + } + + func testClipPartOfLabel() throws { + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 30)) + label.text = "Test" + let viewOnTop = UIView(frame: CGRect(x: 20, y: 0, width: 20, height: 50)) + viewOnTop.backgroundColor = .red + + let image = try XCTUnwrap(prepare(views: [label, viewOnTop])) + let pixel1 = color(at: CGPoint(x: 10, y: 10), in: image) + + assertColor(pixel1, .black) + + let pixel2 = color(at: CGPoint(x: 22, y: 10), in: image) + assertColor(pixel2, .red) + } + + func testClipPartOfLabelTopTransformed() throws { + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 30)) + label.text = "Test" + let viewOnTop = UIView(frame: CGRect(x: 0, y: 15, width: 50, height: 20)) + viewOnTop.backgroundColor = .red + viewOnTop.transform = CGAffineTransform(rotationAngle: 90 * .pi / 180.0) + + let image = try XCTUnwrap(prepare(views: [label, viewOnTop])) + let pixel1 = color(at: CGPoint(x: 10, y: 10), in: image) + + assertColor(pixel1, .black) + + let pixel2 = color(at: CGPoint(x: 22, y: 10), in: image) + assertColor(pixel2, .red) + } + + private func assertColor(_ color1: UIColor, _ color2: UIColor) { + let sRGBColor1 = color1.cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil) + let sRGBColor2 = color2.cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil) + + XCTAssertEqual(sRGBColor1, sRGBColor2) + } - private func color(at point: CGPoint, in image: UIImage) -> UIColor? { + private func color(at point: CGPoint, in image: UIImage) -> UIColor { guard let cgImage = image.cgImage, let dataProvider = cgImage.dataProvider, let pixelData = dataProvider.data else { - return nil + return .clear } let data: UnsafePointer = CFDataGetBytePtr(pixelData) @@ -58,9 +160,9 @@ class SentryViewPhotographerTests : XCTestCase { let bytesPerRow = cgImage.bytesPerRow let pixelOffset = Int(point.y) * bytesPerRow + Int(point.x) * bytesPerPixel - let red = CGFloat(data[pixelOffset]) / 255.0 + let blue = CGFloat(data[pixelOffset]) / 255.0 let green = CGFloat(data[pixelOffset + 1]) / 255.0 - let blue = CGFloat(data[pixelOffset + 2]) / 255.0 + let red = CGFloat(data[pixelOffset + 2]) / 255.0 let alpha = CGFloat(data[pixelOffset + 3]) / 255.0 return UIColor(red: red, green: green, blue: blue, alpha: alpha) From 0bcb7dfe355b18c1354d41382a36f1141d060c16 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 3 Sep 2024 14:25:57 +0200 Subject: [PATCH 04/19] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9328948553..1a26ba5278f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Resumes replay when the app becomes active (#4303) +- Session replay redact view with transformation (#4308) ## 8.36.0 From 15f94cd4b5cc3d587ee23f4320d078b2ab1e308a Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 3 Sep 2024 14:40:12 +0200 Subject: [PATCH 05/19] ref --- Tests/SentryTests/SentryViewPhotographerTests.swift | 2 +- Tests/SentryTests/UIRedactBuilderTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift index a324d214172..aabaa9cedeb 100644 --- a/Tests/SentryTests/SentryViewPhotographerTests.swift +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -1,4 +1,4 @@ -#if os(iOS) +#if os(iOS) && !targetEnvironment(macCatalyst) import Foundation @testable import Sentry import SentryTestUtils diff --git a/Tests/SentryTests/UIRedactBuilderTests.swift b/Tests/SentryTests/UIRedactBuilderTests.swift index 903b8c71fa9..5e30e59319f 100644 --- a/Tests/SentryTests/UIRedactBuilderTests.swift +++ b/Tests/SentryTests/UIRedactBuilderTests.swift @@ -40,7 +40,7 @@ class UIRedactBuilderTests: XCTestCase { XCTAssertEqual(result.first?.color, .purple) XCTAssertEqual(result.first?.size, CGSize(width: 40, height: 40)) XCTAssertEqual(result.first?.type, .redact) - XCTAssertEqual(result.first?.transform, CGAffineTransform(1, 0, 0, 1, 20, 20)) + XCTAssertEqual(result.first?.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20)) } func testDontRedactALabelOptionDisabled() { @@ -135,7 +135,7 @@ class UIRedactBuilderTests: XCTestCase { XCTAssertEqual(result.count, 1) XCTAssertEqual(result.first?.type, .clip) - XCTAssertEqual(result.first?.transform, CGAffineTransform(1, 0, 0, 1, 10, 10)) + XCTAssertEqual(result.first?.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 10, ty: 10)) } func testRedactALabelBehindATransparentView() { From c1fa6935c0aa0c7d01ec828c0904c2e46339a3ce Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 3 Sep 2024 15:11:11 +0200 Subject: [PATCH 06/19] Update SentryViewPhotographerTests.swift --- .../SentryViewPhotographerTests.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift index aabaa9cedeb..041e588f357 100644 --- a/Tests/SentryTests/SentryViewPhotographerTests.swift +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -61,12 +61,12 @@ class SentryViewPhotographerTests: XCTestCase { let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) label.text = "Test" let viewOnTop = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) - viewOnTop.backgroundColor = .red + viewOnTop.backgroundColor = .green let image = try XCTUnwrap(prepare(views: [label, viewOnTop])) let pixel = color(at: CGPoint(x: 10, y: 10), in: image) - assertColor(pixel, .red) + assertColor(pixel, .green) } func testLabelNotRedactedWithTwoOpaqueViewsOnTop() throws { @@ -76,12 +76,12 @@ class SentryViewPhotographerTests: XCTestCase { viewOnTop1.backgroundColor = .red let viewOnTop2 = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) - viewOnTop2.backgroundColor = .blue + viewOnTop2.backgroundColor = .green let image = try XCTUnwrap(prepare(views: [label, viewOnTop1, viewOnTop2])) let pixel = color(at: CGPoint(x: 10, y: 10), in: image) - assertColor(pixel, .blue) + assertColor(pixel, .green) } func testLabelRedactedWithNonOpaqueViewOnTop() throws { @@ -113,7 +113,7 @@ class SentryViewPhotographerTests: XCTestCase { let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 30)) label.text = "Test" let viewOnTop = UIView(frame: CGRect(x: 20, y: 0, width: 20, height: 50)) - viewOnTop.backgroundColor = .red + viewOnTop.backgroundColor = .green let image = try XCTUnwrap(prepare(views: [label, viewOnTop])) let pixel1 = color(at: CGPoint(x: 10, y: 10), in: image) @@ -121,14 +121,14 @@ class SentryViewPhotographerTests: XCTestCase { assertColor(pixel1, .black) let pixel2 = color(at: CGPoint(x: 22, y: 10), in: image) - assertColor(pixel2, .red) + assertColor(pixel2, .green) } func testClipPartOfLabelTopTransformed() throws { let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 30)) label.text = "Test" let viewOnTop = UIView(frame: CGRect(x: 0, y: 15, width: 50, height: 20)) - viewOnTop.backgroundColor = .red + viewOnTop.backgroundColor = .green viewOnTop.transform = CGAffineTransform(rotationAngle: 90 * .pi / 180.0) let image = try XCTUnwrap(prepare(views: [label, viewOnTop])) @@ -137,7 +137,7 @@ class SentryViewPhotographerTests: XCTestCase { assertColor(pixel1, .black) let pixel2 = color(at: CGPoint(x: 22, y: 10), in: image) - assertColor(pixel2, .red) + assertColor(pixel2, .green) } private func assertColor(_ color1: UIColor, _ color2: UIColor) { @@ -160,9 +160,9 @@ class SentryViewPhotographerTests: XCTestCase { let bytesPerRow = cgImage.bytesPerRow let pixelOffset = Int(point.y) * bytesPerRow + Int(point.x) * bytesPerPixel - let blue = CGFloat(data[pixelOffset]) / 255.0 + let red = CGFloat(data[pixelOffset]) / 255.0 let green = CGFloat(data[pixelOffset + 1]) / 255.0 - let red = CGFloat(data[pixelOffset + 2]) / 255.0 + let blue = CGFloat(data[pixelOffset + 2]) / 255.0 let alpha = CGFloat(data[pixelOffset + 3]) / 255.0 return UIColor(red: red, green: green, blue: blue, alpha: alpha) From ffdf298883c3a3c812e79f1cd29439a4d3ec7c18 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 3 Sep 2024 15:53:50 +0200 Subject: [PATCH 07/19] Update Main.storyboard --- Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard | 1 - 1 file changed, 1 deletion(-) diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index 65db895fcba..03234cea810 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -1016,7 +1016,6 @@ - + - - - - - - - - - - - + @@ -1264,7 +1237,7 @@ - + diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift new file mode 100644 index 00000000000..c506bca4190 --- /dev/null +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift @@ -0,0 +1,17 @@ +import Foundation + +class SRRedactSampleViewController: UIViewController { + + @IBOutlet var notRedactedView: UIView! + + @IBOutlet var label: UILabel! + + override func viewDidLoad() { + super.viewDidLoad() + + notRedactedView.backgroundColor = .green + notRedactedView.transform = CGAffineTransform(rotationAngle: 45 * .pi / 180.0) + + SentrySDK.replayIgnore(notRedactedView) + } +} diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift deleted file mode 100644 index a52a782e440..00000000000 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/UITestViewController.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation - -class TargetView: UIView { - -} - -class UITestViewController: UIViewController { - - @IBOutlet var transparentView: UIView! - - @IBOutlet var label: UILabel! - - override func viewDidLoad() { - super.viewDidLoad() - - transparentView.backgroundColor = .green - transparentView.transform = CGAffineTransform(rotationAngle: 45 * .pi / 180.0) - - SentrySDK.replayIgnore(transparentView) - } - - @IBAction func showAlert(_ sender: UIButton) { - - } - -} From ee68606a04fff4d4b0b914c7665e4c69713c54fa Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 11 Sep 2024 13:27:10 +0200 Subject: [PATCH 14/19] Update UIRedactBuilder.swift --- Sources/Swift/Tools/UIRedactBuilder.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 8f9001dc61e..7c878dbd6e8 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -183,8 +183,7 @@ class UIRedactBuilder { Indicates whether the view is opaque and will block other view behind it */ private func isOpaque(_ view: UIView) -> Bool { - //Anything with an alpha greater than 0.9 is opaque enough that it's impossible to see anything behind it. - return view.alpha > 0.9 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9 + return view.alpha == 1 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1 } } From 0b3d9161a9172747789f2e952f1973a7b15f4669 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 12 Sep 2024 09:15:22 +0200 Subject: [PATCH 15/19] Update UIRedactBuilder.swift --- Sources/Swift/Tools/UIRedactBuilder.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 7c878dbd6e8..9e64a2eebf6 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -150,7 +150,9 @@ class UIRedactBuilder { if !ignore && redact { redacting.append(RedactRegion(size: size, transform: newTransform, type: .redact, color: self.color(for: view))) return - } else if isOpaque(view) { + } + + if isOpaque(view) { let finalViewFrame = CGRect(origin: .zero, size: size).applying(newTransform) if isAxisAligned(newTransform) && finalViewFrame == rootFrame { //Because the current view is covering everything we found so far we can clear `redacting` list From 35bb1e75f74e393567d00c80c1f21e3da2b2c826 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 12 Sep 2024 09:52:47 +0200 Subject: [PATCH 16/19] Update SRRedactSampleViewController.swift --- .../ViewControllers/SRRedactSampleViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift index c506bca4190..e7a23a5e850 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift @@ -12,6 +12,6 @@ class SRRedactSampleViewController: UIViewController { notRedactedView.backgroundColor = .green notRedactedView.transform = CGAffineTransform(rotationAngle: 45 * .pi / 180.0) - SentrySDK.replayIgnore(notRedactedView) + SentrySDK.replay.ignoreView(notRedactedView) } } From cf60b5eb52a5978f2e073b245a021e3c0e3b1f62 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 13 Sep 2024 15:56:41 +0200 Subject: [PATCH 17/19] fix: Don't redact clipped views (#4325) A label that is outside of bounds of a view with clipBounds true is no longer being redacted. --- CHANGELOG.md | 1 + .../iOS-Swift/Base.lproj/Main.storyboard | 25 ++++++++- .../Swift/Tools/SentryViewPhotographer.swift | 25 ++++++--- Sources/Swift/Tools/UIRedactBuilder.swift | 56 ++++++++++++++----- .../SentryViewPhotographerTests.swift | 36 ++++++++++++ Tests/SentryTests/UIRedactBuilderTests.swift | 2 +- 6 files changed, 119 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d151e53a408..81714923d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Resumes replay when the app becomes active (#4303) - Session replay redact view with transformation (#4308) +- Don't redact clipped views (#4325) - Double-quoted include, expected angle-bracketed instead (#4298) ## 8.36.0 diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index f163b2b6542..e22411a583e 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -1139,6 +1139,27 @@ + + + + + + + + + diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift index a0b539c79bb..d22708f618b 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -46,25 +46,32 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { context.cgContext.addRect(CGRect(origin: CGPoint.zero, size: imageSize)) context.cgContext.clip(using: .evenOdd) + UIColor.blue.setStroke() context.cgContext.interpolationQuality = .none image.draw(at: .zero) for region in redact { - context.cgContext.saveGState() - context.cgContext.concatenate(region.transform) - let rect = CGRect(origin: CGPoint.zero, size: region.size) + var transform = region.transform + let path = CGPath(rect: rect, transform: &transform) + switch region.type { case .redact: - (region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: rect)).setFill() - context.fill(rect) - context.cgContext.restoreGState() - case .clip: + (region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: rect.applying(region.transform))).setFill() + context.cgContext.addPath(path) + context.cgContext.fillPath() + case .clipOut: context.cgContext.addRect(context.cgContext.boundingBoxOfClipPath) - context.cgContext.addRect(rect) - context.cgContext.restoreGState() + context.cgContext.addPath(path) context.cgContext.clip(using: .evenOdd) + case .clipBegin: + context.cgContext.saveGState() + context.cgContext.resetClip() + context.cgContext.addPath(path) + context.cgContext.clip() + case .clipEnd: + context.cgContext.restoreGState() } } } diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 9e64a2eebf6..f7a8451e26b 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -8,8 +8,20 @@ import WebKit #endif enum RedactRegionType { - case clip + /// Redacts the region. case redact + + /// Marks a region to not draw anything. + /// This is used for opaque views. + case clipOut + + /// Push a clip region to the drawing context. + /// This is used for views that clip to its bounds. + case clipBegin + + /// Pop the last Pushed region from the drawing context. + /// Used after prossing every child of a view that clip to its bounds. + case clipEnd } struct RedactRegion { @@ -137,42 +149,58 @@ class UIRedactBuilder { guard (redactOptions.redactAllImages || redactOptions.redactAllText) && !view.isHidden && view.alpha != 0 else { return } let layer = view.layer.presentation() ?? view.layer - let size = layer.bounds.size - let layerMiddle = CGPoint(x: size.width * layer.anchorPoint.x, y: size.height * layer.anchorPoint.y) - var newTransform = transform.translatedBy(x: layer.position.x, y: layer.position.y) - newTransform = view.transform.concatenating(newTransform) - newTransform = newTransform.translatedBy(x: -layerMiddle.x, y: -layerMiddle.y) + let newTransform = concatenateTranform(transform, with: layer) let ignore = shouldIgnore(view: view) let redact = shouldRedact(view: view, redactOptions: redactOptions) if !ignore && redact { - redacting.append(RedactRegion(size: size, transform: newTransform, type: .redact, color: self.color(for: view))) + redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .redact, color: self.color(for: view))) return } if isOpaque(view) { - let finalViewFrame = CGRect(origin: .zero, size: size).applying(newTransform) + let finalViewFrame = CGRect(origin: .zero, size: layer.bounds.size).applying(newTransform) if isAxisAligned(newTransform) && finalViewFrame == rootFrame { //Because the current view is covering everything we found so far we can clear `redacting` list redacting.removeAll() } else { - redacting.append(RedactRegion(size: size, transform: newTransform, type: .clip)) + redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .clipOut)) } } - if !ignore { - for subview in view.subviews { - mapRedactRegion(fromView: subview, redacting: &redacting, rootFrame: rootFrame, redactOptions: redactOptions, transform: newTransform) - } + guard !ignore else { return } + + if view.clipsToBounds { + /// Because the order in which we process the redacted regions is reversed, we add the end of the clip region first. + /// The beginning will be added after all the subviews have been mapped. + redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .clipEnd)) + } + for subview in view.subviews { + mapRedactRegion(fromView: subview, redacting: &redacting, rootFrame: rootFrame, redactOptions: redactOptions, transform: newTransform) } + if view.clipsToBounds { + redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .clipBegin)) + } + } + + /** + Apply the layer transformation and position to given transformation. + */ + private func concatenateTranform(_ transform: CGAffineTransform, with layer: CALayer) -> CGAffineTransform { + let size = layer.bounds.size + let layerMiddle = CGPoint(x: size.width * layer.anchorPoint.x, y: size.height * layer.anchorPoint.y) + + var newTransform = transform.translatedBy(x: layer.position.x, y: layer.position.y) + newTransform = CATransform3DGetAffineTransform(layer.transform).concatenating(newTransform) + return newTransform.translatedBy(x: -layerMiddle.x, y: -layerMiddle.y) } /** Whether the transform does not contains rotation or skew */ - func isAxisAligned(_ transform: CGAffineTransform) -> Bool { + private func isAxisAligned(_ transform: CGAffineTransform) -> Bool { // Rotation exists if b or c are not zero return transform.b == 0 && transform.c == 0 } diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift index 041e588f357..df2ff39aa37 100644 --- a/Tests/SentryTests/SentryViewPhotographerTests.swift +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -140,6 +140,42 @@ class SentryViewPhotographerTests: XCTestCase { assertColor(pixel2, .green) } + func testDontRedactClippedLabel() throws { + let label = UILabel(frame: CGRect(x: 0, y: 25, width: 50, height: 25)) + label.text = "Test" + + let labelParent = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 25)) + labelParent.backgroundColor = .green + labelParent.clipsToBounds = true + labelParent.addSubview(label) + + let image = try XCTUnwrap(prepare(views: [labelParent])) + let pixel1 = color(at: CGPoint(x: 10, y: 10), in: image) + + assertColor(pixel1, .green) + + let pixel2 = color(at: CGPoint(x: 10, y: 30), in: image) + assertColor(pixel2, .white) + } + + func testRedactLabelInsideClippedView() throws { + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 25)) + label.text = "Test" + + let labelParent = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 25)) + labelParent.backgroundColor = .green + labelParent.clipsToBounds = true + labelParent.addSubview(label) + + let image = try XCTUnwrap(prepare(views: [labelParent])) + let pixel1 = color(at: CGPoint(x: 10, y: 10), in: image) + + assertColor(pixel1, .black) + + let pixel2 = color(at: CGPoint(x: 10, y: 30), in: image) + assertColor(pixel2, .white) + } + private func assertColor(_ color1: UIColor, _ color2: UIColor) { let sRGBColor1 = color1.cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil) let sRGBColor2 = color2.cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil) diff --git a/Tests/SentryTests/UIRedactBuilderTests.swift b/Tests/SentryTests/UIRedactBuilderTests.swift index ad6e1a2952a..995dc5e0e66 100644 --- a/Tests/SentryTests/UIRedactBuilderTests.swift +++ b/Tests/SentryTests/UIRedactBuilderTests.swift @@ -134,7 +134,7 @@ class UIRedactBuilderTests: XCTestCase { let result = sut.redactRegionsFor(view: rootView, options: RedactOptions()) XCTAssertEqual(result.count, 1) - XCTAssertEqual(result.first?.type, .clip) + XCTAssertEqual(result.first?.type, .clipOut) XCTAssertEqual(result.first?.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 10, ty: 10)) } From 7878f047f5b02cf2acea18186e22a52200a33152 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 13 Sep 2024 16:03:38 +0200 Subject: [PATCH 18/19] Correct redact UIView with higher zPosition (#4309) --- CHANGELOG.md | 1 + .../SentryTests/SentryViewPhotographerTests.swift | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81714923d64..c5520de89db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Resumes replay when the app becomes active (#4303) - Session replay redact view with transformation (#4308) +- Correct redact UIView with higher zPosition (#4309) - Don't redact clipped views (#4325) - Double-quoted include, expected angle-bracketed instead (#4298) diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift index df2ff39aa37..c8d1f412ad0 100644 --- a/Tests/SentryTests/SentryViewPhotographerTests.swift +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -84,6 +84,20 @@ class SentryViewPhotographerTests: XCTestCase { assertColor(pixel, .green) } + func testLabelRedactedWithHigherZpotition() throws { + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) + label.text = "Test" + label.layer.zPosition = 1 + + let viewOnTop = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) + viewOnTop.backgroundColor = .green + + let image = try XCTUnwrap(prepare(views: [label, viewOnTop])) + let pixel = color(at: CGPoint(x: 10, y: 10), in: image) + + assertColor(pixel, .black) + } + func testLabelRedactedWithNonOpaqueViewOnTop() throws { let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) label.text = "Test" From 3bc34ab5760e4b49bf2dbe42cf749adba8407e14 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 13 Sep 2024 16:32:18 +0200 Subject: [PATCH 19/19] Update UIRedactBuilder.swift --- Sources/Swift/Tools/UIRedactBuilder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index f7a8451e26b..0389707b99b 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -177,7 +177,7 @@ class UIRedactBuilder { /// The beginning will be added after all the subviews have been mapped. redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .clipEnd)) } - for subview in view.subviews { + for subview in view.subviews.sorted(by: { $0.layer.zPosition < $1.layer.zPosition }) { mapRedactRegion(fromView: subview, redacting: &redacting, rootFrame: rootFrame, redactOptions: redactOptions, transform: newTransform) } if view.clipsToBounds {