Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Session replay masking preview for SwiftUI #4737

Merged
merged 12 commits into from
Jan 30, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
### Features

- Add `showMaskPreview` to `SentrySDK.replay` api to debug replay masking (#4761)
- Session replay masking preview for SwiftUI (#4737)

## 8.44.0-beta.1

Expand Down
5 changes: 3 additions & 2 deletions Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -235,7 +235,7 @@ struct ContentView: View {
.background(Color.white)
}
SecondView()

Text(TTDInfo)
.accessibilityIdentifier("TTDInfo")
}
Expand All @@ -255,5 +255,6 @@ struct SecondView: View {
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.sentryReplayPreviewMask(opacity: 0.3)
}
}
20 changes: 20 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,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 */; };
Expand Down Expand Up @@ -1985,6 +1988,9 @@
D86B6820293F39E000B8B1FC /* TestSentryViewHierarchy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestSentryViewHierarchy.h; sourceTree = "<group>"; };
D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryAttachment+Private.h"; path = "include/SentryAttachment+Private.h"; sourceTree = "<group>"; };
D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackerExtension.swift; sourceTree = "<group>"; };
D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayMaskPreview.swift; sourceTree = "<group>"; };
D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayMaskPreviewUIView.swift; sourceTree = "<group>"; };
D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewRedactOptions.swift; sourceTree = "<group>"; };
D8739CF22BECF70F007D2F66 /* SentryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLevel.swift; sourceTree = "<group>"; };
D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTransactionNameSource.swift; sourceTree = "<group>"; };
D8739D132BEE5049007D2F66 /* SentryRRWebSpanEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRRWebSpanEvent.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3779,6 +3785,7 @@
isa = PBXGroup;
children = (
D8199DB429376ECC0074249E /* SentryInternal */,
D8709AC92D3F83A6006C491E /* Preview */,
D8199DB529376ECC0074249E /* SentrySwiftUI.h */,
D88D25E92B8E0BAC0073C3D5 /* module.modulemap */,
D8199DB629376ECC0074249E /* SentryTracedView.swift */,
Expand Down Expand Up @@ -3876,6 +3883,16 @@
name = CoreData;
sourceTree = "<group>";
};
D8709AC92D3F83A6006C491E /* Preview */ = {
isa = PBXGroup;
children = (
D8709ACA2D3F8480006C491E /* SentryReplayMaskPreviewUIView.swift */,
D8709AC32D3E9C5C006C491E /* SentryReplayMaskPreview.swift */,
D8709ACC2D3F84C9006C491E /* PreviewRedactOptions.swift */,
);
path = Preview;
sourceTree = "<group>";
};
D8739CF62BECFF86007D2F66 /* Log */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5323,7 +5340,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 */,
D48E8B9D2D3E82AC0032E35E /* SentrySpanOperation.swift in Sources */,
D8199DBF29376EE20074249E /* SentryInternal.m in Sources */,
D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion SentrySwiftUI.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ Pod::Spec.new do |s|
s.watchos.framework = 'WatchKit'

s.source_files = "Sources/SentrySwiftUI/**/*.{swift,h,m}"
s.dependency 'Sentry', "8.44.0-beta.1"
s.dependency 'Sentry/HybridSDK', "8.44.0-beta.1"
end
6 changes: 6 additions & 0 deletions Sources/Sentry/PrivateSentrySDKOnly.mm
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,12 @@
}

#if SENTRY_TARGET_REPLAY_SUPPORTED

+ (UIView *)sessionReplayMaskingOverlay:(id<SentryRedactOptions>)options
{
return [[SentryMaskingPreviewView alloc] initWithRedactOptions:options];
}

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

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/PrivateSentrySDKOnly.mm#L327-L328

Added lines #L327 - L328 were not covered by tests

+ (nullable SentrySessionReplayIntegration *)getReplayIntegration
{

Expand Down
4 changes: 3 additions & 1 deletion Sources/Sentry/SentrySDK.m
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,16 @@ + (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.
NSLog(@"[SENTRY] [WARNING] SentrySDK not started. Running from Xcode preview.");
return;
}

startOption = options;
[SentryLog configure:options.debug diagnosticLevel:options.diagnosticLevel];

// We accept the tradeoff that the SDK might not be fully initialized directly after
Expand Down
16 changes: 8 additions & 8 deletions Sources/Sentry/SentrySessionReplayIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
id<SentryRateLimits> _rateLimits;
id<SentryViewScreenshotProvider> _currentScreenshotProvider;
id<SentryReplayBreadcrumbConverter> _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
Expand Down Expand Up @@ -629,19 +629,19 @@
return;
}

if (previewView == nil) {
previewView = [[SentryMaskingPreviewView alloc] initWithRedactOptions:_replayOptions];
if (_previewView == nil) {
_previewView = [[SentryMaskingPreviewView alloc] initWithRedactOptions:_replayOptions];
}

previewView.opacity = opacity;
[previewView setFrame:window.bounds];
[window addSubview:previewView];
_previewView.opacity = opacity;
[_previewView setFrame:window.bounds];
[window addSubview:_previewView];
}

- (void)hideMaskPreview
{
[previewView removeFromSuperview];
previewView = nil;
[_previewView removeFromSuperview];
_previewView = nil;

Check warning on line 644 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L643-L644

Added lines #L643 - L644 were not covered by tests
}

# pragma mark - SentryReachabilityObserver
Expand Down
9 changes: 9 additions & 0 deletions Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
@class SentryEnvelope;
@class SentryId;
@class SentrySessionReplayIntegration;
@class UIView;

@protocol SentryReplayBreadcrumbConverter;
@protocol SentryViewScreenshotProvider;
@protocol SentryRedactOptions;

NS_ASSUME_NONNULL_BEGIN

Expand Down Expand Up @@ -184,6 +186,13 @@ typedef void (^SentryOnAppStartMeasurementAvailable)(

#if SENTRY_TARGET_REPLAY_SUPPORTED

/**
* Return an instance of SentryRedactOptions with given option
* To be used from SentrySwiftUI, which cannot access the private
* `SentryRedactOptions` class.
*/
+ (UIView *)sessionReplayMaskingOverlay:(id<SentryRedactOptions>)options;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since SentryMaskingPreviewView is an internal Swift class from Sentry, we cant use from SentrySwiftUI. In order to solve this, we just use PrivateSentrySDKOnly to return it as a UIView.


/**
* Configure session replay with different breadcrumb converter
* and screeshot provider. Used by the Hybrid SDKs.
Expand Down
18 changes: 18 additions & 0 deletions Sources/SentrySwiftUI/Preview/PreviewRedactOptions.swift
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions Sources/SentrySwiftUI/Preview/SentryReplayMaskPreview.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#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 = CGFloat(opacity)
}
}

#endif
36 changes: 36 additions & 0 deletions Sources/SentrySwiftUI/Preview/SentryReplayMaskPreviewUIView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#if canImport(SwiftUI) && canImport(UIKit) && os(iOS) || os(tvOS)
import Sentry
import UIKit

#if CARTHAGE || SWIFT_PACKAGE
import Sentry._Hybrid
@_implementationOnly import SentryInternal
#endif

class SentryReplayMaskPreviewUIView: UIView {

private let maskingOverlay: UIView

var opacity: CGFloat {
get { maskingOverlay.alpha }
set { maskingOverlay.alpha = newValue }
}

init(redactOptions: SentryRedactOptions) {
maskingOverlay = PrivateSentrySDKOnly.sessionReplayMaskingOverlay(redactOptions)
super.init(frame: .zero)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func didMoveToWindow() {
super.didMoveToWindow()
guard let window = self.window else { return }
maskingOverlay.frame = window.bounds
window.addSubview(maskingOverlay)
}
}

#endif
2 changes: 2 additions & 0 deletions Sources/SentrySwiftUI/SentryInternal/SentryInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

NS_ASSUME_NONNULL_BEGIN

extern NSString *const SENTRY_XCODE_PREVIEW_ENVIRONMENT_KEY;

typedef NS_ENUM(NSInteger, SentryTransactionNameSource);

@class SentrySpanId;
Expand Down
18 changes: 14 additions & 4 deletions Tests/SentryTests/SentrySDKTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
Loading