Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6a5c478
fix(session-replay): Add multiple masking improvements
philprime Oct 28, 2025
e00e236
test(session-replay): Add masking tests for common cases
philprime Oct 28, 2025
0b17145
test(session-replay): Add masking tests for React Native views
philprime Oct 28, 2025
000efc9
test(session-replay): Add masking tests for edge cases
philprime Oct 28, 2025
acd9692
test(session-replay): Enhance edge case tests for animation masking
philprime Oct 28, 2025
f44c933
test(session-replay): Refine animation midpoint assertions in edge ca…
philprime Oct 28, 2025
816fbc2
Merge remote-tracking branch 'origin/main' into philprime/fix-masking…
philprime Oct 29, 2025
0271708
remove duplicate entries
philprime Oct 29, 2025
f5d58ec
Merge branch 'philprime/fix-masking_split_2' into philprime/fix-maski…
philprime Oct 29, 2025
dd810e7
fix project
philprime Oct 29, 2025
63e919f
Merge branch 'philprime/fix-masking_split_2' into philprime/fix-maski…
philprime Oct 29, 2025
5ffea17
fix flaky test
philprime Oct 29, 2025
3cbb366
Merge branch 'main' into philprime/fix-masking_split_3
philprime Oct 29, 2025
7524f6a
Merge branch 'main' into philprime/fix-masking_split_3
philprime Oct 29, 2025
3c24964
fix: Include layer background color when checkig if a view is opaque
itaybre Nov 2, 2025
8fd8489
Merge remote-tracking branch 'origin/main' into itay/improve_opaque_l…
philprime Nov 3, 2025
6ad6a3a
remove snapshot testing
philprime Nov 3, 2025
9a54453
Merge remote-tracking branch 'origin/main' into philprime/fix-masking…
philprime Nov 3, 2025
324b9c5
Merge branch 'philprime/fix-masking_split_3' into itay/improve_opaque…
philprime Nov 3, 2025
009b54a
add tests
philprime Nov 3, 2025
f00abad
fix logic
philprime Nov 3, 2025
e50806f
Merge branch 'main' into itay/improve_opaque_logic
philprime Nov 3, 2025
e9deb8c
update changelog
philprime Nov 3, 2025
7bfa880
fix implementation
philprime Nov 3, 2025
604be44
Merge remote-tracking branch 'origin/main' into itay/improve_opaque_l…
philprime Nov 3, 2025
a0ea183
made masking stricter to properly handle translucent views
philprime Nov 3, 2025
f5330ad
Merge remote-tracking branch 'origin/main' into itay/improve_opaque_l…
philprime Nov 3, 2025
378cc12
update changelog
philprime Nov 3, 2025
514938e
Merge branch 'main' into itay/improve_opaque_logic
philprime Nov 4, 2025
8068e8e
Merge branch 'main' into itay/improve_opaque_logic
philprime Nov 5, 2025
a5d64ec
add missing tests
philprime Nov 5, 2025
ace899e
Merge remote-tracking branch 'origin/main' into itay/improve_opaque_l…
philprime Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
- Fix axis-aligned transform detection for optimized opaque view clipping
- Rename `SentryMechanismMeta` to `SentryMechanismContext` to resolve Kotlin Multi-Platform build errors (#6607)
- Fix conversion of frame rate to time interval for session replay (#6623)
- Change Session Replay masking to prevent semi‑transparent full‑screen overlays from clearing redactions by making opaque clipping stricter (#6629)
Views now need to be fully opaque (view and layer backgrounds with alpha == 1) and report opaque to qualify for clip‑out.
This avoids leaks at the cost of fewer clip‑out optimizations.

### Improvements

Expand Down
2 changes: 0 additions & 2 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,6 @@
D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; };
D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; };
D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; };
D4AF7D262E9401EB004F0F59 /* SentryUIRedactBuilderTests+UIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AF7D252E9401EB004F0F59 /* SentryUIRedactBuilderTests+UIKit.swift */; };
D4AF7D2A2E940493004F0F59 /* SentryUIRedactBuilderTests+Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AF7D292E940492004F0F59 /* SentryUIRedactBuilderTests+Common.swift */; };
D4AF7D2C2E9404ED004F0F59 /* SentryUIRedactBuilderTests+EdgeCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4AF7D2B2E9404ED004F0F59 /* SentryUIRedactBuilderTests+EdgeCases.swift */; };
D4B0DC7F2DA9257A00DE61B6 /* SentryRenderVideoResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B0DC7E2DA9257200DE61B6 /* SentryRenderVideoResult.swift */; };
Expand Down Expand Up @@ -6271,7 +6270,6 @@
D45E2D772E003EBF0072A6B7 /* TestRedactOptions.swift in Sources */,
63FE720520DA66EC00CDBAE8 /* FileBasedTestCase.m in Sources */,
51B15F802BE88D510026A2F2 /* URLSessionTaskHelperTests.swift in Sources */,
D4AF7D262E9401EB004F0F59 /* SentryUIRedactBuilderTests+UIKit.swift in Sources */,
63EED6C32237989300E02400 /* SentryOptionsTest.m in Sources */,
7BBD18B22451804C00427C76 /* SentryRetryAfterHeaderParserTests.swift in Sources */,
7BD337E424A356180050DB6E /* SentryCrashIntegrationTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,9 +618,58 @@ final class SentryUIRedactBuilder {
}

/// 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.
/// This is used to optimize redaction by clearing out regions that are fully covered.
///
/// The method checks multiple properties because UIKit views can become transparent in several ways:
/// - `view.alpha` (mapped to `layer.opacity`) can make the entire view semi-transparent
/// - `view.backgroundColor` or `layer.backgroundColor` can have alpha components
/// - Either the view or layer can explicitly set their `isOpaque` property to false
///
/// ## Implementation Notes:
/// - We use the presentation layer when available to get the actual rendered state during animations
/// - We require BOTH the view and the layer to appear opaque (alpha == 1 and marked opaque)
/// to classify a view as opaque. This avoids false positives where only one side is configured,
/// which previously caused semi‑transparent overlays or partially configured views to clear
/// redactions behind them.
/// - We use `SentryRedactViewHelper.shouldClipOut(view)` for views explicitly marked as opaque
///
/// ## Bug Fix Context:
/// This implementation fixes the issue where semi-transparent overlays (e.g., with `alpha = 0.2`)
/// were incorrectly treated as opaque, causing text behind them to not be redacted.
/// See: https://github.com/getsentry/sentry-cocoa/pull/6629#issuecomment-3479730690
private func isOpaque(_ view: UIView) -> Bool {
let layer = view.layer.presentation() ?? view.layer
return SentryRedactViewHelper.shouldClipOut(view) || (layer.opacity == 1 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1)

// Allow explicit override: if a view is marked to clip out, treat it as opaque
if SentryRedactViewHelper.shouldClipOut(view) {
return true
}

// First check: Ensure the layer opacity is 1.0
// This catches views with `alpha < 1.0`, which are semi-transparent regardless of background color.
// For example, a view with `alpha = 0.2` should never be considered opaque, even if it has
// a solid background color, because the entire view (including the background) is semi-transparent.
guard layer.opacity == 1 else {
return false
}

// Second check: Verify the view has an opaque background color
// We check the view's properties first because this is the most common pattern in UIKit.
let isViewOpaque = view.isOpaque && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1

// Third check: Verify the layer has an opaque background color
// We also check the layer's properties because:
// - Some views customize their CALayer directly without setting view.backgroundColor
// - Libraries or custom views might override backgroundColor to return different values
// - The layer's backgroundColor is the actual rendered property (view.backgroundColor is a convenience)
let isLayerOpaque = layer.isOpaque && layer.backgroundColor != nil && (layer.backgroundColor?.alpha ?? 0) == 1

// We REQUIRE BOTH: the view AND the layer must be opaque for the view to be treated as opaque.
// This stricter rule prevents semi‑transparent overlays or partially configured backgrounds
// (only view or only layer) from clearing previously collected redact regions.
return isViewOpaque && isLayerOpaque
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ class SentryUIRedactBuilderTests_Common: SentryUIRedactBuilderTests { // swiftli

let opaqueView = UIView(frame: CGRect(x: 10, y: 10, width: 60, height: 60))
opaqueView.backgroundColor = .white
opaqueView.isOpaque = true
opaqueView.layer.isOpaque = true
opaqueView.layer.backgroundColor = UIColor.white.cgColor
rootView.addSubview(opaqueView)

// View Hierarchy:
Expand Down Expand Up @@ -837,6 +840,9 @@ class SentryUIRedactBuilderTests_Common: SentryUIRedactBuilderTests { // swiftli

let overView = UIView(frame: rootView.bounds)
overView.backgroundColor = .black
overView.isOpaque = true
overView.layer.isOpaque = true
overView.layer.backgroundColor = UIColor.black.cgColor
rootView.addSubview(overView)

// View Hierarchy:
Expand Down Expand Up @@ -1098,7 +1104,7 @@ class SentryUIRedactBuilderTests_Common: SentryUIRedactBuilderTests { // swiftli

// View Hierarchy:
// ---------------
// == iOS 26 ==
// == iOS 26.1 - Xcode 26 ==
// <UIView: 0x11b209710; frame = (0 0; 100 100); layer = <CALayer: 0x600000cd1440>>
// | <UISlider: 0x11b23e2e0; frame = (10 10; 80 20); opaque = NO; gestureRecognizers = <NSArray: 0x600000276be0>; layer = <CALayer: 0x600000ce80c0>; value: 0.000000>
// | | <UIKit._UISliderGlassVisualElement: 0x11b25bdd0; frame = (0 0; 80 20); autoresize = W+H; layer = <CALayer: 0x600000cde0a0>>
Expand All @@ -1109,48 +1115,36 @@ class SentryUIRedactBuilderTests_Common: SentryUIRedactBuilderTests { // swiftli
// | | | | <UIView: 0x11b22bf60; frame = (0 0; 37 24); autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x600000ce9410>>
// | | | | | <UIView: 0x11b434710; frame = (0 0; 37 24); autoresize = W+H; userInteractionEnabled = NO; backgroundColor = <UIDynamicSystemColor: 0x600001749000; name = _controlForegroundColor>; layer = <CALayer: 0x600000cdd740>>
//
// == iOS 26.1 - Xcode 16.4 ==
// <UIView: 0x10701dbf0; frame = (0 0; 100 100); layer = <CALayer: 0x600000cb96b0>>
// | <UISlider: 0x100e28d30; frame = (10 10; 80 20); opaque = NO; layer = <CALayer: 0x600000cae490>; value: 0.000000>
// | | <_UISlideriOSVisualElement: 0x100f06990; frame = (0 0; 80 20); opaque = NO; autoresize = W+H; layer = <CALayer: 0x600000c05fe0>>
//
// == iOS 18 & 17 & 16 ==
// <UIView: 0x12ed12bc0; frame = (0 0; 100 100); layer = <CALayer: 0x600001de3540>>
// | <UISlider: 0x13ed0f7e0; frame = (10 10; 80 20); opaque = NO; layer = <CALayer: 0x600001df0020>; value: 0.000000>
// | | <_UISlideriOSVisualElement: 0x13ed0fbd0; frame = (0 0; 80 20); opaque = NO; autoresize = W+H; layer = <CALayer: 0x600001da7f80>>

// -- Act --
print(rootView.value(forKey: "recursiveDescription")!)
let sut = getSut(maskAllText: true, maskAllImages: true)
let result = sut.redactRegionsFor(view: rootView)

// -- Assert --
// UISlider behavior differs by iOS version
if #available(iOS 26.0, *) {
// On iOS 26, UISlider uses a new visual implementation that creates clipping regions
// even though the slider itself is in the ignore list
let region0 = try XCTUnwrap(result.element(at: 0))
XCTAssertNil(region0.color)
XCTAssertEqual(region0.size, CGSize(width: 37, height: 24))
XCTAssertEqual(region0.type, .clipOut)
XCTAssertEqual(region0.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 10, ty: 8))

let region1 = try XCTUnwrap(result.element(at: 1))
if #available(iOS 26, *), isBuiltWithSDK26() {
// Only applies to Liquid Glass (enabled when built with Xcode 26+)
let region1 = try XCTUnwrap(result.element(at: 0))
XCTAssertNil(region1.color)
XCTAssertEqual(region1.size, CGSize(width: 80, height: 6))
XCTAssertEqual(region1.type, .clipBegin)
XCTAssertEqual(region1.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 10, ty: 17))

let region2 = try XCTUnwrap(result.element(at: 2))
let region2 = try XCTUnwrap(result.element(at: 1))
XCTAssertNil(region2.color)
XCTAssertEqual(region2.size, CGSize(width: 0, height: 6))
XCTAssertEqual(region2.type, .clipOut)
XCTAssertEqual(region2.size, CGSize(width: 80, height: 6))
XCTAssertEqual(region2.type, .clipEnd)
XCTAssertEqual(region2.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 10, ty: 17))

let region3 = try XCTUnwrap(result.element(at: 3))
XCTAssertNil(region3.color)
XCTAssertEqual(region3.size, CGSize(width: 80, height: 6))
XCTAssertEqual(region3.type, .clipEnd)
XCTAssertEqual(region3.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 10, ty: 17))

// Assert that there are no other regions
XCTAssertEqual(result.count, 4)
} else {
// On iOS < 26, UISlider is completely ignored (no regions)
XCTAssertEqual(result.count, 0)
}
}
Expand Down Expand Up @@ -1347,6 +1341,17 @@ 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 {
guard let value = Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String else {
return false
}
guard let xcodeVersion = Int(value) else {
return false
}
return xcodeVersion >= 2_600
}

#endif // os(iOS) && !targetEnvironment(macCatalyst)
Expand Down
Loading
Loading