Skip to content

Commit

Permalink
fix: Session replay redact view with transformation (#4308)
Browse files Browse the repository at this point in the history
  • Loading branch information
brustolin authored Sep 16, 2024
1 parent 24e0744 commit e5a239a
Show file tree
Hide file tree
Showing 11 changed files with 532 additions and 241 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
### Fixes

- Resumes replay when the app becomes active (#4303)
- Session replay redact view with transformation (#4308)
- Correct redact UIView with higher zPosition (#4309)
- Don't redact clipped views (#4325)
- Session replay for crash not created because of a race condition (#4314)
- Double-quoted include, expected angle-bracketed instead (#4298)

Expand Down
4 changes: 4 additions & 0 deletions Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
D8D7BB4A2750067900044146 /* UIAssert.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D7BB492750067900044146 /* UIAssert.swift */; };
D8D7BB4C2750095800044146 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D7BB4B2750095800044146 /* UIViewExtension.swift */; };
D8D7BB4E27501B9400044146 /* SpanObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D7BB4D27501B9400044146 /* SpanObserver.swift */; };
D8DA29042C7F2199008BC825 /* SRRedactSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DA29032C7F2199008BC825 /* SRRedactSampleViewController.swift */; };
D8DBDA76274D591F00007380 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DBDA75274D591F00007380 /* TableViewController.swift */; };
D8DBDA78274D5FC400007380 /* SplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DBDA77274D5FC400007380 /* SplitViewController.swift */; };
D8F01DEA2A1376B5008F4996 /* InfoForBreadcrumbController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F01DE92A1376B5008F4996 /* InfoForBreadcrumbController.swift */; };
Expand Down Expand Up @@ -367,6 +368,7 @@
D8D7BB492750067900044146 /* UIAssert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAssert.swift; sourceTree = "<group>"; };
D8D7BB4B2750095800044146 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = "<group>"; };
D8D7BB4D27501B9400044146 /* SpanObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpanObserver.swift; sourceTree = "<group>"; };
D8DA29032C7F2199008BC825 /* SRRedactSampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRRedactSampleViewController.swift; sourceTree = "<group>"; };
D8DBDA75274D591F00007380 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = "<group>"; };
D8DBDA77274D5FC400007380 /* SplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewController.swift; sourceTree = "<group>"; };
D8F01DE92A1376B5008F4996 /* InfoForBreadcrumbController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoForBreadcrumbController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -612,6 +614,7 @@
D8832B1D2AF52D0500C522B0 /* PageViewController.swift */,
B70038842BB33E7700065A38 /* ReplaceContentViewController.swift */,
D8AE48C82C57DC2F0092A2A6 /* WebViewController.swift */,
D8DA29032C7F2199008BC825 /* SRRedactSampleViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
Expand Down Expand Up @@ -1080,6 +1083,7 @@
D8444E4C275E38090042F4DE /* UIViewControllerExtension.swift in Sources */,
637AFDAE243B02760034958B /* TransactionsViewController.swift in Sources */,
D8832B132AF4F7FE00C522B0 /* TopViewControllerInspector.swift in Sources */,
D8DA29042C7F2199008BC825 /* SRRedactSampleViewController.swift in Sources */,
0AABE2EA28855FF80057ED69 /* PermissionsViewController.swift in Sources */,
84BA71E42C8BBBEC0045B828 /* DSNDisplayViewController.swift in Sources */,
7B5525B32938B5B5006A2932 /* DiskWriteException.swift in Sources */,
Expand Down
104 changes: 92 additions & 12 deletions Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

class SRRedactSampleViewController: UIViewController {

@IBOutlet var notRedactedView: UIView!

@IBOutlet var label: UILabel!

override func viewDidLoad() {
super.viewDidLoad()

notRedactedView.backgroundColor = .green
notRedactedView.transform = CGAffineTransform(rotationAngle: 45 * .pi / 180.0)

SentrySDK.replay.ignoreView(notRedactedView)
}
}
8 changes: 4 additions & 4 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@
D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */; };
D82859432C3E753C009A28AA /* SentrySessionReplaySyncC.c in Sources */ = {isa = PBXBuildFile; fileRef = D82859422C3E753C009A28AA /* SentrySessionReplaySyncC.c */; };
D82859442C3E753C009A28AA /* SentrySessionReplaySyncC.h in Headers */ = {isa = PBXBuildFile; fileRef = D82859412C3E753C009A28AA /* SentrySessionReplaySyncC.h */; };
D82915632C85EF0C00A6CDD4 /* SentryViewPhotographerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82915622C85EF0C00A6CDD4 /* SentryViewPhotographerTests.swift */; };
D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; };
D82DD1CD2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82DD1CC2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift */; };
D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; };
Expand Down Expand Up @@ -885,7 +886,6 @@
D8AFC01A2BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */; };
D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */; };
D8AFC0572BDA895400118BE1 /* UIRedactBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0562BDA895400118BE1 /* UIRedactBuilder.swift */; };
D8AFC05A2BDA89C100118BE1 /* RedactRegionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */; };
D8B0542E2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */; };
D8B088B629C9E3FF00213258 /* SentryTracerConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8B088B429C9E3FF00213258 /* SentryTracerConfiguration.h */; };
D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = D8B088B529C9E3FF00213258 /* SentryTracerConfiguration.m */; };
Expand Down Expand Up @@ -1873,6 +1873,7 @@
D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = "<group>"; };
D82859412C3E753C009A28AA /* SentrySessionReplaySyncC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplaySyncC.h; path = include/SentrySessionReplaySyncC.h; sourceTree = "<group>"; };
D82859422C3E753C009A28AA /* SentrySessionReplaySyncC.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SentrySessionReplaySyncC.c; sourceTree = "<group>"; };
D82915622C85EF0C00A6CDD4 /* SentryViewPhotographerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewPhotographerTests.swift; sourceTree = "<group>"; };
D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderSanitizer.swift; sourceTree = "<group>"; };
D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = "<group>"; };
D82DD1CC2BEEB1A0001AB556 /* SentrySRDefaultBreadcrumbConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySRDefaultBreadcrumbConverterTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1957,7 +1958,6 @@
D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewScreenshotProvider.swift; sourceTree = "<group>"; };
D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayVideoMaker.swift; sourceTree = "<group>"; };
D8AFC0562BDA895400118BE1 /* UIRedactBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIRedactBuilder.swift; sourceTree = "<group>"; };
D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactRegionTests.swift; sourceTree = "<group>"; };
D8AFC0612BDBEDF100118BE1 /* SentrySessionReplayIntegration+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySessionReplayIntegration+Private.h"; path = "include/SentrySessionReplayIntegration+Private.h"; sourceTree = "<group>"; };
D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
D8B088B429C9E3FF00213258 /* SentryTracerConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTracerConfiguration.h; path = include/SentryTracerConfiguration.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3702,10 +3702,10 @@
D84541192A2DC55100E2B11C /* SentryBinaryImageCache+Private.h */,
D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */,
D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */,
D8AFC0582BDA899A00118BE1 /* RedactRegionTests.swift */,
D8F67AEF2BE0D31A00C9197B /* UIImageHelperTests.swift */,
D8F67AF22BE10F7600C9197B /* UIRedactBuilderTests.swift */,
51B15F7F2BE88D510026A2F2 /* URLSessionTaskHelperTests.swift */,
D82915622C85EF0C00A6CDD4 /* SentryViewPhotographerTests.swift */,
);
name = Tools;
sourceTree = "<group>";
Expand Down Expand Up @@ -4830,6 +4830,7 @@
63FE722420DA66EC00CDBAE8 /* SentryCrashMonitor_NSException_Tests.m in Sources */,
7B5AB65D27E48E5200F1D1BA /* TestThreadInspector.swift in Sources */,
7BF9EF742722A85B00B5BBEF /* SentryClassRegistrator.m in Sources */,
D82915632C85EF0C00A6CDD4 /* SentryViewPhotographerTests.swift in Sources */,
D8DBE0CA2C0E093000FAB1FD /* SentryTouchTrackerTests.swift in Sources */,
D8F67AF42BE10F9600C9197B /* UIRedactBuilderTests.swift in Sources */,
63B819141EC352A7002FDF4C /* SentryInterfacesTests.m in Sources */,
Expand Down Expand Up @@ -4958,7 +4959,6 @@
62BAD74E2BA1C58D00EBAAFC /* EncodeMetricTests.swift in Sources */,
7BE0DC29272A9E1C004FA8B7 /* SentryBreadcrumbTrackerTests.swift in Sources */,
63FE722520DA66EC00CDBAE8 /* SentryCrashFileUtils_Tests.m in Sources */,
D8AFC05A2BDA89C100118BE1 /* RedactRegionTests.swift in Sources */,
D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */,
7BFC16BA2524D4AF00FF6266 /* SentryMessage+Equality.m in Sources */,
7B4260342630315C00B36EDD /* SampleError.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
* A list of custom UIView subclasses to be ignored
* during masking step of the session replay.
* The view itself and any child will be ignored and not masked.
* This property has precedence over `redactViewTypes`.
*/
public var ignoreRedactViewTypes = [AnyClass]()

Expand Down
62 changes: 54 additions & 8 deletions Sources/Swift/Tools/SentryViewPhotographer.swift
Original file line number Diff line number Diff line change
@@ -1,32 +1,78 @@
#if canImport(UIKit) && !SENTRY_NO_UIKIT
#if os(iOS) || os(tvOS)

@_implementationOnly import _SentryPrivate
import CoreGraphics
import Foundation
import UIKit

protocol ViewRenderer {
func render(view: UIView) -> UIImage
}

class DefaultViewRenderer: ViewRenderer {
func render(view: UIView) -> UIImage {
let image = UIGraphicsImageRenderer(size: view.bounds.size).image { _ in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
}
return image
}
}

@objcMembers
class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider {

static let shared = SentryViewPhotographer()

private let redactBuilder = UIRedactBuilder()
private let dispatchQueue = SentryDispatchQueueWrapper()

var renderer: ViewRenderer

init(renderer: ViewRenderer) {
self.renderer = renderer
super.init()
}

private convenience override init() {
self.init(renderer: DefaultViewRenderer())
}

func image(view: UIView, options: SentryRedactOptions, onComplete: @escaping ScreenshotCallback ) {
let image = UIGraphicsImageRenderer(size: view.bounds.size).image { _ in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)
}
let image = renderer.render(view: view)

let redact = redactBuilder.redactRegionsFor(view: view, options: options)
let imageSize = view.bounds.size
DispatchQueue.global().async {
dispatchQueue.dispatchAsync {
let screenshot = UIGraphicsImageRenderer(size: imageSize, format: .init(for: .init(displayScale: 1))).image { context in

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 {
(region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: region.rect)).setFill()
context.fill(region.rect)
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.applying(region.transform))).setFill()
context.cgContext.addPath(path)
context.cgContext.fillPath()
case .clipOut:
context.cgContext.addRect(context.cgContext.boundingBoxOfClipPath)
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()
}
}
}
onComplete(screenshot)
Expand Down
Loading

0 comments on commit e5a239a

Please sign in to comment.