From f8eb5a720da550f68ad85c96f6ba621f9c0636a2 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 15 Nov 2024 09:53:27 +0100 Subject: [PATCH] Fix: Session replay opacity animation masking (#4532) ## :scroll: Description Animated opacity was not being properly masked --- CHANGELOG.md | 1 + Sources/Swift/Tools/UIRedactBuilder.swift | 6 ++-- Tests/SentryTests/UIRedactBuilderTests.swift | 37 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bd37b4c01..1b5e8eaed7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Concurrency crash with Swift 6 (#4512) - Make `Scope.span` fully thread safe (#4519) - Finish TTFD when not calling reportFullyDisplayed before binding a new transaction to the scope (#4526). +- Session replay opacity animation masking (#4532) ## 8.40.1 diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 35c9e7ddd7..f1d2c1e9a8 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -238,9 +238,8 @@ class UIRedactBuilder { } private func mapRedactRegion(fromView view: UIView, relativeTo parentLayer: CALayer?, redacting: inout [RedactRegion], rootFrame: CGRect, forceRedact: Bool = false) { - guard !redactClassesIdentifiers.isEmpty && !view.isHidden && view.alpha != 0 else { return } - let layer = view.layer.presentation() ?? view.layer + guard !redactClassesIdentifiers.isEmpty && !layer.isHidden && layer.opacity != 0 else { return } let newTransform = getTranform(from: layer, withParent: parentLayer) @@ -308,7 +307,8 @@ class UIRedactBuilder { Indicates whether the view is opaque and will block other view behind it */ private func isOpaque(_ view: UIView) -> Bool { - return SentryRedactViewHelper.shouldClipOut(view) || (view.alpha == 1 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1) + let layer = view.layer.presentation() ?? view.layer + return SentryRedactViewHelper.shouldClipOut(view) || (layer.opacity == 1 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1) } } diff --git a/Tests/SentryTests/UIRedactBuilderTests.swift b/Tests/SentryTests/UIRedactBuilderTests.swift index 21d408bb93..e6f78fa92d 100644 --- a/Tests/SentryTests/UIRedactBuilderTests.swift +++ b/Tests/SentryTests/UIRedactBuilderTests.swift @@ -20,6 +20,17 @@ class RedactOptions: SentryRedactOptions { } class UIRedactBuilderTests: XCTestCase { + private class CustomVisibilityView: UIView { + class CustomLayer: CALayer { + override var opacity: Float { + get { 0.5 } + set { } + } + } + override class var layerClass: AnyClass { + return CustomLayer.self + } + } private let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) @@ -394,6 +405,32 @@ class UIRedactBuilderTests: XCTestCase { XCTAssertTrue(sut.containsIgnoreClass(element), "\(element) not found") } } + + func testLayerIsNotFullyTransparentRedacted() { + let sut = getSut() + let view = CustomVisibilityView(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + view.alpha = 0 + view.sentryReplayMask() + + view.backgroundColor = .purple + rootView.addSubview(view) + + let result = sut.redactRegionsFor(view: rootView) + XCTAssertEqual(result.count, 1) + } + + func testViewLayerOnTopIsNotFullyTransparentRedacted() { + let sut = getSut() + let view = CustomVisibilityView(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + let label = UILabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40)) + view.backgroundColor = .purple + rootView.addSubview(label) + rootView.addSubview(view) + + let result = sut.redactRegionsFor(view: rootView) + XCTAssertEqual(result.first?.type, .redact) + XCTAssertEqual(result.count, 1) + } } #endif