Skip to content

Commit 6afd6ed

Browse files
authored
Merge 5555827 into 43815bc
2 parents 43815bc + 5555827 commit 6afd6ed

12 files changed

Lines changed: 175 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- Add protocol for custom screenName for UIViewControllers (#4646)
1818
- Allow hybrid SDK to set replay options tags information (#4710)
1919
- Add threshold to always log fatal logs (#4707)
20+
- Session replay masking preview for SwiftUI (#4737)
2021

2122
### Internal
2223

Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ struct ContentView: View {
137137

138138
return DataBag.shared.info["lastSpan"] as? Span
139139
}
140-
140+
141141
var body: some View {
142142
return SentryTracedView("Content View Body", waitForFullDisplay: true) {
143143
NavigationView {
@@ -235,9 +235,10 @@ struct ContentView: View {
235235
.background(Color.white)
236236
}
237237
SecondView()
238-
238+
239239
Text(TTDInfo)
240240
.accessibilityIdentifier("TTDInfo")
241+
241242
}
242243
}
243244
}
@@ -255,5 +256,7 @@ struct SecondView: View {
255256
struct ContentView_Previews: PreviewProvider {
256257
static var previews: some View {
257258
ContentView()
259+
.sentryReplayPreviewMask(opacity: 0.3)
260+
258261
}
259262
}

Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ struct SwiftUIApp: App {
1010
options.debug = true
1111
options.tracesSampleRate = 1.0
1212
options.profilesSampleRate = 1.0
13-
options.sessionReplay.sessionSampleRate = 1.0
13+
options.sessionReplay.sessionSampleRate = 0.0
1414
options.initialScope = { scope in
1515
scope.injectGitInformation()
1616
return scope

Sentry.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,9 @@
874874
D867063F27C3BC2400048851 /* SentryCoreDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063C27C3BC2400048851 /* SentryCoreDataTracker.h */; };
875875
D86B6835294348A400B8B1FC /* SentryAttachment+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */; };
876876
D86F419827C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */; };
877+
D8709AC42D3E9C63006C491E /* SentryReplayMaskPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */; };
878+
D8709ACB2D3F848E006C491E /* SentryReplayMaskPreviewUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */; };
879+
D8709ACD2D3F84CF006C491E /* PreviewRedactOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */; };
877880
D8739CF32BECF70F007D2F66 /* SentryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8739CF22BECF70F007D2F66 /* SentryLevel.swift */; };
878881
D8739CF92BECFFB5007D2F66 /* SentryTransactionNameSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */; };
879882
D8739D142BEE5049007D2F66 /* SentryRRWebSpanEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8739D132BEE5049007D2F66 /* SentryRRWebSpanEvent.swift */; };
@@ -1984,6 +1987,9 @@
19841987
D86B6820293F39E000B8B1FC /* TestSentryViewHierarchy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestSentryViewHierarchy.h; sourceTree = "<group>"; };
19851988
D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryAttachment+Private.h"; path = "include/SentryAttachment+Private.h"; sourceTree = "<group>"; };
19861989
D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackerExtension.swift; sourceTree = "<group>"; };
1990+
D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayMaskPreview.swift; sourceTree = "<group>"; };
1991+
D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayMaskPreviewUIView.swift; sourceTree = "<group>"; };
1992+
D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewRedactOptions.swift; sourceTree = "<group>"; };
19871993
D8739CF22BECF70F007D2F66 /* SentryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLevel.swift; sourceTree = "<group>"; };
19881994
D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTransactionNameSource.swift; sourceTree = "<group>"; };
19891995
D8739D132BEE5049007D2F66 /* SentryRRWebSpanEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRRWebSpanEvent.swift; sourceTree = "<group>"; };
@@ -3779,6 +3785,7 @@
37793785
isa = PBXGroup;
37803786
children = (
37813787
D8199DB429376ECC0074249E /* SentryInternal */,
3788+
D8709AC92D3F83A6006C491E /* Preview */,
37823789
D8199DB529376ECC0074249E /* SentrySwiftUI.h */,
37833790
D88D25E92B8E0BAC0073C3D5 /* module.modulemap */,
37843791
D8199DB629376ECC0074249E /* SentryTracedView.swift */,
@@ -3876,6 +3883,16 @@
38763883
name = CoreData;
38773884
sourceTree = "<group>";
38783885
};
3886+
D8709AC92D3F83A6006C491E /* Preview */ = {
3887+
isa = PBXGroup;
3888+
children = (
3889+
D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */,
3890+
D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */,
3891+
D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */,
3892+
);
3893+
path = Preview;
3894+
sourceTree = "<group>";
3895+
};
38793896
D8739CF62BECFF86007D2F66 /* Log */ = {
38803897
isa = PBXGroup;
38813898
children = (
@@ -5313,7 +5330,10 @@
53135330
isa = PBXSourcesBuildPhase;
53145331
buildActionMask = 2147483647;
53155332
files = (
5333+
D8709AC42D3E9C63006C491E /* SentryReplayMaskPreview.swift in Sources */,
53165334
D8199DC129376EEC0074249E /* SentryTracedView.swift in Sources */,
5335+
D8709ACD2D3F84CF006C491E /* PreviewRedactOptions.swift in Sources */,
5336+
D8709ACB2D3F848E006C491E /* SentryReplayMaskPreviewUIView.swift in Sources */,
53175337
D48E8B9D2D3E82AC0032E35E /* SentrySpanOperation.swift in Sources */,
53185338
D8199DBF29376EE20074249E /* SentryInternal.m in Sources */,
53195339
D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */,

Sources/Sentry/SentrySDK.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,14 +203,16 @@ + (void)setStartTimestamp:(NSDate *)value
203203

204204
+ (void)startWithOptions:(SentryOptions *)options
205205
{
206+
// We save the options before checking for xcode preview because
207+
// we will use this options in the preview
208+
startOption = options;
206209
if ([SentryDependencyContainer.sharedInstance.processInfoWrapper
207210
.environment[SENTRY_XCODE_PREVIEW_ENVIRONMENT_KEY] isEqualToString:@"1"]) {
208211
// Using NSLog because SentryLog was not initialized yet.
209212
NSLog(@"[SENTRY] [WARNING] SentrySDK not started. Running from Xcode preview.");
210213
return;
211214
}
212215

213-
startOption = options;
214216
[SentryLog configure:options.debug diagnosticLevel:options.diagnosticLevel];
215217

216218
// We accept the tradeoff that the SDK might not be fully initialized directly after
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS)
2+
import Sentry
3+
4+
public class PreviewRedactOptions: SentryRedactOptions {
5+
public let maskAllText: Bool
6+
public let maskAllImages: Bool
7+
public let maskedViewClasses: [AnyClass]
8+
public let unmaskedViewClasses: [AnyClass]
9+
10+
public init(maskAllText: Bool = true, maskAllImages: Bool = true, maskedViewClasses: [AnyClass] = [], unmaskedViewClasses: [AnyClass] = []) {
11+
self.maskAllText = maskAllText
12+
self.maskAllImages = maskAllImages
13+
self.maskedViewClasses = maskedViewClasses
14+
self.unmaskedViewClasses = unmaskedViewClasses
15+
}
16+
}
17+
18+
#endif
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS)
2+
import Sentry
3+
import SwiftUI
4+
import UIKit
5+
6+
#if CARTHAGE || SWIFT_PACKAGE
7+
@_implementationOnly import SentryInternal
8+
#endif
9+
10+
@available(iOS 13, macOS 10.15, tvOS 13, *)
11+
struct SentryReplayMaskPreview: ViewModifier {
12+
let redactOptions: SentryRedactOptions
13+
let opacity: Float
14+
func body(content: Content) -> some View {
15+
content.overlay(SentryReplayPreviewView(redactOptions: redactOptions, opacity: opacity))
16+
}
17+
}
18+
19+
@available(iOS 13, macOS 10.15, tvOS 13, *)
20+
public extension View {
21+
func sentryReplayPreviewMask(redactOptions: SentryRedactOptions? = nil, opacity: Float = 1) -> some View {
22+
let options = redactOptions ?? SentrySDK.options?.sessionReplay ?? PreviewRedactOptions()
23+
return modifier(SentryReplayMaskPreview(redactOptions: options, opacity: opacity))
24+
}
25+
}
26+
27+
@available(iOS 13, macOS 10.15, tvOS 13, *)
28+
struct SentryReplayPreviewView: UIViewRepresentable {
29+
let redactOptions: SentryRedactOptions
30+
let opacity: Float
31+
32+
func makeUIView(context: Context) -> UIView {
33+
let view = SentryReplayMaskPreviewUIView(redactOptions: redactOptions)
34+
view.isUserInteractionEnabled = false
35+
return view
36+
}
37+
38+
func updateUIView(_ uiView: UIView, context: Context) {
39+
(uiView as? SentryReplayMaskPreviewUIView)?.opacity = opacity
40+
}
41+
}
42+
43+
#endif
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS)
2+
import Sentry
3+
import SwiftUI
4+
import UIKit
5+
6+
#if CARTHAGE || SWIFT_PACKAGE
7+
@_implementationOnly import SentryInternal
8+
#endif
9+
10+
class SentryReplayMaskPreviewUIView: UIView {
11+
private let photographer: SentryViewPhotographer
12+
private var displayLink: CADisplayLink?
13+
private var imageView = UIImageView()
14+
15+
var opacity: Float {
16+
get { return Float(imageView.alpha) }
17+
set { imageView.alpha = CGFloat(newValue)}
18+
}
19+
20+
init(redactOptions: SentryRedactOptions) {
21+
self.photographer = SentryViewPhotographer(renderer: PreviewRederer(), redactOptions: redactOptions)
22+
super.init(frame: .zero)
23+
self.isUserInteractionEnabled = false
24+
imageView.isUserInteractionEnabled = false
25+
imageView.sentryReplayUnmask()
26+
}
27+
28+
required init?(coder: NSCoder) {
29+
fatalError("init(coder:) has not been implemented")
30+
}
31+
32+
override func didMoveToSuperview() {
33+
if ProcessInfo.processInfo.environment[SENTRY_XCODE_PREVIEW_ENVIRONMENT_KEY] == "1" {
34+
displayLink = CADisplayLink(target: self, selector: #selector(update))
35+
displayLink?.add(to: .main, forMode: .common)
36+
} else {
37+
print("[SENTRY] [WARNING] SentryReplayMaskPreview is not meant to be used in your app, only with SwiftUI Previews.")
38+
}
39+
}
40+
41+
@objc
42+
private func update() {
43+
guard let window = self.window else { return }
44+
self.photographer.image(view: window) { image in
45+
DispatchQueue.main.async {
46+
self.showImage(image: image)
47+
}
48+
}
49+
}
50+
51+
private func showImage(image: UIImage) {
52+
guard let window = super.window else { return }
53+
if imageView.superview != window {
54+
window.addSubview(imageView)
55+
}
56+
imageView.image = image
57+
imageView.frame = window.bounds
58+
}
59+
}
60+
61+
class PreviewRederer: ViewRenderer {
62+
func render(view: UIView) -> UIImage {
63+
return UIGraphicsImageRenderer(size: view.frame.size, format: .init(for: .init(displayScale: 1))).image { _ in
64+
// Creates a transparent image of the view size that will be used to drawn the redact regions.
65+
// Transparent background is the default, so no additional drawing is required.
66+
// Left blank on purpose
67+
}
68+
}
69+
}
70+
71+
#endif

Sources/SentrySwiftUI/SentryInternal/SentryInternal.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
NS_ASSUME_NONNULL_BEGIN
2626

27+
extern NSString *const SENTRY_XCODE_PREVIEW_ENVIRONMENT_KEY;
28+
2729
typedef NS_ENUM(NSInteger, SentryTransactionNameSource);
2830

2931
@class SentrySpanId;
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
@objc
4-
protocol SentryRedactOptions {
4+
public protocol SentryRedactOptions {
55
var maskAllText: Bool { get }
66
var maskAllImages: Bool { get }
77
var maskedViewClasses: [AnyClass] { get }
@@ -10,8 +10,8 @@ protocol SentryRedactOptions {
1010

1111
@objcMembers
1212
final class SentryRedactDefaultOptions: NSObject, SentryRedactOptions {
13-
var maskAllText: Bool = true
14-
var maskAllImages: Bool = true
15-
var maskedViewClasses: [AnyClass] = []
16-
var unmaskedViewClasses: [AnyClass] = []
13+
public var maskAllText: Bool = true
14+
public var maskAllImages: Bool = true
15+
public var maskedViewClasses: [AnyClass] = []
16+
public var unmaskedViewClasses: [AnyClass] = []
1717
}

0 commit comments

Comments
 (0)