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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 23 additions & 2 deletions Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5CD-RQ-aBU">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5CD-RQ-aBU">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
Expand Down Expand Up @@ -1139,6 +1139,27 @@
<constraint firstAttribute="width" constant="240" id="dn8-bG-2cz"/>
</constraints>
</view>
<view clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nXU-s5-VDP">
<rect key="frame" x="40" y="90" width="240" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="This should not appear" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Cuu-no-Hgq">
<rect key="frame" x="33" y="150" width="175" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="A label inside a clip view" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TkV-ad-u0T">
<rect key="frame" x="28" y="54" width="184" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="Qfh-TT-5mw"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
Expand Down
25 changes: 16 additions & 9 deletions Sources/Swift/Tools/SentryViewPhotographer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
Expand Down
56 changes: 42 additions & 14 deletions Sources/Swift/Tools/UIRedactBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
36 changes: 36 additions & 0 deletions Tests/SentryTests/SentryViewPhotographerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Tests/SentryTests/UIRedactBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down