From b893e9560b8367add97f29f8ed516a31ec94dde6 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 21 Jan 2025 16:01:47 +0100 Subject: [PATCH 01/18] Masking preview for SwiftUI --- CHANGELOG.md | 1 + .../iOS-SwiftUI/iOS-SwiftUI/ContentView.swift | 7 +- .../iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift | 2 +- Sentry.xcodeproj/project.pbxproj | 20 ++++++ Sources/Sentry/SentrySDK.m | 4 +- .../Preview/PreviewRedactOptions.swift | 18 +++++ .../Preview/SentryReplayMaskPreview.swift | 43 +++++++++++ .../SentryReplayMaskPreviewUIView.swift | 71 +++++++++++++++++++ .../SentryInternal/SentryInternal.h | 2 + .../Swift/Protocol/SentryRedactOptions.swift | 10 +-- .../Swift/Tools/SentryViewPhotographer.swift | 10 +-- .../Tools/SentryViewScreenshotProvider.swift | 2 +- 12 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift create mode 100644 Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift create mode 100644 Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b0a9093c7..0e3069a4e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Add protocol for custom screenName for UIViewControllers (#4646) - Allow hybrid SDK to set replay options tags information (#4710) - Add threshold to always log fatal logs (#4707) +- Masking preview for SwiftUI () ### Internal diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift index ea5d75249d..7eec775867 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift @@ -137,7 +137,7 @@ struct ContentView: View { return DataBag.shared.info["lastSpan"] as? Span } - + var body: some View { return SentryTracedView("Content View Body", waitForFullDisplay: true) { NavigationView { @@ -235,9 +235,10 @@ struct ContentView: View { .background(Color.white) } SecondView() - + Text(TTDInfo) .accessibilityIdentifier("TTDInfo") + } } } @@ -255,5 +256,7 @@ struct SecondView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() + .sentryReplayPreviewMask(opacity: 0.3) + } } diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift index 87dbe088b6..1881041b86 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift @@ -10,7 +10,7 @@ struct SwiftUIApp: App { options.debug = true options.tracesSampleRate = 1.0 options.profilesSampleRate = 1.0 - options.sessionReplay.sessionSampleRate = 1.0 + options.sessionReplay.sessionSampleRate = 0.0 options.initialScope = { scope in scope.injectGitInformation() return scope diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 5fb5bd8c36..01ccb7d4d6 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -870,6 +870,9 @@ D867063F27C3BC2400048851 /* SentryCoreDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063C27C3BC2400048851 /* SentryCoreDataTracker.h */; }; D86B6835294348A400B8B1FC /* SentryAttachment+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */; }; D86F419827C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */; }; + D8709AC42D3E9C63006C491E /* SentryReplayMaskPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */; }; + D8709ACB2D3F848E006C491E /* SentryReplayMaskPreviewUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */; }; + D8709ACD2D3F84CF006C491E /* PreviewRedactOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */; }; D8739CF32BECF70F007D2F66 /* SentryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8739CF22BECF70F007D2F66 /* SentryLevel.swift */; }; D8739CF92BECFFB5007D2F66 /* SentryTransactionNameSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */; }; D8739D142BEE5049007D2F66 /* SentryRRWebSpanEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8739D132BEE5049007D2F66 /* SentryRRWebSpanEvent.swift */; }; @@ -1976,6 +1979,9 @@ D86B6820293F39E000B8B1FC /* TestSentryViewHierarchy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestSentryViewHierarchy.h; sourceTree = ""; }; D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryAttachment+Private.h"; path = "include/SentryAttachment+Private.h"; sourceTree = ""; }; D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackerExtension.swift; sourceTree = ""; }; + D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayMaskPreview.swift; sourceTree = ""; }; + D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayMaskPreviewUIView.swift; sourceTree = ""; }; + D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewRedactOptions.swift; sourceTree = ""; }; D8739CF22BECF70F007D2F66 /* SentryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLevel.swift; sourceTree = ""; }; D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTransactionNameSource.swift; sourceTree = ""; }; D8739D132BEE5049007D2F66 /* SentryRRWebSpanEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRRWebSpanEvent.swift; sourceTree = ""; }; @@ -3759,6 +3765,7 @@ isa = PBXGroup; children = ( D8199DB429376ECC0074249E /* SentryInternal */, + D8709AC92D3F83A6006C491E /* Preview */, D8199DB529376ECC0074249E /* SentrySwiftUI.h */, D88D25E92B8E0BAC0073C3D5 /* module.modulemap */, D8199DB629376ECC0074249E /* SentryTracedView.swift */, @@ -3854,6 +3861,16 @@ name = CoreData; sourceTree = ""; }; + D8709AC92D3F83A6006C491E /* Preview */ = { + isa = PBXGroup; + children = ( + D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */, + D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */, + D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */, + ); + path = Preview; + sourceTree = ""; + }; D8739CF62BECFF86007D2F66 /* Log */ = { isa = PBXGroup; children = ( @@ -5289,7 +5306,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D8709AC42D3E9C63006C491E /* SentryReplayMaskPreview.swift in Sources */, D8199DC129376EEC0074249E /* SentryTracedView.swift in Sources */, + D8709ACD2D3F84CF006C491E /* PreviewRedactOptions.swift in Sources */, + D8709ACB2D3F848E006C491E /* SentryReplayMaskPreviewUIView.swift in Sources */, D8199DBF29376EE20074249E /* SentryInternal.m in Sources */, D8A65B5D2C98656800974B74 /* SentryReplayView.swift in Sources */, ); diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index 7454324759..11543d6f4c 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -203,6 +203,9 @@ + (void)setStartTimestamp:(NSDate *)value + (void)startWithOptions:(SentryOptions *)options { + // We save the options before checking for xcode preview because + // we will use this options in the preview + startOption = options; if ([SentryDependencyContainer.sharedInstance.processInfoWrapper .environment[SENTRY_XCODE_PREVIEW_ENVIRONMENT_KEY] isEqualToString:@"1"]) { // Using NSLog because SentryLog was not initialized yet. @@ -210,7 +213,6 @@ + (void)startWithOptions:(SentryOptions *)options return; } - startOption = options; [SentryLog configure:options.debug diagnosticLevel:options.diagnosticLevel]; // We accept the tradeoff that the SDK might not be fully initialized directly after diff --git a/Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift b/Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift new file mode 100644 index 0000000000..02fc6c3bbb --- /dev/null +++ b/Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift @@ -0,0 +1,18 @@ +#if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS) +import Sentry + +public class PreviewRedactOptions: SentryRedactOptions { + public let maskAllText: Bool + public let maskAllImages: Bool + public let maskedViewClasses: [AnyClass] + public let unmaskedViewClasses: [AnyClass] + + public init(maskAllText: Bool = true, maskAllImages: Bool = true, maskedViewClasses: [AnyClass] = [], unmaskedViewClasses: [AnyClass] = []) { + self.maskAllText = maskAllText + self.maskAllImages = maskAllImages + self.maskedViewClasses = maskedViewClasses + self.unmaskedViewClasses = unmaskedViewClasses + } +} + +#endif diff --git a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift new file mode 100644 index 0000000000..74b9561410 --- /dev/null +++ b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift @@ -0,0 +1,43 @@ +#if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS) +import Sentry +import SwiftUI +import UIKit + +#if CARTHAGE || SWIFT_PACKAGE +@_implementationOnly import SentryInternal +#endif + +@available(iOS 13, macOS 10.15, tvOS 13, *) +struct SentryReplayMaskPreview: ViewModifier { + let redactOptions: SentryRedactOptions + let opacity: Float + func body(content: Content) -> some View { + content.overlay(SentryReplayPreviewView(redactOptions: redactOptions, opacity: opacity)) + } +} + +@available(iOS 13, macOS 10.15, tvOS 13, *) +public extension View { + func sentryReplayPreviewMask(redactOptions: SentryRedactOptions? = nil, opacity: Float = 1) -> some View { + let options = redactOptions ?? SentrySDK.options?.sessionReplay ?? PreviewRedactOptions() + return modifier(SentryReplayMaskPreview(redactOptions: options, opacity: opacity)) + } +} + +@available(iOS 13, macOS 10.15, tvOS 13, *) +struct SentryReplayPreviewView: UIViewRepresentable { + let redactOptions: SentryRedactOptions + let opacity: Float + + func makeUIView(context: Context) -> UIView { + let view = SentryReplayMaskPreviewUIView(redactOptions: redactOptions) + view.isUserInteractionEnabled = false + return view + } + + func updateUIView(_ uiView: UIView, context: Context) { + (uiView as? SentryReplayMaskPreviewUIView)?.opacity = opacity + } +} + +#endif diff --git a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift new file mode 100644 index 0000000000..a86292a2ae --- /dev/null +++ b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift @@ -0,0 +1,71 @@ +#if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS) +import Sentry +import SwiftUI +import UIKit + +#if CARTHAGE || SWIFT_PACKAGE +@_implementationOnly import SentryInternal +#endif + +class SentryReplayMaskPreviewUIView: UIView { + private let photographer: SentryViewPhotographer + private var displayLink: CADisplayLink? + private var imageView = UIImageView() + + var opacity: Float { + get { return Float(imageView.alpha) } + set { imageView.alpha = CGFloat(newValue)} + } + + init(redactOptions: SentryRedactOptions) { + self.photographer = SentryViewPhotographer(renderer: PreviewRederer(), redactOptions: redactOptions) + super.init(frame: .zero) + self.isUserInteractionEnabled = false + imageView.isUserInteractionEnabled = false + imageView.sentryReplayUnmask() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func didMoveToSuperview() { + if ProcessInfo.processInfo.environment[SENTRY_XCODE_PREVIEW_ENVIRONMENT_KEY] == "1" { + displayLink = CADisplayLink(target: self, selector: #selector(update)) + displayLink?.add(to: .main, forMode: .common) + } else { + print("[SENTRY] [WARNING] SentryReplayMaskPreview is not meant to be used in your app, only with SwiftUI Previews.") + } + } + + @objc + private func update() { + guard let window = self.window else { return } + self.photographer.image(view: window) { image in + DispatchQueue.main.async { + self.showImage(image: image) + } + } + } + + private func showImage(image: UIImage) { + guard let window = super.window else { return } + if imageView.superview != window { + window.addSubview(imageView) + } + imageView.image = image + imageView.frame = window.bounds + } +} + +class PreviewRederer: ViewRenderer { + func render(view: UIView) -> UIImage { + return UIGraphicsImageRenderer(size: view.frame.size, format: .init(for: .init(displayScale: 1))).image { _ in + // Creates a transparent image of the view size that will be used to drawn the redact regions. + // Transparent background is the default, so no additional drawing is required. + // Left blank on purpose + } + } +} + +#endif diff --git a/Sources/SentrySwiftUI/SentryInternal/SentryInternal.h b/Sources/SentrySwiftUI/SentryInternal/SentryInternal.h index d327121f0d..438d64527d 100644 --- a/Sources/SentrySwiftUI/SentryInternal/SentryInternal.h +++ b/Sources/SentrySwiftUI/SentryInternal/SentryInternal.h @@ -24,6 +24,8 @@ NS_ASSUME_NONNULL_BEGIN +extern NSString *const SENTRY_XCODE_PREVIEW_ENVIRONMENT_KEY; + typedef NS_ENUM(NSInteger, SentryTransactionNameSource); @class SentrySpanId; diff --git a/Sources/Swift/Protocol/SentryRedactOptions.swift b/Sources/Swift/Protocol/SentryRedactOptions.swift index 26cc222a3d..e352d45e98 100644 --- a/Sources/Swift/Protocol/SentryRedactOptions.swift +++ b/Sources/Swift/Protocol/SentryRedactOptions.swift @@ -1,7 +1,7 @@ import Foundation @objc -protocol SentryRedactOptions { +public protocol SentryRedactOptions { var maskAllText: Bool { get } var maskAllImages: Bool { get } var maskedViewClasses: [AnyClass] { get } @@ -10,8 +10,8 @@ protocol SentryRedactOptions { @objcMembers final class SentryRedactDefaultOptions: NSObject, SentryRedactOptions { - var maskAllText: Bool = true - var maskAllImages: Bool = true - var maskedViewClasses: [AnyClass] = [] - var unmaskedViewClasses: [AnyClass] = [] + public var maskAllText: Bool = true + public var maskAllImages: Bool = true + public var maskedViewClasses: [AnyClass] = [] + public var unmaskedViewClasses: [AnyClass] = [] } diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift index a7442faf63..359c4eba19 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -6,7 +6,7 @@ import CoreGraphics import Foundation import UIKit -protocol ViewRenderer { +public protocol ViewRenderer { func render(view: UIView) -> UIImage } @@ -20,24 +20,24 @@ class DefaultViewRenderer: ViewRenderer { } @objcMembers -class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { +public class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { private let redactBuilder: UIRedactBuilder private let dispatchQueue = SentryDispatchQueueWrapper() var renderer: ViewRenderer - init(renderer: ViewRenderer, redactOptions: SentryRedactOptions) { + public init(renderer: ViewRenderer, redactOptions: SentryRedactOptions) { self.renderer = renderer redactBuilder = UIRedactBuilder(options: redactOptions) super.init() } - init(redactOptions: SentryRedactOptions) { + public init(redactOptions: SentryRedactOptions) { self.renderer = DefaultViewRenderer() self.redactBuilder = UIRedactBuilder(options: redactOptions) } - func image(view: UIView, onComplete: @escaping ScreenshotCallback) { + public func image(view: UIView, onComplete: @escaping ScreenshotCallback) { let redact = redactBuilder.redactRegionsFor(view: view) let image = renderer.render(view: view) let viewSize = view.bounds.size diff --git a/Sources/Swift/Tools/SentryViewScreenshotProvider.swift b/Sources/Swift/Tools/SentryViewScreenshotProvider.swift index 0b99bc1bff..d399e50335 100644 --- a/Sources/Swift/Tools/SentryViewScreenshotProvider.swift +++ b/Sources/Swift/Tools/SentryViewScreenshotProvider.swift @@ -3,7 +3,7 @@ import Foundation import UIKit -typealias ScreenshotCallback = (UIImage) -> Void +public typealias ScreenshotCallback = (UIImage) -> Void @objc protocol SentryViewScreenshotProvider: NSObjectProtocol { From ff2ad506ea454b193895b89ecab62d55a370ae29 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 21 Jan 2025 16:04:12 +0100 Subject: [PATCH 02/18] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e3069a4e8..83197adab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ - Add protocol for custom screenName for UIViewControllers (#4646) - Allow hybrid SDK to set replay options tags information (#4710) - Add threshold to always log fatal logs (#4707) -- Masking preview for SwiftUI () +- Session replay masking preview for SwiftUI (#4737) ### Internal From afcb89e2cb992faaf895564cea138190c7045582 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 23 Jan 2025 10:12:34 +0100 Subject: [PATCH 03/18] Update Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift --- Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift index 1881041b86..87dbe088b6 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift @@ -10,7 +10,7 @@ struct SwiftUIApp: App { options.debug = true options.tracesSampleRate = 1.0 options.profilesSampleRate = 1.0 - options.sessionReplay.sessionSampleRate = 0.0 + options.sessionReplay.sessionSampleRate = 1.0 options.initialScope = { scope in scope.injectGitInformation() return scope From efdbf25eb3f48f6abdd908b05cc09b820852eab9 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 23 Jan 2025 10:50:07 +0100 Subject: [PATCH 04/18] wip --- Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift | 6 +++++- .../Preview/SentryReplayMaskPreview.swift | 6 +++--- .../Preview/SentryReplayMaskPreviewUIView.swift | 12 +++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift index 7eec775867..c6d0475c5c 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift @@ -239,10 +239,15 @@ struct ContentView: View { Text(TTDInfo) .accessibilityIdentifier("TTDInfo") + TextField("DAE", text: $dae) + .background(Color.red) + TextField("Ola", text: $dae).sentryReplayUnmask() } } } } + + @State var dae: String = "" } struct SecondView: View { @@ -257,6 +262,5 @@ struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() .sentryReplayPreviewMask(opacity: 0.3) - } } diff --git a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift index 74b9561410..e03d1f0784 100644 --- a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift +++ b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift @@ -29,14 +29,14 @@ struct SentryReplayPreviewView: UIViewRepresentable { let redactOptions: SentryRedactOptions let opacity: Float - func makeUIView(context: Context) -> UIView { + func makeUIView(context: Context) -> SentryReplayMaskPreviewUIView { let view = SentryReplayMaskPreviewUIView(redactOptions: redactOptions) view.isUserInteractionEnabled = false return view } - func updateUIView(_ uiView: UIView, context: Context) { - (uiView as? SentryReplayMaskPreviewUIView)?.opacity = opacity + func updateUIView(_ uiView: SentryReplayMaskPreviewUIView, context: Context) { + uiView.opacity = opacity } } diff --git a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift index a86292a2ae..8fdbf8481e 100644 --- a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift +++ b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift @@ -7,10 +7,16 @@ import UIKit @_implementationOnly import SentryInternal #endif +class PreviewImageView: UIImageView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } +} + class SentryReplayMaskPreviewUIView: UIView { private let photographer: SentryViewPhotographer private var displayLink: CADisplayLink? - private var imageView = UIImageView() + private var imageView = PreviewImageView() var opacity: Float { get { return Float(imageView.alpha) } @@ -56,6 +62,10 @@ class SentryReplayMaskPreviewUIView: UIView { imageView.image = image imageView.frame = window.bounds } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } } class PreviewRederer: ViewRenderer { From ef266202b19b4b8b5fbdb63cb0b40f434baf5d31 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 24 Jan 2025 07:48:44 +0100 Subject: [PATCH 05/18] Apply suggestions from code review Co-authored-by: Philipp Hofmann --- Sources/Sentry/SentrySDK.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index 11543d6f4c..284bcd0c5e 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -203,7 +203,7 @@ + (void)setStartTimestamp:(NSDate *)value + (void)startWithOptions:(SentryOptions *)options { - // We save the options before checking for xcode preview because + // We save the options before checking for Xcode preview because // we will use this options in the preview startOption = options; if ([SentryDependencyContainer.sharedInstance.processInfoWrapper From b402fe68869184b725762bdc0e4250fc73a198b7 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 24 Jan 2025 07:49:20 +0100 Subject: [PATCH 06/18] more fixes --- Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift | 2 +- .../Preview/SentryReplayMaskPreviewUIView.swift | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift index e03d1f0784..9d099654b5 100644 --- a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift +++ b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift @@ -12,7 +12,7 @@ struct SentryReplayMaskPreview: ViewModifier { let redactOptions: SentryRedactOptions let opacity: Float func body(content: Content) -> some View { - content.overlay(SentryReplayPreviewView(redactOptions: redactOptions, opacity: opacity)) + content.overlay(SentryReplayPreviewView(redactOptions: redactOptions, opacity: opacity).disabled(true)) } } diff --git a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift index 8fdbf8481e..5de867f19e 100644 --- a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift +++ b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift @@ -1,6 +1,5 @@ #if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS) import Sentry -import SwiftUI import UIKit #if CARTHAGE || SWIFT_PACKAGE @@ -31,6 +30,10 @@ class SentryReplayMaskPreviewUIView: UIView { imageView.sentryReplayUnmask() } + deinit { + displayLink?.invalidate() + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } From 88fce2ddd451c2c1b4ff71de25619c5b313b3156 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 24 Jan 2025 08:00:17 +0100 Subject: [PATCH 07/18] Update SentryReplayMaskPreview.swift --- Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift index 9d099654b5..8e946a5071 100644 --- a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift +++ b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift @@ -30,9 +30,7 @@ struct SentryReplayPreviewView: UIViewRepresentable { let opacity: Float func makeUIView(context: Context) -> SentryReplayMaskPreviewUIView { - let view = SentryReplayMaskPreviewUIView(redactOptions: redactOptions) - view.isUserInteractionEnabled = false - return view + return SentryReplayMaskPreviewUIView(redactOptions: redactOptions) } func updateUIView(_ uiView: SentryReplayMaskPreviewUIView, context: Context) { From fd559e904c846d4f7567399e9314006aa89828ae Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 24 Jan 2025 08:06:48 +0100 Subject: [PATCH 08/18] Update SentrySDKTests.swift --- Tests/SentryTests/SentrySDKTests.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Tests/SentryTests/SentrySDKTests.swift b/Tests/SentryTests/SentrySDKTests.swift index 9cfef1dea7..b0e507d9c4 100644 --- a/Tests/SentryTests/SentrySDKTests.swift +++ b/Tests/SentryTests/SentrySDKTests.swift @@ -195,10 +195,7 @@ class SentrySDKTests: XCTestCase { } func testDontStartInsideXcodePreview() { - let testProcessInfoWrapper = TestSentryNSProcessInfoWrapper() - testProcessInfoWrapper.overrides.environment = ["XCODE_RUNNING_FOR_PREVIEWS": "1"] - - SentryDependencyContainer.sharedInstance().processInfoWrapper = testProcessInfoWrapper + startprocessInfoWrapperForPreview() SentrySDK.start { options in options.debug = true @@ -561,6 +558,13 @@ class SentrySDKTests: XCTestCase { SentrySDK.start(options: fixture.options) XCTAssertEqual(SentrySDK.options, fixture.options) } + + func testGlobalOptionsForPreview() { + startprocessInfoWrapperForPreview() + + SentrySDK.start(options: fixture.options) + XCTAssertEqual(SentrySDK.options, fixture.options) + } #if SENTRY_HAS_UIKIT func testSetAppStartMeasurement_CallsPrivateSDKCallback() { @@ -974,6 +978,12 @@ class SentrySDKTests: XCTestCase { private func advanceTime(bySeconds: TimeInterval) { fixture.currentDate.setDate(date: SentryDependencyContainer.sharedInstance().dateProvider.date().addingTimeInterval(bySeconds)) } + + private func startprocessInfoWrapperForPreview() { + let testProcessInfoWrapper = TestSentryNSProcessInfoWrapper() + testProcessInfoWrapper.overrides.environment = ["XCODE_RUNNING_FOR_PREVIEWS": "1"] + SentryDependencyContainer.sharedInstance().processInfoWrapper = testProcessInfoWrapper + } } /// Tests in this class aren't part of SentrySDKTests because we need would need to undo a bunch of operations From f0a3ba7e18bf601eb8f2bbf2dee3179279aa6c7e Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Fri, 24 Jan 2025 08:08:00 +0100 Subject: [PATCH 09/18] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c7dd1b301..02fde14bf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ - Use strlcpy to save session replay info path (#4740) - `sentryReplayUnmask` and `sentryReplayUnmask` preventing interaction (#4749) +### Features +- Session replay masking preview for SwiftUI (#4737) + ## 8.44.0-beta.1 ### Fixes @@ -24,7 +27,6 @@ - Add protocol for custom screenName for UIViewControllers (#4646) - Allow hybrid SDK to set replay options tags information (#4710) - Add threshold to always log fatal logs (#4707) -- Session replay masking preview for SwiftUI (#4737) ### Internal From df45376deac54b72f10326b9f1e9a0ab27d3dc10 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 28 Jan 2025 17:01:43 +0100 Subject: [PATCH 10/18] Add `showMaskPreview` to `SentrySDK.replay` api to debug replay masking --- CHANGELOG.md | 3 +- .../iOS-Swift/Base.lproj/Main.storyboard | 25 +- .../iOS-Swift/ExtraViewController.swift | 4 + Sentry.xcodeproj/project.pbxproj | 22 +- Sources/Sentry/Public/SentryReplayApi.h | 11 + Sources/Sentry/SentryReplayApi.m | 12 + .../Sentry/SentrySessionReplayIntegration.m | 244 ++++++++++-------- .../SentrySessionReplayIntegration.h | 2 + .../Preview/SentryMaskingPreviewView.swift | 67 +++++ .../Swift/Tools/SentryViewPhotographer.swift | 4 +- Sources/Swift/Tools/UIImageHelper.swift | 3 +- Sources/Swift/Tools/UIRedactBuilder.swift | 2 +- 12 files changed, 268 insertions(+), 131 deletions(-) create mode 100644 Sources/Swift/Integrations/SessionReplay/Preview/SentryMaskingPreviewView.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 02fde14bf1..630858ab7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ - `sentryReplayUnmask` and `sentryReplayUnmask` preventing interaction (#4749) ### Features -- Session replay masking preview for SwiftUI (#4737) + +- Add `showMaskPreview` to `SentrySDK.replay` api to debug replay masking () ## 8.44.0-beta.1 diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index c00b0caea3..1b14c7a24c 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -920,7 +920,7 @@ + diff --git a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift index 6a14cd5644..c18d6b2f99 100644 --- a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift @@ -218,6 +218,10 @@ class ExtraViewController: UIViewController { displayStringForUITest(string: appSupportDirectory) } + @IBAction func showMaskingPreview(_ sender: Any) { + SentrySDK.replay.showMaskPreview(0.5) + } + func displayStringForUITest(string: String) { dataMarshalingField.text = string dataMarshalingField.isHidden = false diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 5bd794f975..c89cd698fe 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -788,10 +788,10 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; - D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */; }; - D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */; }; D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; + D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */; }; + D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */; }; D48E8B9D2D3E82AC0032E35E /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B9C2D3E82AC0032E35E /* SentrySpanOperation.swift */; }; D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; @@ -893,6 +893,7 @@ D88817D826D7149100BF2251 /* SentryTraceContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D88817D626D7149100BF2251 /* SentryTraceContext.m */; }; D88817DA26D72AB800BF2251 /* SentryTraceContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D88817D926D72AB800BF2251 /* SentryTraceContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */; }; + D88B30A92D48D8C3008DE513 /* SentryMaskingPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88B30A82D48D88E008DE513 /* SentryMaskingPreviewView.swift */; }; D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */; }; D8A3649C2C91AA3300AC569B /* SentryReplayApi.m in Sources */ = {isa = PBXBuildFile; fileRef = D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */; }; D8A3649D2C91AA3300AC569B /* SentryReplayApi.h in Headers */ = {isa = PBXBuildFile; fileRef = D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1895,10 +1896,10 @@ A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; - D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOriginTests.swift; sourceTree = ""; }; - D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = ""; }; + D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOriginTests.swift; sourceTree = ""; }; + D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48E8B9C2D3E82AC0032E35E /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; @@ -2008,6 +2009,7 @@ D88817D626D7149100BF2251 /* SentryTraceContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTraceContext.m; sourceTree = ""; }; D88817D926D72AB800BF2251 /* SentryTraceContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryTraceContext.h; path = Public/SentryTraceContext.h; sourceTree = ""; }; D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceStateTests.swift; sourceTree = ""; }; + D88B30A82D48D88E008DE513 /* SentryMaskingPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMaskingPreviewView.swift; sourceTree = ""; }; D88D25E92B8E0BAC0073C3D5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKIntegrationTestsBase.swift; sourceTree = ""; }; D8A3649A2C91AA3300AC569B /* SentryReplayApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayApi.h; path = Public/SentryReplayApi.h; sourceTree = ""; }; @@ -3640,8 +3642,6 @@ 8ECC674325C23A1F000E2BF6 /* SentrySpanContext.m */, 622C08D929E554B9002571D4 /* SentrySpanContext+Private.h */, 8E4E7C7325DAAB49006AB9E2 /* SentrySpanProtocol.h */, - 7B3B83712833832B0001FDEB /* SentrySpanOperations.h */, - 622C08D729E546F4002571D4 /* SentryTraceOrigins.h */, 8E4E7C6C25DAAAFE006AB9E2 /* SentrySpan.h */, 84A789092C0E9F5800FF0803 /* SentrySpan+Private.h */, 8EC3AE7925CA23B600E7591A /* SentrySpan.m */, @@ -3986,6 +3986,14 @@ path = CoreData; sourceTree = ""; }; + D88B30A72D48D87F008DE513 /* Preview */ = { + isa = PBXGroup; + children = ( + D88B30A82D48D88E008DE513 /* SentryMaskingPreviewView.swift */, + ); + path = Preview; + sourceTree = ""; + }; D8AB40D92806EBDC00E5E9F7 /* Screenshot */ = { isa = PBXGroup; children = ( @@ -4032,6 +4040,7 @@ isa = PBXGroup; children = ( D81988C12BEC18710020E36C /* RRWeb */, + D88B30A72D48D87F008DE513 /* Preview */, D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */, D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */, D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */, @@ -4916,6 +4925,7 @@ 7BBD18932449BEDD00427C76 /* SentryDefaultRateLimits.m in Sources */, 7BD729982463E93500EA3610 /* SentryDateUtil.m in Sources */, 639FCF9D1EBC7F9500778193 /* SentryThread.m in Sources */, + D88B30A92D48D8C3008DE513 /* SentryMaskingPreviewView.swift in Sources */, 849B8F992C6E906900148E1F /* SentryUserFeedbackFormConfiguration.swift in Sources */, 8E8C57A225EEFC07001CEEFA /* SentrySampling.m in Sources */, 8454CF8D293EAF9A006AC140 /* SentryMetricProfiler.mm in Sources */, diff --git a/Sources/Sentry/Public/SentryReplayApi.h b/Sources/Sentry/Public/SentryReplayApi.h index f9f001e9c1..6584c5ead8 100644 --- a/Sources/Sentry/Public/SentryReplayApi.h +++ b/Sources/Sentry/Public/SentryReplayApi.h @@ -56,6 +56,17 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)stop; +/** + * Shows a overlay on the app the debug session replay masking. + */ +- (void)showMaskPreview; + +/** + * Shows a overlay on the app to debug session replay masking + * with given overlay opacity. + */ +- (void)showMaskPreview:(CGFloat)opacity; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryReplayApi.m b/Sources/Sentry/SentryReplayApi.m index d28c323ec3..0fc387d7cc 100644 --- a/Sources/Sentry/SentryReplayApi.m +++ b/Sources/Sentry/SentryReplayApi.m @@ -73,6 +73,18 @@ - (void)stop [replayIntegration stop]; } +- (void)showMaskPreview { + [self showMaskPreview:1]; +} + +- (void)showMaskPreview:(CGFloat)opacity { + SentrySessionReplayIntegration *replayIntegration + = (SentrySessionReplayIntegration *)[SentrySDK.currentHub + getInstalledIntegration:SentrySessionReplayIntegration.class]; + + [replayIntegration showMaskPreview:opacity]; +} + @end #endif diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index 1ebb336fd2..bc7899cec1 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -50,6 +50,7 @@ @implementation SentrySessionReplayIntegration { id _rateLimits; id _currentScreenshotProvider; id _currentBreadcrumbConverter; + SentryMaskingPreviewView * previewView; // We need to use this variable to identify whether rate limiting was ever activated for session // replay in this session, instead of always looking for the rate status in `SentryRateLimits` // This is the easiest way to ensure segment 0 will always reach the server, because session @@ -77,7 +78,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options if ([super installWithOptions:options] == NO) { return NO; } - + [self setupWith:options.sessionReplay enableTouchTracker:options.enableSwizzling]; return YES; } @@ -87,30 +88,30 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL) _replayOptions = replayOptions; _viewPhotographer = [[SentryViewPhotographer alloc] initWithRedactOptions:replayOptions]; _rateLimits = SentryDependencyContainer.sharedInstance.rateLimits; - + if (touchTracker) { _touchTracker = [[SentryTouchTracker alloc] - initWithDateProvider:SentryDependencyContainer.sharedInstance.dateProvider - scale:replayOptions.sizeScale]; + initWithDateProvider:SentryDependencyContainer.sharedInstance.dateProvider + scale:replayOptions.sizeScale]; [self swizzleApplicationTouch]; } - + _notificationCenter = SentryDependencyContainer.sharedInstance.notificationCenterWrapper; - + [self moveCurrentReplay]; [self cleanUp]; - + [SentrySDK.currentHub registerSessionListener:self]; [SentryGlobalEventProcessor.shared - addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { - if (event.isCrashEvent) { - [self resumePreviousSessionReplay:event]; - } else { - [self.sessionReplay captureReplayForEvent:event]; - } - return event; - }]; - + addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { + if (event.isCrashEvent) { + [self resumePreviousSessionReplay:event]; + } else { + [self.sessionReplay captureReplayForEvent:event]; + } + return event; + }]; + [SentryDependencyContainer.sharedInstance.reachability addObserver:self]; } @@ -119,11 +120,11 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL) NSURL *dir = [self replayDirectory]; NSURL *lastReplayUrl = [dir URLByAppendingPathComponent:SENTRY_LAST_REPLAY]; NSData *lastReplay = [NSData dataWithContentsOfURL:lastReplayUrl]; - + if (lastReplay == nil) { return nil; } - + return [SentrySerialization deserializeDictionaryFromJsonData:lastReplay]; } @@ -137,54 +138,54 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event { NSURL *dir = [self replayDirectory]; NSDictionary *jsonObject = [self lastReplayInfo]; - + if (jsonObject == nil) { return; } - + SentryId *replayId = jsonObject[@"replayId"] - ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] - : [[SentryId alloc] init]; + ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] + : [[SentryId alloc] init]; NSURL *lastReplayURL = [dir URLByAppendingPathComponent:jsonObject[@"path"]]; - + SentryCrashReplay crashInfo = { 0 }; bool hasCrashInfo = sentrySessionReplaySync_readInfo(&crashInfo, - [[lastReplayURL URLByAppendingPathComponent:@"crashInfo"].path - cStringUsingEncoding:NSUTF8StringEncoding]); - + [[lastReplayURL URLByAppendingPathComponent:@"crashInfo"].path + cStringUsingEncoding:NSUTF8StringEncoding]); + SentryReplayType type = hasCrashInfo ? SentryReplayTypeSession : SentryReplayTypeBuffer; NSTimeInterval duration - = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration; + = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration; int segmentId = hasCrashInfo ? crashInfo.segmentId + 1 : 0; - + if (type == SentryReplayTypeBuffer) { float errorSampleRate = [jsonObject[@"errorSampleRate"] floatValue]; if ([SentryDependencyContainer.sharedInstance.random nextNumber] >= errorSampleRate) { return; } } - + SentryOnDemandReplay *resumeReplayMaker = - [[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path]; + [[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path]; resumeReplayMaker.bitRate = _replayOptions.replayBitRate; resumeReplayMaker.videoScale = _replayOptions.sizeScale; - + NSDate *beginning = hasCrashInfo - ? [NSDate dateWithTimeIntervalSinceReferenceDate:crashInfo.lastSegmentEnd] - : [resumeReplayMaker oldestFrameDate]; - + ? [NSDate dateWithTimeIntervalSinceReferenceDate:crashInfo.lastSegmentEnd] + : [resumeReplayMaker oldestFrameDate]; + if (beginning == nil) { return; // no frames to send } - + SentryReplayType _type = type; int _segmentId = segmentId; - + NSError *error; NSArray *videos = - [resumeReplayMaker createVideoWithBeginning:beginning - end:[beginning dateByAddingTimeInterval:duration] - error:&error]; + [resumeReplayMaker createVideoWithBeginning:beginning + end:[beginning dateByAddingTimeInterval:duration] + error:&error]; if (videos == nil) { SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); return; @@ -194,12 +195,12 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event // type buffer is only for the first segment _type = SentryReplayTypeSession; } - + NSMutableDictionary *eventContext = event.context.mutableCopy; eventContext[@"replay"] = - [NSDictionary dictionaryWithObjectsAndKeys:replayId.sentryIdString, @"replay_id", nil]; + [NSDictionary dictionaryWithObjectsAndKeys:replayId.sentryIdString, @"replay_id", nil]; event.context = eventContext; - + if ([NSFileManager.defaultManager removeItemAtURL:lastReplayURL error:&error] == NO) { SENTRY_LOG_ERROR(@"Can`t delete '%@': %@", SENTRY_LAST_REPLAY, error); } @@ -218,28 +219,28 @@ - (void)captureVideo:(SentryVideoInfo *)video SentryReplayRecording *recording = [[SentryReplayRecording alloc] initWithSegmentId:segment video:video extraEvents:@[]]; - + [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:video.path]; - + NSError *error = nil; if (![[NSFileManager defaultManager] removeItemAtURL:video.path error:&error]) { SENTRY_LOG_DEBUG( - @"Could not delete replay segment from disk: %@", error.localizedDescription); + @"Could not delete replay segment from disk: %@", error.localizedDescription); } } - (void)startSession { [self.sessionReplay pause]; - + _startedAsFullSession = [self shouldReplayFullSession:_replayOptions.sessionSampleRate]; - + if (!_startedAsFullSession && _replayOptions.onErrorSampleRate == 0) { return; } - + [self runReplayForAvailableWindow]; } @@ -260,8 +261,8 @@ - (void)newSceneActivate { if (@available(iOS 13.0, tvOS 13.0, *)) { [SentryDependencyContainer.sharedInstance.notificationCenterWrapper - removeObserver:self - name:UISceneDidActivateNotification]; + removeObserver:self + name:UISceneDidActivateNotification]; [self startWithOptions:_replayOptions fullSession:_startedAsFullSession]; } } @@ -270,10 +271,10 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions fullSession:(BOOL)shouldReplayFullSession { [self startWithOptions:replayOptions - screenshotProvider:_currentScreenshotProvider ?: _viewPhotographer - breadcrumbConverter:_currentBreadcrumbConverter - ?: [[SentrySRDefaultBreadcrumbConverter alloc] init] - fullSession:shouldReplayFullSession]; + screenshotProvider:_currentScreenshotProvider ?: _viewPhotographer + breadcrumbConverter:_currentBreadcrumbConverter + ?: [[SentrySRDefaultBreadcrumbConverter alloc] init] + fullSession:shouldReplayFullSession]; } - (void)startWithOptions:(SentryReplayOptions *)replayOptions @@ -284,53 +285,53 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions NSURL *docs = [self replayDirectory]; NSString *currentSession = [NSUUID UUID].UUIDString; docs = [docs URLByAppendingPathComponent:currentSession]; - + if (![NSFileManager.defaultManager fileExistsAtPath:docs.path]) { [NSFileManager.defaultManager createDirectoryAtURL:docs withIntermediateDirectories:YES attributes:nil error:nil]; } - + SentryOnDemandReplay *replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:docs.path]; replayMaker.bitRate = replayOptions.replayBitRate; replayMaker.videoScale = replayOptions.sizeScale; replayMaker.cacheMaxSize - = (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + 1 - : replayOptions.errorReplayDuration + 1); - + = (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + 1 + : replayOptions.errorReplayDuration + 1); + dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class( - DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_LOW, 0); + DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_LOW, 0); SentryDispatchQueueWrapper *dispatchQueue = - [[SentryDispatchQueueWrapper alloc] initWithName:"io.sentry.session-replay" - attributes:attributes]; - + [[SentryDispatchQueueWrapper alloc] initWithName:"io.sentry.session-replay" + attributes:attributes]; + self.sessionReplay = [[SentrySessionReplay alloc] - initWithReplayOptions:replayOptions - replayFolderPath:docs - screenshotProvider:screenshotProvider - replayMaker:replayMaker - breadcrumbConverter:breadcrumbConverter - touchTracker:_touchTracker - dateProvider:SentryDependencyContainer.sharedInstance.dateProvider - delegate:self - dispatchQueue:dispatchQueue - displayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]]; - + initWithReplayOptions:replayOptions + replayFolderPath:docs + screenshotProvider:screenshotProvider + replayMaker:replayMaker + breadcrumbConverter:breadcrumbConverter + touchTracker:_touchTracker + dateProvider:SentryDependencyContainer.sharedInstance.dateProvider + delegate:self + dispatchQueue:dispatchQueue + displayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]]; + [self.sessionReplay - startWithRootView:SentryDependencyContainer.sharedInstance.application.windows.firstObject - fullSession:shouldReplayFullSession]; - + startWithRootView:SentryDependencyContainer.sharedInstance.application.windows.firstObject + fullSession:shouldReplayFullSession]; + [_notificationCenter addObserver:self selector:@selector(pause) name:UIApplicationDidEnterBackgroundNotification object:nil]; - + [_notificationCenter addObserver:self selector:@selector(resume) name:UIApplicationDidBecomeActiveNotification object:nil]; - + [self saveCurrentSessionInfo:self.sessionReplay.sessionReplayId path:docs.path options:replayOptions]; @@ -339,7 +340,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions - (NSURL *)replayDirectory { NSURL *dir = - [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; + [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; return [dir URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; } @@ -348,20 +349,20 @@ - (void)saveCurrentSessionInfo:(SentryId *)sessionId options:(SentryReplayOptions *)options { NSDictionary *info = - [[NSDictionary alloc] initWithObjectsAndKeys:sessionId.sentryIdString, @"replayId", - path.lastPathComponent, @"path", @(options.onErrorSampleRate), @"errorSampleRate", nil]; - + [[NSDictionary alloc] initWithObjectsAndKeys:sessionId.sentryIdString, @"replayId", + path.lastPathComponent, @"path", @(options.onErrorSampleRate), @"errorSampleRate", nil]; + NSData *data = [SentrySerialization dataWithJSONObject:info]; - + NSString *infoPath = [[path stringByDeletingLastPathComponent] - stringByAppendingPathComponent:SENTRY_CURRENT_REPLAY]; + stringByAppendingPathComponent:SENTRY_CURRENT_REPLAY]; if ([NSFileManager.defaultManager fileExistsAtPath:infoPath]) { [NSFileManager.defaultManager removeItemAtPath:infoPath error:nil]; } [data writeToFile:infoPath atomically:YES]; - + sentrySessionReplaySync_start([[path stringByAppendingPathComponent:@"crashInfo"] - cStringUsingEncoding:NSUTF8StringEncoding]); + cStringUsingEncoding:NSUTF8StringEncoding]); } - (void)moveCurrentReplay @@ -369,7 +370,7 @@ - (void)moveCurrentReplay NSURL *path = [self replayDirectory]; NSURL *current = [path URLByAppendingPathComponent:SENTRY_CURRENT_REPLAY]; NSURL *last = [path URLByAppendingPathComponent:SENTRY_LAST_REPLAY]; - + NSError *error; if ([NSFileManager.defaultManager fileExistsAtPath:last.path]) { if ([NSFileManager.defaultManager removeItemAtURL:last error:&error] == NO) { @@ -377,7 +378,7 @@ - (void)moveCurrentReplay return; } } - + if ([NSFileManager.defaultManager moveItemAtURL:current toURL:last error:nil] == NO) { SENTRY_LOG_ERROR(@"Could not move 'currentreplay' to 'lastreplay': %@", error); } @@ -388,7 +389,7 @@ - (void)cleanUp NSURL *replayDir = [self replayDirectory]; NSDictionary *lastReplayInfo = [self lastReplayInfo]; NSString *lastReplayFolder = lastReplayInfo[@"path"]; - + SentryFileManager *fileManager = SentryDependencyContainer.sharedInstance.fileManager; // Mapping replay folder here and not in dispatched queue to prevent a race condition between // listing files and creating a new replay session. @@ -396,16 +397,16 @@ - (void)cleanUp if (replayFiles.count == 0) { return; } - + [SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchAsyncWithBlock:^{ for (NSString *file in replayFiles) { // Skip the last replay folder. if ([file isEqualToString:lastReplayFolder]) { continue; } - + NSString *filePath = [replayDir.path stringByAppendingPathComponent:file]; - + // Check if the file is a directory before deleting it. if ([fileManager isDirectory:filePath]) { [fileManager removeFileAtPath:filePath]; @@ -428,17 +429,17 @@ - (void)start { if (_rateLimited) { SENTRY_LOG_WARN( - @"This session was rate limited. Not starting session replay until next app session"); + @"This session was rate limited. Not starting session replay until next app session"); return; } - + if (self.sessionReplay != nil) { if (self.sessionReplay.isFullSession == NO) { [self.sessionReplay captureReplay]; } return; } - + _startedAsFullSession = YES; [self runReplayForAvailableWindow]; } @@ -479,7 +480,7 @@ - (void)configureReplayWith:(nullable id)breadc _currentBreadcrumbConverter = breadcrumbConverter; self.sessionReplay.breadcrumbConverter = breadcrumbConverter; } - + if (screenshotProvider) { _currentScreenshotProvider = screenshotProvider; self.sessionReplay.screenshotProvider = screenshotProvider; @@ -519,11 +520,11 @@ - (void)swizzleApplicationTouch # pragma clang diagnostic ignored "-Wshadow" SEL selector = NSSelectorFromString(@"sendEvent:"); SentrySwizzleInstanceMethod([UIApplication class], selector, SentrySWReturnType(void), - SentrySWArguments(UIEvent * event), SentrySWReplacement({ - [_touchTracker trackTouchFromEvent:event]; - SentrySWCallOriginal(event); - }), - SentrySwizzleModeOncePerClass, (void *)selector); + SentrySWArguments(UIEvent * event), SentrySWReplacement({ + [_touchTracker trackTouchFromEvent:event]; + SentrySWCallOriginal(event); + }), + SentrySwizzleModeOncePerClass, (void *)selector); # pragma clang diagnostic pop } @@ -570,7 +571,7 @@ - (SentryTouchTracker *)getTouchTracker - (BOOL)sessionReplayShouldCaptureReplayForError { return SentryDependencyContainer.sharedInstance.random.nextNumber - <= _replayOptions.onErrorSampleRate; + <= _replayOptions.onErrorSampleRate; } - (void)sessionReplayNewSegmentWithReplayEvent:(SentryReplayEvent *)replayEvent @@ -580,39 +581,60 @@ - (void)sessionReplayNewSegmentWithReplayEvent:(SentryReplayEvent *)replayEvent if ([_rateLimits isRateLimitActive:kSentryDataCategoryReplay] || [_rateLimits isRateLimitActive:kSentryDataCategoryAll]) { SENTRY_LOG_DEBUG( - @"Rate limiting is active for replays. Stopping session replay until next session."); + @"Rate limiting is active for replays. Stopping session replay until next session."); _rateLimited = YES; [self stop]; return; } - + [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:replayRecording video:videoUrl]; - + sentrySessionReplaySync_updateInfo( - (unsigned int)replayEvent.segmentId, replayEvent.timestamp.timeIntervalSinceReferenceDate); + (unsigned int)replayEvent.segmentId, replayEvent.timestamp.timeIntervalSinceReferenceDate); } - (void)sessionReplayStartedWithReplayId:(SentryId *)replayId { [SentrySDK.currentHub configureScope:^( - SentryScope *_Nonnull scope) { scope.replayId = [replayId sentryIdString]; }]; + SentryScope *_Nonnull scope) { scope.replayId = [replayId sentryIdString]; }]; } - (NSArray *)breadcrumbsForSessionReplay { __block NSArray *result; [SentrySDK.currentHub - configureScope:^(SentryScope *_Nonnull scope) { result = scope.breadcrumbs; }]; + configureScope:^(SentryScope *_Nonnull scope) { result = scope.breadcrumbs; }]; return result; } - (nullable NSString *)currentScreenNameForSessionReplay { return SentrySDK.currentHub.scope.currentScreen - ?: [SentryDependencyContainer.sharedInstance.application relevantViewControllersNames] - .firstObject; + ?: [SentryDependencyContainer.sharedInstance.application relevantViewControllersNames] + .firstObject; +} + +- (void)showMaskPreview:(CGFloat)opacity { + UIWindow* window = SentryDependencyContainer.sharedInstance.application.windows.firstObject; + if (window == nil) { + SENTRY_LOG_WARN(@"There is no UIWindow available to display preview"); + return; + } + + if (previewView == nil) { + previewView = [[SentryMaskingPreviewView alloc] initWithRedactOptions:_replayOptions]; + } + + previewView.opacity = opacity; + [previewView setFrame:window.bounds]; + [window addSubview:previewView]; +} + +- (void)hideMaskPreview { + [previewView removeFromSuperview]; + previewView = nil; } # pragma mark - SentryReachabilityObserver diff --git a/Sources/Sentry/include/HybridPublic/SentrySessionReplayIntegration.h b/Sources/Sentry/include/HybridPublic/SentrySessionReplayIntegration.h index 8ebff68b38..76bf132d8a 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySessionReplayIntegration.h +++ b/Sources/Sentry/include/HybridPublic/SentrySessionReplayIntegration.h @@ -43,6 +43,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)start; +- (void)showMaskPreview:(CGFloat)opacity; + @end #endif // SENTRY_TARGET_REPLAY_SUPPORTED NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/SessionReplay/Preview/SentryMaskingPreviewView.swift b/Sources/Swift/Integrations/SessionReplay/Preview/SentryMaskingPreviewView.swift new file mode 100644 index 0000000000..d4fc5d8a14 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/Preview/SentryMaskingPreviewView.swift @@ -0,0 +1,67 @@ +#if (os(iOS) || os(tvOS)) && !SENTRY_NO_UIKIT +import Foundation +import UIKit + +@objcMembers +class SentryMaskingPreviewView: UIView { + private class PreviewRederer: ViewRenderer { + func render(view: UIView) -> UIImage { + return UIGraphicsImageRenderer(size: view.frame.size, format: .init(for: .init(displayScale: 1))).image { _ in + // Creates a transparent image of the view size that will be used to drawn the redact regions. + // Transparent background is the default, so no additional drawing is required. + // Left blank on purpose + } + } + } + + private let photographer: SentryViewPhotographer + private var displayLink: CADisplayLink? + private var imageView = UIImageView() + + var opacity: Float { + get { return Float(imageView.alpha) } + set { imageView.alpha = CGFloat(newValue)} + } + + init(redactOptions: SentryRedactOptions) { + self.photographer = SentryViewPhotographer(renderer: PreviewRederer(), redactOptions: redactOptions) + super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + self.isUserInteractionEnabled = false + + imageView.sentryReplayUnmask() + imageView.frame = bounds + imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + addSubview(imageView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + displayLink?.invalidate() + } + + override func didMoveToSuperview() { + if displayLink == nil { + displayLink = CADisplayLink(target: self, selector: #selector(update)) + displayLink?.add(to: .main, forMode: .common) + } + + if let superview = self.superview { + self.frame = superview.bounds + } + } + + @objc + private func update() { + guard let superview = self.superview else { return } + self.photographer.image(view: superview) { image in + DispatchQueue.main.async { + self.imageView.image = image + } + } + } +} + +#endif // (os(iOS) || os(tvOS)) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift index 359c4eba19..eb7ddf67d1 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -6,7 +6,7 @@ import CoreGraphics import Foundation import UIKit -public protocol ViewRenderer { +protocol ViewRenderer { func render(view: UIView) -> UIImage } @@ -20,7 +20,7 @@ class DefaultViewRenderer: ViewRenderer { } @objcMembers -public class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { +class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { private let redactBuilder: UIRedactBuilder private let dispatchQueue = SentryDispatchQueueWrapper() diff --git a/Sources/Swift/Tools/UIImageHelper.swift b/Sources/Swift/Tools/UIImageHelper.swift index f53a95995a..bd55873bdf 100644 --- a/Sources/Swift/Tools/UIImageHelper.swift +++ b/Sources/Swift/Tools/UIImageHelper.swift @@ -25,9 +25,8 @@ final class UIImageHelper { let blue = CGFloat(data[0]) / 255.0 let green = CGFloat(data[1]) / 255.0 let red = CGFloat(data[2]) / 255.0 - let alpha = CGFloat(data[3]) / 255.0 - return UIColor(red: red, green: green, blue: blue, alpha: alpha) + return UIColor(red: red, green: green, blue: blue, alpha: 1) } } diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 3db19ce67b..229b510bd0 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -239,7 +239,7 @@ class UIRedactBuilder { } private func mapRedactRegion(fromLayer layer: CALayer, relativeTo parentLayer: CALayer?, redacting: inout [RedactRegion], rootFrame: CGRect, transform: CGAffineTransform, forceRedact: Bool = false) { - guard !redactClassesIdentifiers.isEmpty && !layer.isHidden && layer.opacity != 0, let view = layer.delegate as? UIView else { return } + guard !redactClassesIdentifiers.isEmpty && !layer.isHidden && layer.opacity != 0, let view = layer.delegate as? UIView, !(view is SentryMaskingPreviewView) else { return } let newTransform = concatenateTranform(transform, from: layer, withParent: parentLayer) From 1a8f571dad1cfab25c0806d59205d7721ae6fe2c Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 28 Jan 2025 16:03:51 +0000 Subject: [PATCH 11/18] Format code --- Sources/Sentry/SentryReplayApi.m | 8 +- .../Sentry/SentrySessionReplayIntegration.m | 234 +++++++++--------- 2 files changed, 123 insertions(+), 119 deletions(-) diff --git a/Sources/Sentry/SentryReplayApi.m b/Sources/Sentry/SentryReplayApi.m index 0fc387d7cc..95316951ae 100644 --- a/Sources/Sentry/SentryReplayApi.m +++ b/Sources/Sentry/SentryReplayApi.m @@ -73,15 +73,17 @@ - (void)stop [replayIntegration stop]; } -- (void)showMaskPreview { +- (void)showMaskPreview +{ [self showMaskPreview:1]; } -- (void)showMaskPreview:(CGFloat)opacity { +- (void)showMaskPreview:(CGFloat)opacity +{ SentrySessionReplayIntegration *replayIntegration = (SentrySessionReplayIntegration *)[SentrySDK.currentHub getInstalledIntegration:SentrySessionReplayIntegration.class]; - + [replayIntegration showMaskPreview:opacity]; } diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m index bc7899cec1..05d787a6cb 100644 --- a/Sources/Sentry/SentrySessionReplayIntegration.m +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -50,7 +50,7 @@ @implementation SentrySessionReplayIntegration { id _rateLimits; id _currentScreenshotProvider; id _currentBreadcrumbConverter; - SentryMaskingPreviewView * previewView; + SentryMaskingPreviewView *previewView; // We need to use this variable to identify whether rate limiting was ever activated for session // replay in this session, instead of always looking for the rate status in `SentryRateLimits` // This is the easiest way to ensure segment 0 will always reach the server, because session @@ -78,7 +78,7 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options if ([super installWithOptions:options] == NO) { return NO; } - + [self setupWith:options.sessionReplay enableTouchTracker:options.enableSwizzling]; return YES; } @@ -88,30 +88,30 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL) _replayOptions = replayOptions; _viewPhotographer = [[SentryViewPhotographer alloc] initWithRedactOptions:replayOptions]; _rateLimits = SentryDependencyContainer.sharedInstance.rateLimits; - + if (touchTracker) { _touchTracker = [[SentryTouchTracker alloc] - initWithDateProvider:SentryDependencyContainer.sharedInstance.dateProvider - scale:replayOptions.sizeScale]; + initWithDateProvider:SentryDependencyContainer.sharedInstance.dateProvider + scale:replayOptions.sizeScale]; [self swizzleApplicationTouch]; } - + _notificationCenter = SentryDependencyContainer.sharedInstance.notificationCenterWrapper; - + [self moveCurrentReplay]; [self cleanUp]; - + [SentrySDK.currentHub registerSessionListener:self]; [SentryGlobalEventProcessor.shared - addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { - if (event.isCrashEvent) { - [self resumePreviousSessionReplay:event]; - } else { - [self.sessionReplay captureReplayForEvent:event]; - } - return event; - }]; - + addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { + if (event.isCrashEvent) { + [self resumePreviousSessionReplay:event]; + } else { + [self.sessionReplay captureReplayForEvent:event]; + } + return event; + }]; + [SentryDependencyContainer.sharedInstance.reachability addObserver:self]; } @@ -120,11 +120,11 @@ - (void)setupWith:(SentryReplayOptions *)replayOptions enableTouchTracker:(BOOL) NSURL *dir = [self replayDirectory]; NSURL *lastReplayUrl = [dir URLByAppendingPathComponent:SENTRY_LAST_REPLAY]; NSData *lastReplay = [NSData dataWithContentsOfURL:lastReplayUrl]; - + if (lastReplay == nil) { return nil; } - + return [SentrySerialization deserializeDictionaryFromJsonData:lastReplay]; } @@ -138,54 +138,54 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event { NSURL *dir = [self replayDirectory]; NSDictionary *jsonObject = [self lastReplayInfo]; - + if (jsonObject == nil) { return; } - + SentryId *replayId = jsonObject[@"replayId"] - ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] - : [[SentryId alloc] init]; + ? [[SentryId alloc] initWithUUIDString:jsonObject[@"replayId"]] + : [[SentryId alloc] init]; NSURL *lastReplayURL = [dir URLByAppendingPathComponent:jsonObject[@"path"]]; - + SentryCrashReplay crashInfo = { 0 }; bool hasCrashInfo = sentrySessionReplaySync_readInfo(&crashInfo, - [[lastReplayURL URLByAppendingPathComponent:@"crashInfo"].path - cStringUsingEncoding:NSUTF8StringEncoding]); - + [[lastReplayURL URLByAppendingPathComponent:@"crashInfo"].path + cStringUsingEncoding:NSUTF8StringEncoding]); + SentryReplayType type = hasCrashInfo ? SentryReplayTypeSession : SentryReplayTypeBuffer; NSTimeInterval duration - = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration; + = hasCrashInfo ? _replayOptions.sessionSegmentDuration : _replayOptions.errorReplayDuration; int segmentId = hasCrashInfo ? crashInfo.segmentId + 1 : 0; - + if (type == SentryReplayTypeBuffer) { float errorSampleRate = [jsonObject[@"errorSampleRate"] floatValue]; if ([SentryDependencyContainer.sharedInstance.random nextNumber] >= errorSampleRate) { return; } } - + SentryOnDemandReplay *resumeReplayMaker = - [[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path]; + [[SentryOnDemandReplay alloc] initWithContentFrom:lastReplayURL.path]; resumeReplayMaker.bitRate = _replayOptions.replayBitRate; resumeReplayMaker.videoScale = _replayOptions.sizeScale; - + NSDate *beginning = hasCrashInfo - ? [NSDate dateWithTimeIntervalSinceReferenceDate:crashInfo.lastSegmentEnd] - : [resumeReplayMaker oldestFrameDate]; - + ? [NSDate dateWithTimeIntervalSinceReferenceDate:crashInfo.lastSegmentEnd] + : [resumeReplayMaker oldestFrameDate]; + if (beginning == nil) { return; // no frames to send } - + SentryReplayType _type = type; int _segmentId = segmentId; - + NSError *error; NSArray *videos = - [resumeReplayMaker createVideoWithBeginning:beginning - end:[beginning dateByAddingTimeInterval:duration] - error:&error]; + [resumeReplayMaker createVideoWithBeginning:beginning + end:[beginning dateByAddingTimeInterval:duration] + error:&error]; if (videos == nil) { SENTRY_LOG_ERROR(@"Could not create replay video: %@", error); return; @@ -195,12 +195,12 @@ - (void)resumePreviousSessionReplay:(SentryEvent *)event // type buffer is only for the first segment _type = SentryReplayTypeSession; } - + NSMutableDictionary *eventContext = event.context.mutableCopy; eventContext[@"replay"] = - [NSDictionary dictionaryWithObjectsAndKeys:replayId.sentryIdString, @"replay_id", nil]; + [NSDictionary dictionaryWithObjectsAndKeys:replayId.sentryIdString, @"replay_id", nil]; event.context = eventContext; - + if ([NSFileManager.defaultManager removeItemAtURL:lastReplayURL error:&error] == NO) { SENTRY_LOG_ERROR(@"Can`t delete '%@': %@", SENTRY_LAST_REPLAY, error); } @@ -219,28 +219,28 @@ - (void)captureVideo:(SentryVideoInfo *)video SentryReplayRecording *recording = [[SentryReplayRecording alloc] initWithSegmentId:segment video:video extraEvents:@[]]; - + [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:recording video:video.path]; - + NSError *error = nil; if (![[NSFileManager defaultManager] removeItemAtURL:video.path error:&error]) { SENTRY_LOG_DEBUG( - @"Could not delete replay segment from disk: %@", error.localizedDescription); + @"Could not delete replay segment from disk: %@", error.localizedDescription); } } - (void)startSession { [self.sessionReplay pause]; - + _startedAsFullSession = [self shouldReplayFullSession:_replayOptions.sessionSampleRate]; - + if (!_startedAsFullSession && _replayOptions.onErrorSampleRate == 0) { return; } - + [self runReplayForAvailableWindow]; } @@ -261,8 +261,8 @@ - (void)newSceneActivate { if (@available(iOS 13.0, tvOS 13.0, *)) { [SentryDependencyContainer.sharedInstance.notificationCenterWrapper - removeObserver:self - name:UISceneDidActivateNotification]; + removeObserver:self + name:UISceneDidActivateNotification]; [self startWithOptions:_replayOptions fullSession:_startedAsFullSession]; } } @@ -271,10 +271,10 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions fullSession:(BOOL)shouldReplayFullSession { [self startWithOptions:replayOptions - screenshotProvider:_currentScreenshotProvider ?: _viewPhotographer - breadcrumbConverter:_currentBreadcrumbConverter - ?: [[SentrySRDefaultBreadcrumbConverter alloc] init] - fullSession:shouldReplayFullSession]; + screenshotProvider:_currentScreenshotProvider ?: _viewPhotographer + breadcrumbConverter:_currentBreadcrumbConverter + ?: [[SentrySRDefaultBreadcrumbConverter alloc] init] + fullSession:shouldReplayFullSession]; } - (void)startWithOptions:(SentryReplayOptions *)replayOptions @@ -285,53 +285,53 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions NSURL *docs = [self replayDirectory]; NSString *currentSession = [NSUUID UUID].UUIDString; docs = [docs URLByAppendingPathComponent:currentSession]; - + if (![NSFileManager.defaultManager fileExistsAtPath:docs.path]) { [NSFileManager.defaultManager createDirectoryAtURL:docs withIntermediateDirectories:YES attributes:nil error:nil]; } - + SentryOnDemandReplay *replayMaker = [[SentryOnDemandReplay alloc] initWithOutputPath:docs.path]; replayMaker.bitRate = replayOptions.replayBitRate; replayMaker.videoScale = replayOptions.sizeScale; replayMaker.cacheMaxSize - = (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + 1 - : replayOptions.errorReplayDuration + 1); - + = (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + 1 + : replayOptions.errorReplayDuration + 1); + dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class( - DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_LOW, 0); + DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_PRIORITY_LOW, 0); SentryDispatchQueueWrapper *dispatchQueue = - [[SentryDispatchQueueWrapper alloc] initWithName:"io.sentry.session-replay" - attributes:attributes]; - + [[SentryDispatchQueueWrapper alloc] initWithName:"io.sentry.session-replay" + attributes:attributes]; + self.sessionReplay = [[SentrySessionReplay alloc] - initWithReplayOptions:replayOptions - replayFolderPath:docs - screenshotProvider:screenshotProvider - replayMaker:replayMaker - breadcrumbConverter:breadcrumbConverter - touchTracker:_touchTracker - dateProvider:SentryDependencyContainer.sharedInstance.dateProvider - delegate:self - dispatchQueue:dispatchQueue - displayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]]; - + initWithReplayOptions:replayOptions + replayFolderPath:docs + screenshotProvider:screenshotProvider + replayMaker:replayMaker + breadcrumbConverter:breadcrumbConverter + touchTracker:_touchTracker + dateProvider:SentryDependencyContainer.sharedInstance.dateProvider + delegate:self + dispatchQueue:dispatchQueue + displayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]]; + [self.sessionReplay - startWithRootView:SentryDependencyContainer.sharedInstance.application.windows.firstObject - fullSession:shouldReplayFullSession]; - + startWithRootView:SentryDependencyContainer.sharedInstance.application.windows.firstObject + fullSession:shouldReplayFullSession]; + [_notificationCenter addObserver:self selector:@selector(pause) name:UIApplicationDidEnterBackgroundNotification object:nil]; - + [_notificationCenter addObserver:self selector:@selector(resume) name:UIApplicationDidBecomeActiveNotification object:nil]; - + [self saveCurrentSessionInfo:self.sessionReplay.sessionReplayId path:docs.path options:replayOptions]; @@ -340,7 +340,7 @@ - (void)startWithOptions:(SentryReplayOptions *)replayOptions - (NSURL *)replayDirectory { NSURL *dir = - [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; + [NSURL fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; return [dir URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; } @@ -349,20 +349,20 @@ - (void)saveCurrentSessionInfo:(SentryId *)sessionId options:(SentryReplayOptions *)options { NSDictionary *info = - [[NSDictionary alloc] initWithObjectsAndKeys:sessionId.sentryIdString, @"replayId", - path.lastPathComponent, @"path", @(options.onErrorSampleRate), @"errorSampleRate", nil]; - + [[NSDictionary alloc] initWithObjectsAndKeys:sessionId.sentryIdString, @"replayId", + path.lastPathComponent, @"path", @(options.onErrorSampleRate), @"errorSampleRate", nil]; + NSData *data = [SentrySerialization dataWithJSONObject:info]; - + NSString *infoPath = [[path stringByDeletingLastPathComponent] - stringByAppendingPathComponent:SENTRY_CURRENT_REPLAY]; + stringByAppendingPathComponent:SENTRY_CURRENT_REPLAY]; if ([NSFileManager.defaultManager fileExistsAtPath:infoPath]) { [NSFileManager.defaultManager removeItemAtPath:infoPath error:nil]; } [data writeToFile:infoPath atomically:YES]; - + sentrySessionReplaySync_start([[path stringByAppendingPathComponent:@"crashInfo"] - cStringUsingEncoding:NSUTF8StringEncoding]); + cStringUsingEncoding:NSUTF8StringEncoding]); } - (void)moveCurrentReplay @@ -370,7 +370,7 @@ - (void)moveCurrentReplay NSURL *path = [self replayDirectory]; NSURL *current = [path URLByAppendingPathComponent:SENTRY_CURRENT_REPLAY]; NSURL *last = [path URLByAppendingPathComponent:SENTRY_LAST_REPLAY]; - + NSError *error; if ([NSFileManager.defaultManager fileExistsAtPath:last.path]) { if ([NSFileManager.defaultManager removeItemAtURL:last error:&error] == NO) { @@ -378,7 +378,7 @@ - (void)moveCurrentReplay return; } } - + if ([NSFileManager.defaultManager moveItemAtURL:current toURL:last error:nil] == NO) { SENTRY_LOG_ERROR(@"Could not move 'currentreplay' to 'lastreplay': %@", error); } @@ -389,7 +389,7 @@ - (void)cleanUp NSURL *replayDir = [self replayDirectory]; NSDictionary *lastReplayInfo = [self lastReplayInfo]; NSString *lastReplayFolder = lastReplayInfo[@"path"]; - + SentryFileManager *fileManager = SentryDependencyContainer.sharedInstance.fileManager; // Mapping replay folder here and not in dispatched queue to prevent a race condition between // listing files and creating a new replay session. @@ -397,16 +397,16 @@ - (void)cleanUp if (replayFiles.count == 0) { return; } - + [SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchAsyncWithBlock:^{ for (NSString *file in replayFiles) { // Skip the last replay folder. if ([file isEqualToString:lastReplayFolder]) { continue; } - + NSString *filePath = [replayDir.path stringByAppendingPathComponent:file]; - + // Check if the file is a directory before deleting it. if ([fileManager isDirectory:filePath]) { [fileManager removeFileAtPath:filePath]; @@ -429,17 +429,17 @@ - (void)start { if (_rateLimited) { SENTRY_LOG_WARN( - @"This session was rate limited. Not starting session replay until next app session"); + @"This session was rate limited. Not starting session replay until next app session"); return; } - + if (self.sessionReplay != nil) { if (self.sessionReplay.isFullSession == NO) { [self.sessionReplay captureReplay]; } return; } - + _startedAsFullSession = YES; [self runReplayForAvailableWindow]; } @@ -480,7 +480,7 @@ - (void)configureReplayWith:(nullable id)breadc _currentBreadcrumbConverter = breadcrumbConverter; self.sessionReplay.breadcrumbConverter = breadcrumbConverter; } - + if (screenshotProvider) { _currentScreenshotProvider = screenshotProvider; self.sessionReplay.screenshotProvider = screenshotProvider; @@ -520,11 +520,11 @@ - (void)swizzleApplicationTouch # pragma clang diagnostic ignored "-Wshadow" SEL selector = NSSelectorFromString(@"sendEvent:"); SentrySwizzleInstanceMethod([UIApplication class], selector, SentrySWReturnType(void), - SentrySWArguments(UIEvent * event), SentrySWReplacement({ - [_touchTracker trackTouchFromEvent:event]; - SentrySWCallOriginal(event); - }), - SentrySwizzleModeOncePerClass, (void *)selector); + SentrySWArguments(UIEvent * event), SentrySWReplacement({ + [_touchTracker trackTouchFromEvent:event]; + SentrySWCallOriginal(event); + }), + SentrySwizzleModeOncePerClass, (void *)selector); # pragma clang diagnostic pop } @@ -571,7 +571,7 @@ - (SentryTouchTracker *)getTouchTracker - (BOOL)sessionReplayShouldCaptureReplayForError { return SentryDependencyContainer.sharedInstance.random.nextNumber - <= _replayOptions.onErrorSampleRate; + <= _replayOptions.onErrorSampleRate; } - (void)sessionReplayNewSegmentWithReplayEvent:(SentryReplayEvent *)replayEvent @@ -581,48 +581,49 @@ - (void)sessionReplayNewSegmentWithReplayEvent:(SentryReplayEvent *)replayEvent if ([_rateLimits isRateLimitActive:kSentryDataCategoryReplay] || [_rateLimits isRateLimitActive:kSentryDataCategoryAll]) { SENTRY_LOG_DEBUG( - @"Rate limiting is active for replays. Stopping session replay until next session."); + @"Rate limiting is active for replays. Stopping session replay until next session."); _rateLimited = YES; [self stop]; return; } - + [SentrySDK.currentHub captureReplayEvent:replayEvent replayRecording:replayRecording video:videoUrl]; - + sentrySessionReplaySync_updateInfo( - (unsigned int)replayEvent.segmentId, replayEvent.timestamp.timeIntervalSinceReferenceDate); + (unsigned int)replayEvent.segmentId, replayEvent.timestamp.timeIntervalSinceReferenceDate); } - (void)sessionReplayStartedWithReplayId:(SentryId *)replayId { [SentrySDK.currentHub configureScope:^( - SentryScope *_Nonnull scope) { scope.replayId = [replayId sentryIdString]; }]; + SentryScope *_Nonnull scope) { scope.replayId = [replayId sentryIdString]; }]; } - (NSArray *)breadcrumbsForSessionReplay { __block NSArray *result; [SentrySDK.currentHub - configureScope:^(SentryScope *_Nonnull scope) { result = scope.breadcrumbs; }]; + configureScope:^(SentryScope *_Nonnull scope) { result = scope.breadcrumbs; }]; return result; } - (nullable NSString *)currentScreenNameForSessionReplay { return SentrySDK.currentHub.scope.currentScreen - ?: [SentryDependencyContainer.sharedInstance.application relevantViewControllersNames] - .firstObject; + ?: [SentryDependencyContainer.sharedInstance.application relevantViewControllersNames] + .firstObject; } -- (void)showMaskPreview:(CGFloat)opacity { - UIWindow* window = SentryDependencyContainer.sharedInstance.application.windows.firstObject; +- (void)showMaskPreview:(CGFloat)opacity +{ + UIWindow *window = SentryDependencyContainer.sharedInstance.application.windows.firstObject; if (window == nil) { SENTRY_LOG_WARN(@"There is no UIWindow available to display preview"); return; } - + if (previewView == nil) { previewView = [[SentryMaskingPreviewView alloc] initWithRedactOptions:_replayOptions]; } @@ -632,7 +633,8 @@ - (void)showMaskPreview:(CGFloat)opacity { [window addSubview:previewView]; } -- (void)hideMaskPreview { +- (void)hideMaskPreview +{ [previewView removeFromSuperview]; previewView = nil; } From 772793ae8f9f6b04b59898dbef606fa190bf38e9 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 28 Jan 2025 17:04:17 +0100 Subject: [PATCH 12/18] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 630858ab7a..105c068131 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ### Features -- Add `showMaskPreview` to `SentrySDK.replay` api to debug replay masking () +- Add `showMaskPreview` to `SentrySDK.replay` api to debug replay masking (#4761) ## 8.44.0-beta.1 From d174cc25eea540f770c45ef2ff10e5a9e604f7e1 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 28 Jan 2025 17:06:11 +0100 Subject: [PATCH 13/18] ref --- .../iOS-SwiftUI/iOS-SwiftUI/ContentView.swift | 45 +++++----- Sentry.xcodeproj/project.pbxproj | 20 ----- .../Preview/PreviewRedactOptions.swift | 18 ---- .../Preview/SentryReplayMaskPreview.swift | 41 --------- .../SentryReplayMaskPreviewUIView.swift | 84 ------------------- .../SentryInternal/SentryInternal.h | 2 - 6 files changed, 19 insertions(+), 191 deletions(-) delete mode 100644 Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift delete mode 100644 Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift delete mode 100644 Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift index fb4a5c61ea..490aea28c0 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift @@ -4,17 +4,17 @@ import SwiftUI //This is for test purpose class DataBag { - + static let shared = DataBag() - + var info = [String: Any]() - + private init() { } } struct ContentView: View { - + @State var TTDInfo: String = "" var addBreadcrumbAction: () -> Void = { @@ -90,7 +90,7 @@ struct ContentView: View { } } } - + func showTTD() { guard let tracer = getCurrentTracer() else { return } @@ -119,25 +119,25 @@ struct ContentView: View { func hasTTFD(tracer: SentryTracer?) -> Bool { tracer?.children.contains { $0.spanDescription?.contains("full display") == true } == true } - + func getCurrentSpan() -> Span? { - + let tracker = SentryPerformanceTracker.shared guard let currentSpanId = tracker.activeSpanId() else { return DataBag.shared.info["lastSpan"] as? Span } - + if DataBag.shared.info["lastSpan"] == nil { let span = tracker.getSpan(currentSpanId) - + if !(span is SentryTracer) { DataBag.shared.info["lastSpan"] = span } } - + return DataBag.shared.info["lastSpan"] as? Span } - + var body: some View { return SentryTracedView("Content View Body", waitForFullDisplay: true) { NavigationView { @@ -145,7 +145,7 @@ struct ContentView: View { Group { Text(getCurrentTracer()?.transactionContext.name ?? "NO SPAN") .accessibilityIdentifier("TRANSACTION_NAME") - + Text(getCurrentTracer()?.transactionContext.spanId.sentrySpanIdString ?? "NO ID") .accessibilityIdentifier("TRANSACTION_ID") .sentryReplayMask() @@ -169,27 +169,27 @@ struct ContentView: View { } HStack (spacing: 30) { VStack(spacing: 16) { - + Button(action: addBreadcrumbAction) { Text("Add Breadcrumb") } - + Button(action: captureMessageAction) { Text("Capture Message") } - + Button(action: captureUserFeedbackAction) { Text("Capture User Feedback") } - + Button(action: captureErrorAction) { Text("Capture Error") } - + Button(action: captureNSExceptionAction) { Text("Capture NSException") } - + Button(action: captureTransactionAction) { Text("Capture Transaction") } @@ -235,19 +235,13 @@ struct ContentView: View { .background(Color.white) } SecondView() - + Text(TTDInfo) .accessibilityIdentifier("TTDInfo") - - TextField("DAE", text: $dae) - .background(Color.red) - TextField("Ola", text: $dae).sentryReplayUnmask() } } } } - - @State var dae: String = "" } struct SecondView: View { @@ -261,6 +255,5 @@ struct SecondView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() - .sentryReplayPreviewMask(opacity: 0.3) } } diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index c89cd698fe..d91b4c5440 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -874,9 +874,6 @@ D867063F27C3BC2400048851 /* SentryCoreDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063C27C3BC2400048851 /* SentryCoreDataTracker.h */; }; D86B6835294348A400B8B1FC /* SentryAttachment+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */; }; D86F419827C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */; }; - D8709AC42D3E9C63006C491E /* SentryReplayMaskPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */; }; - D8709ACB2D3F848E006C491E /* SentryReplayMaskPreviewUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */; }; - D8709ACD2D3F84CF006C491E /* PreviewRedactOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */; }; D8739CF32BECF70F007D2F66 /* SentryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8739CF22BECF70F007D2F66 /* SentryLevel.swift */; }; D8739CF92BECFFB5007D2F66 /* SentryTransactionNameSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */; }; D8739D142BEE5049007D2F66 /* SentryRRWebSpanEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8739D132BEE5049007D2F66 /* SentryRRWebSpanEvent.swift */; }; @@ -1988,9 +1985,6 @@ D86B6820293F39E000B8B1FC /* TestSentryViewHierarchy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestSentryViewHierarchy.h; sourceTree = ""; }; D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryAttachment+Private.h"; path = "include/SentryAttachment+Private.h"; sourceTree = ""; }; D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackerExtension.swift; sourceTree = ""; }; - D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayMaskPreview.swift; sourceTree = ""; }; - D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayMaskPreviewUIView.swift; sourceTree = ""; }; - D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewRedactOptions.swift; sourceTree = ""; }; D8739CF22BECF70F007D2F66 /* SentryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLevel.swift; sourceTree = ""; }; D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTransactionNameSource.swift; sourceTree = ""; }; D8739D132BEE5049007D2F66 /* SentryRRWebSpanEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRRWebSpanEvent.swift; sourceTree = ""; }; @@ -3785,7 +3779,6 @@ isa = PBXGroup; children = ( D8199DB429376ECC0074249E /* SentryInternal */, - D8709AC92D3F83A6006C491E /* Preview */, D8199DB529376ECC0074249E /* SentrySwiftUI.h */, D88D25E92B8E0BAC0073C3D5 /* module.modulemap */, D8199DB629376ECC0074249E /* SentryTracedView.swift */, @@ -3883,16 +3876,6 @@ name = CoreData; sourceTree = ""; }; - D8709AC92D3F83A6006C491E /* Preview */ = { - isa = PBXGroup; - children = ( - D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */, - D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */, - D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */, - ); - path = Preview; - sourceTree = ""; - }; D8739CF62BECFF86007D2F66 /* Log */ = { isa = PBXGroup; children = ( @@ -5340,10 +5323,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D8709AC42D3E9C63006C491E /* SentryReplayMaskPreview.swift in Sources */, D8199DC129376EEC0074249E /* SentryTracedView.swift in Sources */, - D8709ACD2D3F84CF006C491E /* PreviewRedactOptions.swift in Sources */, - D8709ACB2D3F848E006C491E /* SentryReplayMaskPreviewUIView.swift in Sources */, D48E8B9D2D3E82AC0032E35E /* SentrySpanOperation.swift in Sources */, D8199DBF29376EE20074249E /* SentryInternal.m in Sources */, D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */, diff --git a/Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift b/Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift deleted file mode 100644 index 02fc6c3bbb..0000000000 --- a/Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift +++ /dev/null @@ -1,18 +0,0 @@ -#if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS) -import Sentry - -public class PreviewRedactOptions: SentryRedactOptions { - public let maskAllText: Bool - public let maskAllImages: Bool - public let maskedViewClasses: [AnyClass] - public let unmaskedViewClasses: [AnyClass] - - public init(maskAllText: Bool = true, maskAllImages: Bool = true, maskedViewClasses: [AnyClass] = [], unmaskedViewClasses: [AnyClass] = []) { - self.maskAllText = maskAllText - self.maskAllImages = maskAllImages - self.maskedViewClasses = maskedViewClasses - self.unmaskedViewClasses = unmaskedViewClasses - } -} - -#endif diff --git a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift deleted file mode 100644 index 8e946a5071..0000000000 --- a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift +++ /dev/null @@ -1,41 +0,0 @@ -#if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS) -import Sentry -import SwiftUI -import UIKit - -#if CARTHAGE || SWIFT_PACKAGE -@_implementationOnly import SentryInternal -#endif - -@available(iOS 13, macOS 10.15, tvOS 13, *) -struct SentryReplayMaskPreview: ViewModifier { - let redactOptions: SentryRedactOptions - let opacity: Float - func body(content: Content) -> some View { - content.overlay(SentryReplayPreviewView(redactOptions: redactOptions, opacity: opacity).disabled(true)) - } -} - -@available(iOS 13, macOS 10.15, tvOS 13, *) -public extension View { - func sentryReplayPreviewMask(redactOptions: SentryRedactOptions? = nil, opacity: Float = 1) -> some View { - let options = redactOptions ?? SentrySDK.options?.sessionReplay ?? PreviewRedactOptions() - return modifier(SentryReplayMaskPreview(redactOptions: options, opacity: opacity)) - } -} - -@available(iOS 13, macOS 10.15, tvOS 13, *) -struct SentryReplayPreviewView: UIViewRepresentable { - let redactOptions: SentryRedactOptions - let opacity: Float - - func makeUIView(context: Context) -> SentryReplayMaskPreviewUIView { - return SentryReplayMaskPreviewUIView(redactOptions: redactOptions) - } - - func updateUIView(_ uiView: SentryReplayMaskPreviewUIView, context: Context) { - uiView.opacity = opacity - } -} - -#endif diff --git a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift b/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift deleted file mode 100644 index 5de867f19e..0000000000 --- a/Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift +++ /dev/null @@ -1,84 +0,0 @@ -#if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS) -import Sentry -import UIKit - -#if CARTHAGE || SWIFT_PACKAGE -@_implementationOnly import SentryInternal -#endif - -class PreviewImageView: UIImageView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return nil - } -} - -class SentryReplayMaskPreviewUIView: UIView { - private let photographer: SentryViewPhotographer - private var displayLink: CADisplayLink? - private var imageView = PreviewImageView() - - var opacity: Float { - get { return Float(imageView.alpha) } - set { imageView.alpha = CGFloat(newValue)} - } - - init(redactOptions: SentryRedactOptions) { - self.photographer = SentryViewPhotographer(renderer: PreviewRederer(), redactOptions: redactOptions) - super.init(frame: .zero) - self.isUserInteractionEnabled = false - imageView.isUserInteractionEnabled = false - imageView.sentryReplayUnmask() - } - - deinit { - displayLink?.invalidate() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func didMoveToSuperview() { - if ProcessInfo.processInfo.environment[SENTRY_XCODE_PREVIEW_ENVIRONMENT_KEY] == "1" { - displayLink = CADisplayLink(target: self, selector: #selector(update)) - displayLink?.add(to: .main, forMode: .common) - } else { - print("[SENTRY] [WARNING] SentryReplayMaskPreview is not meant to be used in your app, only with SwiftUI Previews.") - } - } - - @objc - private func update() { - guard let window = self.window else { return } - self.photographer.image(view: window) { image in - DispatchQueue.main.async { - self.showImage(image: image) - } - } - } - - private func showImage(image: UIImage) { - guard let window = super.window else { return } - if imageView.superview != window { - window.addSubview(imageView) - } - imageView.image = image - imageView.frame = window.bounds - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return nil - } -} - -class PreviewRederer: ViewRenderer { - func render(view: UIView) -> UIImage { - return UIGraphicsImageRenderer(size: view.frame.size, format: .init(for: .init(displayScale: 1))).image { _ in - // Creates a transparent image of the view size that will be used to drawn the redact regions. - // Transparent background is the default, so no additional drawing is required. - // Left blank on purpose - } - } -} - -#endif diff --git a/Sources/SentrySwiftUI/SentryInternal/SentryInternal.h b/Sources/SentrySwiftUI/SentryInternal/SentryInternal.h index 438d64527d..d327121f0d 100644 --- a/Sources/SentrySwiftUI/SentryInternal/SentryInternal.h +++ b/Sources/SentrySwiftUI/SentryInternal/SentryInternal.h @@ -24,8 +24,6 @@ NS_ASSUME_NONNULL_BEGIN -extern NSString *const SENTRY_XCODE_PREVIEW_ENVIRONMENT_KEY; - typedef NS_ENUM(NSInteger, SentryTransactionNameSource); @class SentrySpanId; From fca125770c44ac9ee27272c06b82691cbb5978a7 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 28 Jan 2025 17:06:46 +0100 Subject: [PATCH 14/18] Update ContentView.swift --- .../iOS-SwiftUI/iOS-SwiftUI/ContentView.swift | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift index 490aea28c0..77563b15f9 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift @@ -4,17 +4,17 @@ import SwiftUI //This is for test purpose class DataBag { - + static let shared = DataBag() - + var info = [String: Any]() - + private init() { } } struct ContentView: View { - + @State var TTDInfo: String = "" var addBreadcrumbAction: () -> Void = { @@ -90,7 +90,7 @@ struct ContentView: View { } } } - + func showTTD() { guard let tracer = getCurrentTracer() else { return } @@ -119,22 +119,22 @@ struct ContentView: View { func hasTTFD(tracer: SentryTracer?) -> Bool { tracer?.children.contains { $0.spanDescription?.contains("full display") == true } == true } - + func getCurrentSpan() -> Span? { - + let tracker = SentryPerformanceTracker.shared guard let currentSpanId = tracker.activeSpanId() else { return DataBag.shared.info["lastSpan"] as? Span } - + if DataBag.shared.info["lastSpan"] == nil { let span = tracker.getSpan(currentSpanId) - + if !(span is SentryTracer) { DataBag.shared.info["lastSpan"] = span } } - + return DataBag.shared.info["lastSpan"] as? Span } @@ -145,7 +145,7 @@ struct ContentView: View { Group { Text(getCurrentTracer()?.transactionContext.name ?? "NO SPAN") .accessibilityIdentifier("TRANSACTION_NAME") - + Text(getCurrentTracer()?.transactionContext.spanId.sentrySpanIdString ?? "NO ID") .accessibilityIdentifier("TRANSACTION_ID") .sentryReplayMask() @@ -169,27 +169,27 @@ struct ContentView: View { } HStack (spacing: 30) { VStack(spacing: 16) { - + Button(action: addBreadcrumbAction) { Text("Add Breadcrumb") } - + Button(action: captureMessageAction) { Text("Capture Message") } - + Button(action: captureUserFeedbackAction) { Text("Capture User Feedback") } - + Button(action: captureErrorAction) { Text("Capture Error") } - + Button(action: captureNSExceptionAction) { Text("Capture NSException") } - + Button(action: captureTransactionAction) { Text("Capture Transaction") } From 0ed79cf9154dae66c422d23a38ce87189ad1e196 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 28 Jan 2025 17:09:41 +0100 Subject: [PATCH 15/18] revert --- Sources/Swift/Protocol/SentryRedactOptions.swift | 8 ++++---- Sources/Swift/Tools/SentryViewPhotographer.swift | 6 +++--- Sources/Swift/Tools/SentryViewScreenshotProvider.swift | 2 +- Sources/Swift/Tools/UIRedactBuilder.swift | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/Swift/Protocol/SentryRedactOptions.swift b/Sources/Swift/Protocol/SentryRedactOptions.swift index e352d45e98..01ba4dd32a 100644 --- a/Sources/Swift/Protocol/SentryRedactOptions.swift +++ b/Sources/Swift/Protocol/SentryRedactOptions.swift @@ -10,8 +10,8 @@ public protocol SentryRedactOptions { @objcMembers final class SentryRedactDefaultOptions: NSObject, SentryRedactOptions { - public var maskAllText: Bool = true - public var maskAllImages: Bool = true - public var maskedViewClasses: [AnyClass] = [] - public var unmaskedViewClasses: [AnyClass] = [] + 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 eb7ddf67d1..a7442faf63 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -26,18 +26,18 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { var renderer: ViewRenderer - public init(renderer: ViewRenderer, redactOptions: SentryRedactOptions) { + init(renderer: ViewRenderer, redactOptions: SentryRedactOptions) { self.renderer = renderer redactBuilder = UIRedactBuilder(options: redactOptions) super.init() } - public init(redactOptions: SentryRedactOptions) { + init(redactOptions: SentryRedactOptions) { self.renderer = DefaultViewRenderer() self.redactBuilder = UIRedactBuilder(options: redactOptions) } - public 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 diff --git a/Sources/Swift/Tools/SentryViewScreenshotProvider.swift b/Sources/Swift/Tools/SentryViewScreenshotProvider.swift index d399e50335..0b99bc1bff 100644 --- a/Sources/Swift/Tools/SentryViewScreenshotProvider.swift +++ b/Sources/Swift/Tools/SentryViewScreenshotProvider.swift @@ -3,7 +3,7 @@ import Foundation import UIKit -public typealias ScreenshotCallback = (UIImage) -> Void +typealias ScreenshotCallback = (UIImage) -> Void @objc protocol SentryViewScreenshotProvider: NSObjectProtocol { diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 229b510bd0..3db19ce67b 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -239,7 +239,7 @@ class UIRedactBuilder { } private func mapRedactRegion(fromLayer layer: CALayer, relativeTo parentLayer: CALayer?, redacting: inout [RedactRegion], rootFrame: CGRect, transform: CGAffineTransform, forceRedact: Bool = false) { - guard !redactClassesIdentifiers.isEmpty && !layer.isHidden && layer.opacity != 0, let view = layer.delegate as? UIView, !(view is SentryMaskingPreviewView) else { return } + guard !redactClassesIdentifiers.isEmpty && !layer.isHidden && layer.opacity != 0, let view = layer.delegate as? UIView else { return } let newTransform = concatenateTranform(transform, from: layer, withParent: parentLayer) From 3d87e52830ce0615b9865c4910b42ba105062127 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 29 Jan 2025 09:22:04 +0100 Subject: [PATCH 16/18] revert --- Sources/Sentry/SentrySDK.m | 4 +--- Tests/SentryTests/SentrySDKTests.swift | 18 ++++-------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index 284bcd0c5e..7454324759 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -203,9 +203,6 @@ + (void)setStartTimestamp:(NSDate *)value + (void)startWithOptions:(SentryOptions *)options { - // We save the options before checking for Xcode preview because - // we will use this options in the preview - startOption = options; if ([SentryDependencyContainer.sharedInstance.processInfoWrapper .environment[SENTRY_XCODE_PREVIEW_ENVIRONMENT_KEY] isEqualToString:@"1"]) { // Using NSLog because SentryLog was not initialized yet. @@ -213,6 +210,7 @@ + (void)startWithOptions:(SentryOptions *)options return; } + startOption = options; [SentryLog configure:options.debug diagnosticLevel:options.diagnosticLevel]; // We accept the tradeoff that the SDK might not be fully initialized directly after diff --git a/Tests/SentryTests/SentrySDKTests.swift b/Tests/SentryTests/SentrySDKTests.swift index b0e507d9c4..9cfef1dea7 100644 --- a/Tests/SentryTests/SentrySDKTests.swift +++ b/Tests/SentryTests/SentrySDKTests.swift @@ -195,7 +195,10 @@ class SentrySDKTests: XCTestCase { } func testDontStartInsideXcodePreview() { - startprocessInfoWrapperForPreview() + let testProcessInfoWrapper = TestSentryNSProcessInfoWrapper() + testProcessInfoWrapper.overrides.environment = ["XCODE_RUNNING_FOR_PREVIEWS": "1"] + + SentryDependencyContainer.sharedInstance().processInfoWrapper = testProcessInfoWrapper SentrySDK.start { options in options.debug = true @@ -558,13 +561,6 @@ class SentrySDKTests: XCTestCase { SentrySDK.start(options: fixture.options) XCTAssertEqual(SentrySDK.options, fixture.options) } - - func testGlobalOptionsForPreview() { - startprocessInfoWrapperForPreview() - - SentrySDK.start(options: fixture.options) - XCTAssertEqual(SentrySDK.options, fixture.options) - } #if SENTRY_HAS_UIKIT func testSetAppStartMeasurement_CallsPrivateSDKCallback() { @@ -978,12 +974,6 @@ class SentrySDKTests: XCTestCase { private func advanceTime(bySeconds: TimeInterval) { fixture.currentDate.setDate(date: SentryDependencyContainer.sharedInstance().dateProvider.date().addingTimeInterval(bySeconds)) } - - private func startprocessInfoWrapperForPreview() { - let testProcessInfoWrapper = TestSentryNSProcessInfoWrapper() - testProcessInfoWrapper.overrides.environment = ["XCODE_RUNNING_FOR_PREVIEWS": "1"] - SentryDependencyContainer.sharedInstance().processInfoWrapper = testProcessInfoWrapper - } } /// Tests in this class aren't part of SentrySDKTests because we need would need to undo a bunch of operations From e431ac50a022308a7aeb59bb040a33b2f0be0bd6 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 29 Jan 2025 09:25:33 +0100 Subject: [PATCH 17/18] Update SentryReplayApi.h --- Sources/Sentry/Public/SentryReplayApi.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/Sentry/Public/SentryReplayApi.h b/Sources/Sentry/Public/SentryReplayApi.h index 6584c5ead8..03a28d6693 100644 --- a/Sources/Sentry/Public/SentryReplayApi.h +++ b/Sources/Sentry/Public/SentryReplayApi.h @@ -58,12 +58,18 @@ NS_ASSUME_NONNULL_BEGIN /** * Shows a overlay on the app the debug session replay masking. + * + * @warning This is an experimental feature and may still have bugs. + * Do not use this is production. */ - (void)showMaskPreview; /** * Shows a overlay on the app to debug session replay masking * with given overlay opacity. + * + * @warning This is an experimental feature and may still have bugs. + * Do not use this is production. */ - (void)showMaskPreview:(CGFloat)opacity; From 02b7ffb1cebbc11467e75b9260b3ab83c6badcc7 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 29 Jan 2025 09:40:27 +0100 Subject: [PATCH 18/18] Update SentryReplayApi.h --- Sources/Sentry/Public/SentryReplayApi.h | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/Sentry/Public/SentryReplayApi.h b/Sources/Sentry/Public/SentryReplayApi.h index 03a28d6693..88a02da80a 100644 --- a/Sources/Sentry/Public/SentryReplayApi.h +++ b/Sources/Sentry/Public/SentryReplayApi.h @@ -57,7 +57,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)stop; /** - * Shows a overlay on the app the debug session replay masking. + * Shows a overlay on the app to debug session replay masking. + * + * By calling this function an overlay will appear covering the parts + * of the app that will be masked for the session replay. This should + * be used for debug purposes only, it will cause some slow frames. * * @warning This is an experimental feature and may still have bugs. * Do not use this is production. @@ -65,8 +69,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)showMaskPreview; /** - * Shows a overlay on the app to debug session replay masking - * with given overlay opacity. + * Shows a overlay on the app to debug session replay masking. + * + * By calling this function an overlay will appear covering the parts + * of the app that will be masked for the session replay. This should + * be used for debug purposes only, it will cause some slow frames. + * + * @param opacity The opacity of the overlay. * * @warning This is an experimental feature and may still have bugs. * Do not use this is production.