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 (#4623)

## 8.42.0-beta.2

Expand Down
2 changes: 1 addition & 1 deletion Sources/Sentry/PrivateSentrySDKOnly.mm
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@

+ (NSArray<NSData *> *)captureScreenshots
{
#if SENTRY_HAS_UIKIT
#if SENTRY_TARGET_REPLAY_SUPPORTED

Check warning on line 277 in Sources/Sentry/PrivateSentrySDKOnly.mm

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/PrivateSentrySDKOnly.mm#L277

Added line #L277 was not covered by tests
return [SentryDependencyContainer.sharedInstance.screenshot appScreenshots];
#else
SENTRY_LOG_DEBUG(
Expand Down
4 changes: 3 additions & 1 deletion Sources/Sentry/SentryDependencyContainer.m
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ - (SentryUIDeviceWrapper *)uiDeviceWrapper SENTRY_DISABLE_THREAD_SANITIZER(

#endif // SENTRY_HAS_UIKIT

#if SENTRY_UIKIT_AVAILABLE
#if SENTRY_TARGET_REPLAY_SUPPORTED
- (SentryScreenshot *)screenshot SENTRY_DISABLE_THREAD_SANITIZER(
"double-checked lock produce false alarms")
{
Expand All @@ -275,7 +275,9 @@ - (SentryScreenshot *)screenshot SENTRY_DISABLE_THREAD_SANITIZER(
return nil;
# endif // SENTRY_HAS_UIKIT
}
#endif

#if SENTRY_UIKIT_AVAILABLE
- (SentryViewHierarchy *)viewHierarchy SENTRY_DISABLE_THREAD_SANITIZER(
"double-checked lock produce false alarms")
{
Expand Down
11 changes: 7 additions & 4 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,19 @@ @implementation SentryOptions {
// SentryCrashIntegration needs to be initialized before SentryAutoSessionTrackingIntegration.
// And SentrySessionReplayIntegration before SentryCrashIntegration.
NSMutableArray<Class> *defaultIntegrations = [NSMutableArray<Class> arrayWithObjects:
#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION
#if SENTRY_TARGET_REPLAY_SUPPORTED
[SentrySessionReplayIntegration class],
#endif
#endif // SENTRY_TARGET_REPLAY_SUPPORTED
[SentryCrashIntegration class],
#if SENTRY_HAS_UIKIT
[SentryAppStartTrackingIntegration class], [SentryFramesTrackingIntegration class],
[SentryPerformanceTrackingIntegration class], [SentryScreenshotIntegration class],
[SentryUIEventTrackingIntegration class], [SentryViewHierarchyIntegration class],
[SentryPerformanceTrackingIntegration class], [SentryUIEventTrackingIntegration class],
[SentryViewHierarchyIntegration class],
[SentryWatchdogTerminationTrackingIntegration class],
#endif // SENTRY_HAS_UIKIT
#if SENTRY_TARGET_REPLAY_SUPPORTED
[SentryScreenshotIntegration class],
#endif // SENTRY_TARGET_REPLAY_SUPPORTED
brustolin marked this conversation as resolved.
Show resolved Hide resolved
[SentryANRTrackingIntegration class], [SentryAutoBreadcrumbTrackingIntegration class],
[SentryAutoSessionTrackingIntegration class], [SentryCoreDataTrackingIntegration class],
[SentryFileIOTrackingIntegration class], [SentryNetworkTrackingIntegration class],
Expand Down
36 changes: 21 additions & 15 deletions Sources/Sentry/SentryScreenshot.m
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
#import "SentryScreenshot.h"

#if SENTRY_HAS_UIKIT
#if SENTRY_TARGET_REPLAY_SUPPORTED

# 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
2 changes: 1 addition & 1 deletion Sources/Sentry/SentryScreenshotIntegration.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#import "SentryScreenshotIntegration.h"

#if SENTRY_HAS_UIKIT
#if SENTRY_TARGET_REPLAY_SUPPORTED

# import "SentryAttachment.h"
# import "SentryCrashC.h"
Expand Down
2 changes: 1 addition & 1 deletion Sources/Sentry/include/SentryScreenshot.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#import "SentryDefines.h"

#if SENTRY_HAS_UIKIT
#if SENTRY_TARGET_REPLAY_SUPPORTED

NS_ASSUME_NONNULL_BEGIN

Expand Down
2 changes: 1 addition & 1 deletion Sources/Sentry/include/SentryScreenshotIntegration.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#import "SentryDefines.h"

#if SENTRY_HAS_UIKIT
#if SENTRY_TARGET_REPLAY_SUPPORTED

# import "SentryBaseIntegration.h"
# import "SentryClient+Private.h"
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] = []
}
102 changes: 58 additions & 44 deletions Sources/Swift/Tools/SentryViewPhotographer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,60 +37,74 @@ 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 viewSize = view.bounds.size

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, size: viewSize, masking: redact)
onComplete(screenshot)
}
}

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

return self.maskScreenshot(screenshot: image, size: viewSize, masking: redact)
}

private func maskScreenshot(screenshot image: UIImage, size: CGSize, masking: [RedactRegion]) -> UIImage {
brustolin marked this conversation as resolved.
Show resolved Hide resolved

let screenshot = UIGraphicsImageRenderer(size: size, format: .init(for: .init(displayScale: 1))).image { context in

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

let imageRect = CGRect(origin: .zero, size: size)
context.cgContext.addRect(CGRect(origin: CGPoint.zero, size: size))
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
Loading