Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/replay-square-mask-fill.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion PostHog/Replay/RRWireframe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class RRWireframe {

for rect in maskableWidgets {
UIColor.black.setFill()
UIBezierPath(roundedRect: rect, cornerRadius: 10).fill()
UIRectFill(rect)
Comment thread
turnipdabeets marked this conversation as resolved.
Resolved
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion PostHogTests/PostHogMaskSnapshotTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions PostHogTests/RRWireframeTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import Foundation
@testable import PostHog
import Testing
#if os(iOS)
import UIKit
#endif

@Suite("RRWireframe Tests")
struct RRWireframeTest {
Expand Down Expand Up @@ -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
}
Binary file modified PostHogTests/__MaskSnapshots__/case_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_11.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_12.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_13.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_14.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_17.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_18.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_20.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_21.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_22.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_23.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified PostHogTests/__MaskSnapshots__/case_9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading