diff --git a/.changeset/replay-square-mask-fill.md b/.changeset/replay-square-mask-fill.md new file mode 100644 index 000000000..06f3a9faa --- /dev/null +++ b/.changeset/replay-square-mask-fill.md @@ -0,0 +1,5 @@ +--- +"posthog-ios": patch +--- + +Fix screenshot session replay masks leaving the corners of each masked area visible; masks now cover the full rectangle. diff --git a/PostHog/Replay/RRWireframe.swift b/PostHog/Replay/RRWireframe.swift index dd69136fc..66147a59f 100644 --- a/PostHog/Replay/RRWireframe.swift +++ b/PostHog/Replay/RRWireframe.swift @@ -68,7 +68,7 @@ class RRWireframe { for rect in maskableWidgets { UIColor.black.setFill() - UIBezierPath(roundedRect: rect, cornerRadius: 10).fill() + UIRectFill(rect) } } } diff --git a/PostHogTests/PostHogMaskSnapshotTest.swift b/PostHogTests/PostHogMaskSnapshotTest.swift index c51305adb..cd960b815 100644 --- a/PostHogTests/PostHogMaskSnapshotTest.swift +++ b/PostHogTests/PostHogMaskSnapshotTest.swift @@ -449,7 +449,7 @@ let original = render(controller.view) // Redact through the exact production painter (RRWireframe.maskImage) so the golden - // reflects real replay output (rounded corners, scale-1), not hand-drawn squares. + // reflects real replay output (scale-1), not hand-drawn rects. let rects = integration.collectMaskableRects(in: window) ?? [] let masked = RRWireframe.maskImage(original, maskableWidgets: rects) ?? original return compose(scenario, original: original, masked: masked) diff --git a/PostHogTests/RRWireframeTest.swift b/PostHogTests/RRWireframeTest.swift index e1b0e4ca0..2ca2a38ef 100644 --- a/PostHogTests/RRWireframeTest.swift +++ b/PostHogTests/RRWireframeTest.swift @@ -6,6 +6,9 @@ import Foundation @testable import PostHog import Testing +#if os(iOS) + import UIKit +#endif @Suite("RRWireframe Tests") struct RRWireframeTest { @@ -202,4 +205,41 @@ struct RRWireframeTest { #expect(wireframe.toDict()["type"] as? String == type) } } + + #if os(iOS) + @Suite("maskImage") + struct MaskImageTests { + @Test("fills every pixel of a mask rect, including its corners") + func fillsCorners() throws { + let white = UIGraphicsImageRenderer(size: CGSize(width: 100, height: 100)).image { ctx in + UIColor.white.setFill() + ctx.fill(CGRect(x: 0, y: 0, width: 100, height: 100)) + } + let rect = CGRect(x: 20, y: 20, width: 60, height: 40) + + let masked = try #require(RRWireframe.maskImage(white, maskableWidgets: [rect])) + + for corner in [CGPoint(x: 20, y: 20), CGPoint(x: 79, y: 20), CGPoint(x: 20, y: 59), CGPoint(x: 79, y: 59)] { + #expect(Self.red(of: masked, at: corner) == 0, "corner \(corner) is not masked") + } + for outside in [CGPoint(x: 19, y: 20), CGPoint(x: 80, y: 59), CGPoint(x: 20, y: 19), CGPoint(x: 79, y: 60)] { + #expect(Self.red(of: masked, at: outside) == 255, "mask spilled outside its rect at \(outside)") + } + } + + /// Red channel of the pixel at `point`, read by drawing the image into a 1x1 bitmap. + private static func red(of image: UIImage, at point: CGPoint) -> UInt8? { + guard let cgImage = image.cgImage else { return nil } + var pixel = [UInt8](repeating: 0, count: 4) + guard let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) + else { return nil } + // CGContext's origin is bottom-left: shift so the target pixel lands on (0, 0). + context.draw(cgImage, in: CGRect(x: -point.x, y: point.y - CGFloat(cgImage.height) + 1, + width: CGFloat(cgImage.width), height: CGFloat(cgImage.height))) + return pixel[0] + } + } + #endif } diff --git a/PostHogTests/__MaskSnapshots__/case_1.png b/PostHogTests/__MaskSnapshots__/case_1.png index 473d744b1..f653590d6 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_1.png and b/PostHogTests/__MaskSnapshots__/case_1.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_10.png b/PostHogTests/__MaskSnapshots__/case_10.png index 5ef7565f7..1f9c035d5 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_10.png and b/PostHogTests/__MaskSnapshots__/case_10.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_11.png b/PostHogTests/__MaskSnapshots__/case_11.png index f01ba88c7..96aadc4c8 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_11.png and b/PostHogTests/__MaskSnapshots__/case_11.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_12.png b/PostHogTests/__MaskSnapshots__/case_12.png index a6edf6aeb..5e8308ae4 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_12.png and b/PostHogTests/__MaskSnapshots__/case_12.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_13.png b/PostHogTests/__MaskSnapshots__/case_13.png index c1728d815..2125888f3 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_13.png and b/PostHogTests/__MaskSnapshots__/case_13.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_14.png b/PostHogTests/__MaskSnapshots__/case_14.png index afa230456..ace11fe3a 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_14.png and b/PostHogTests/__MaskSnapshots__/case_14.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_17.png b/PostHogTests/__MaskSnapshots__/case_17.png index 94d8c0bc3..d6ee48040 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_17.png and b/PostHogTests/__MaskSnapshots__/case_17.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_18.png b/PostHogTests/__MaskSnapshots__/case_18.png index 97c9d5808..a06b705fe 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_18.png and b/PostHogTests/__MaskSnapshots__/case_18.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_2.png b/PostHogTests/__MaskSnapshots__/case_2.png index 763605467..b500bd2b0 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_2.png and b/PostHogTests/__MaskSnapshots__/case_2.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_20.png b/PostHogTests/__MaskSnapshots__/case_20.png index 98096d23a..1e39b80bd 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_20.png and b/PostHogTests/__MaskSnapshots__/case_20.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_21.png b/PostHogTests/__MaskSnapshots__/case_21.png index 07cb2bcae..73663518c 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_21.png and b/PostHogTests/__MaskSnapshots__/case_21.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_22.png b/PostHogTests/__MaskSnapshots__/case_22.png index 5f7b1694d..b86925ead 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_22.png and b/PostHogTests/__MaskSnapshots__/case_22.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_23.png b/PostHogTests/__MaskSnapshots__/case_23.png index ebcb20e85..96c9e752c 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_23.png and b/PostHogTests/__MaskSnapshots__/case_23.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_24.png b/PostHogTests/__MaskSnapshots__/case_24.png index 3e16881b7..d8bb38b15 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_24.png and b/PostHogTests/__MaskSnapshots__/case_24.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_3.png b/PostHogTests/__MaskSnapshots__/case_3.png index 5d31f4d27..d2713073e 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_3.png and b/PostHogTests/__MaskSnapshots__/case_3.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_4.png b/PostHogTests/__MaskSnapshots__/case_4.png index e7e1a6230..b59c97b35 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_4.png and b/PostHogTests/__MaskSnapshots__/case_4.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_5.png b/PostHogTests/__MaskSnapshots__/case_5.png index 8ccfabf1c..4507cad8f 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_5.png and b/PostHogTests/__MaskSnapshots__/case_5.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_6.png b/PostHogTests/__MaskSnapshots__/case_6.png index d123f76cb..1528582a9 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_6.png and b/PostHogTests/__MaskSnapshots__/case_6.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_7.png b/PostHogTests/__MaskSnapshots__/case_7.png index e252338e4..db0e74cd6 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_7.png and b/PostHogTests/__MaskSnapshots__/case_7.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_8.png b/PostHogTests/__MaskSnapshots__/case_8.png index d6800ea0d..9bc731909 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_8.png and b/PostHogTests/__MaskSnapshots__/case_8.png differ diff --git a/PostHogTests/__MaskSnapshots__/case_9.png b/PostHogTests/__MaskSnapshots__/case_9.png index 72e738ea1..c80f09035 100644 Binary files a/PostHogTests/__MaskSnapshots__/case_9.png and b/PostHogTests/__MaskSnapshots__/case_9.png differ