Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref: Mask screenshots for errors #4623

Merged
merged 13 commits into from
Dec 19, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Improvements

- Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603)
- Mask screenshots for errors ()

## 8.42.0-beta.2

Expand Down
34 changes: 20 additions & 14 deletions Sources/Sentry/SentryScreenshot.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@
# import "SentryCompiler.h"
# import "SentryDependencyContainer.h"
# import "SentryDispatchQueueWrapper.h"
# import "SentrySwift.h"
# import "SentryUIApplication.h"
# import <UIKit/UIKit.h>

@implementation SentryScreenshot
@implementation SentryScreenshot {
SentryViewPhotographer *photographer;
}

- (instancetype)init
{
if (self = [super init]) {
photographer = [[SentryViewPhotographer alloc]
initWithRedactOptions:[[SentryRedactDefaultOptions alloc] init]];
}
return self;
}

- (NSArray<NSData *> *)appScreenshotsFromMainThread
{
Expand Down Expand Up @@ -41,7 +53,6 @@ - (void)saveScreenShots:(NSString *)imagesDirectoryPath
- (NSArray<NSData *> *)appScreenshots
{
NSArray<UIWindow *> *windows = [SentryDependencyContainer.sharedInstance.application windows];

NSMutableArray *result = [NSMutableArray arrayWithCapacity:windows.count];

for (UIWindow *window in windows) {
Expand All @@ -53,21 +64,16 @@ - (void)saveScreenShots:(NSString *)imagesDirectoryPath
continue;
}

UIGraphicsBeginImageContext(size);
UIImage *img = [photographer imageWithView:window];

if ([window drawViewHierarchyInRect:window.bounds afterScreenUpdates:false]) {
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
// this shouldn't happen now that we discard windows with either 0 height or 0 width,
// but still, we shouldn't send any images with either one.
if (LIKELY(img.size.width > 0 && img.size.height > 0)) {
NSData *bytes = UIImagePNGRepresentation(img);
if (bytes && bytes.length > 0) {
[result addObject:bytes];
}
// this shouldn't happen now that we discard windows with either 0 height or 0 width,
// but still, we shouldn't send any images with either one.
if (LIKELY(img.size.width > 0 && img.size.height > 0)) {
NSData *bytes = UIImagePNGRepresentation(img);
if (bytes && bytes.length > 0) {
[result addObject:bytes];
}
}

UIGraphicsEndImageContext();
}
return result;
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/Swift/Protocol/SentryRedactOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,11 @@ protocol SentryRedactOptions {
var maskedViewClasses: [AnyClass] { get }
var unmaskedViewClasses: [AnyClass] { get }
}

@objcMembers
final class SentryRedactDefaultOptions: NSObject, SentryRedactOptions {
var maskAllText: Bool = true
var maskAllImages: Bool = true
var maskedViewClasses: [AnyClass] = []
var unmaskedViewClasses: [AnyClass] = []
}
100 changes: 56 additions & 44 deletions Sources/Swift/Tools/SentryViewPhotographer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,60 +37,72 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider {
self.redactBuilder = UIRedactBuilder(options: redactOptions)
}

func image(view: UIView, onComplete: @escaping ScreenshotCallback ) {
func image(view: UIView, onComplete: @escaping ScreenshotCallback) {
let redact = redactBuilder.redactRegionsFor(view: view)
let image = renderer.render(view: view)

let imageSize = view.bounds.size
dispatchQueue.dispatchAsync {
let screenshot = UIGraphicsImageRenderer(size: imageSize, format: .init(for: .init(displayScale: 1))).image { context in

let clipOutPath = CGMutablePath(rect: CGRect(origin: .zero, size: imageSize), transform: nil)
var clipPaths = [CGPath]()
let screenshot = self.maskScreenshot(screenshot: image, from: view, masking: redact)
onComplete(screenshot)
}
}

func image(view: UIView) -> UIImage {
let redact = redactBuilder.redactRegionsFor(view: view)
let image = renderer.render(view: view)

return self.maskScreenshot(screenshot: image, from: view, masking: redact)
}

private func maskScreenshot(screenshot image: UIImage, from view: UIView, masking: [RedactRegion]) -> UIImage {
let imageSize = view.bounds.size
let screenshot = UIGraphicsImageRenderer(size: imageSize, format: .init(for: .init(displayScale: 1))).image { context in

let clipOutPath = CGMutablePath(rect: CGRect(origin: .zero, size: imageSize), transform: nil)
var clipPaths = [CGPath]()

let imageRect = CGRect(origin: .zero, size: imageSize)
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)

var latestRegion: RedactRegion?
for region in masking {
let rect = CGRect(origin: CGPoint.zero, size: region.size)
var transform = region.transform
let path = CGPath(rect: rect, transform: &transform)

let imageRect = CGRect(origin: .zero, size: imageSize)
context.cgContext.addRect(CGRect(origin: CGPoint.zero, size: imageSize))
context.cgContext.clip(using: .evenOdd)
UIColor.blue.setStroke()
defer { latestRegion = region }

context.cgContext.interpolationQuality = .none
image.draw(at: .zero)
guard latestRegion?.canReplace(as: region) != true && imageRect.intersects(path.boundingBoxOfPath) else { continue }

var latestRegion: RedactRegion?
for region in redact {
let rect = CGRect(origin: CGPoint.zero, size: region.size)
var transform = region.transform
let path = CGPath(rect: rect, transform: &transform)

defer { latestRegion = region }

guard latestRegion?.canReplace(as: region) != true && imageRect.intersects(path.boundingBoxOfPath) else { continue }

switch region.type {
case .redact, .redactSwiftUI:
(region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: rect.applying(region.transform))).setFill()
context.cgContext.addPath(path)
context.cgContext.fillPath()
case .clipOut:
clipOutPath.addPath(path)
self.updateClipping(for: context.cgContext,
clipPaths: clipPaths,
clipOutPath: clipOutPath)
case .clipBegin:
clipPaths.append(path)
self.updateClipping(for: context.cgContext,
clipPaths: clipPaths,
clipOutPath: clipOutPath)
case .clipEnd:
clipPaths.removeLast()
self.updateClipping(for: context.cgContext,
clipPaths: clipPaths,
clipOutPath: clipOutPath)
}
switch region.type {
case .redact, .redactSwiftUI:
(region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: rect.applying(region.transform))).setFill()
context.cgContext.addPath(path)
context.cgContext.fillPath()
case .clipOut:
clipOutPath.addPath(path)
self.updateClipping(for: context.cgContext,
clipPaths: clipPaths,
clipOutPath: clipOutPath)
case .clipBegin:
clipPaths.append(path)
self.updateClipping(for: context.cgContext,
clipPaths: clipPaths,
clipOutPath: clipOutPath)
case .clipEnd:
clipPaths.removeLast()
self.updateClipping(for: context.cgContext,
clipPaths: clipPaths,
clipOutPath: clipOutPath)
}
}
onComplete(screenshot)
}
return screenshot
}

private func updateClipping(for context: CGContext, clipPaths: [CGPath], clipOutPath: CGPath) {
Expand Down