diff --git a/CHANGELOG.md b/CHANGELOG.md index d5fe232fb89..fa6afb4856d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) ### Fixes diff --git a/Sources/Sentry/PrivateSentrySDKOnly.mm b/Sources/Sentry/PrivateSentrySDKOnly.mm index e1a04912632..635ee078a3b 100644 --- a/Sources/Sentry/PrivateSentrySDKOnly.mm +++ b/Sources/Sentry/PrivateSentrySDKOnly.mm @@ -274,7 +274,7 @@ + (SentryScreenFrames *)currentScreenFrames + (NSArray *)captureScreenshots { -#if SENTRY_HAS_UIKIT +#if SENTRY_TARGET_REPLAY_SUPPORTED return [SentryDependencyContainer.sharedInstance.screenshot appScreenshots]; #else SENTRY_LOG_DEBUG( diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m index 710470a6ada..1e6c3cb1313 100644 --- a/Sources/Sentry/SentryDependencyContainer.m +++ b/Sources/Sentry/SentryDependencyContainer.m @@ -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") { @@ -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") { diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 7f713ad7544..9d9fc80085c 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -59,16 +59,19 @@ @implementation SentryOptions { // SentryCrashIntegration needs to be initialized before SentryAutoSessionTrackingIntegration. // And SentrySessionReplayIntegration before SentryCrashIntegration. NSMutableArray *defaultIntegrations = [NSMutableArray 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 [SentryANRTrackingIntegration class], [SentryAutoBreadcrumbTrackingIntegration class], [SentryAutoSessionTrackingIntegration class], [SentryCoreDataTrackingIntegration class], [SentryFileIOTrackingIntegration class], [SentryNetworkTrackingIntegration class], diff --git a/Sources/Sentry/SentryScreenshot.m b/Sources/Sentry/SentryScreenshot.m index 50017f073df..f1d03090f26 100644 --- a/Sources/Sentry/SentryScreenshot.m +++ b/Sources/Sentry/SentryScreenshot.m @@ -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 -@implementation SentryScreenshot +@implementation SentryScreenshot { + SentryViewPhotographer *photographer; +} + +- (instancetype)init +{ + if (self = [super init]) { + photographer = [[SentryViewPhotographer alloc] + initWithRedactOptions:[[SentryRedactDefaultOptions alloc] init]]; + } + return self; +} - (NSArray *)appScreenshotsFromMainThread { @@ -41,7 +53,6 @@ - (void)saveScreenShots:(NSString *)imagesDirectoryPath - (NSArray *)appScreenshots { NSArray *windows = [SentryDependencyContainer.sharedInstance.application windows]; - NSMutableArray *result = [NSMutableArray arrayWithCapacity:windows.count]; for (UIWindow *window in windows) { @@ -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; } diff --git a/Sources/Sentry/SentryScreenshotIntegration.m b/Sources/Sentry/SentryScreenshotIntegration.m index 05e798e4f5b..e0e85c85946 100644 --- a/Sources/Sentry/SentryScreenshotIntegration.m +++ b/Sources/Sentry/SentryScreenshotIntegration.m @@ -1,6 +1,6 @@ #import "SentryScreenshotIntegration.h" -#if SENTRY_HAS_UIKIT +#if SENTRY_TARGET_REPLAY_SUPPORTED # import "SentryAttachment.h" # import "SentryCrashC.h" diff --git a/Sources/Sentry/include/SentryScreenshot.h b/Sources/Sentry/include/SentryScreenshot.h index 90c24b827a2..ac0635ff385 100644 --- a/Sources/Sentry/include/SentryScreenshot.h +++ b/Sources/Sentry/include/SentryScreenshot.h @@ -1,6 +1,6 @@ #import "SentryDefines.h" -#if SENTRY_HAS_UIKIT +#if SENTRY_TARGET_REPLAY_SUPPORTED NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/include/SentryScreenshotIntegration.h b/Sources/Sentry/include/SentryScreenshotIntegration.h index 161966ea0f3..6c2ba1351fc 100644 --- a/Sources/Sentry/include/SentryScreenshotIntegration.h +++ b/Sources/Sentry/include/SentryScreenshotIntegration.h @@ -1,6 +1,6 @@ #import "SentryDefines.h" -#if SENTRY_HAS_UIKIT +#if SENTRY_TARGET_REPLAY_SUPPORTED # import "SentryBaseIntegration.h" # import "SentryClient+Private.h" diff --git a/Sources/Swift/Protocol/SentryRedactOptions.swift b/Sources/Swift/Protocol/SentryRedactOptions.swift index 24560dddea7..26cc222a3d5 100644 --- a/Sources/Swift/Protocol/SentryRedactOptions.swift +++ b/Sources/Swift/Protocol/SentryRedactOptions.swift @@ -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] = [] +} diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift index 33d22c15e8a..f742f987250 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -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) {