Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

ref: SwiftUI custom redact #4392

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
### Features

- Added breadcrumb.origin private field (#4358)
- Custom redact modifier for SwiftUI (#4362)
- Custom redact modifier for SwiftUI (#4362, #4392)

### Improvements

- Speed up HTTP tracking for multiple requests in parallel (#4366)
- Slightly speed up SentryInAppLogic (#4370)
- Rename session replay `redact` options and APIs to `mask` (#4373)
- Stop canceling timer for manual transactions (#4380)

## 8.37.0-beta.1
Expand Down
4 changes: 2 additions & 2 deletions Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ - (BOOL)application:(UIApplication *)application
options.failedRequestStatusCodes = @[ httpStatusCodeRange ];

options.experimental.sessionReplay.quality = SentryReplayQualityMedium;
options.experimental.sessionReplay.redactAllText = true;
options.experimental.sessionReplay.redactAllImages = true;
options.experimental.sessionReplay.maskAllText = true;
options.experimental.sessionReplay.maskAllImages = true;
options.experimental.sessionReplay.sessionSampleRate = 0;
options.experimental.sessionReplay.onErrorSampleRate = 1;

Expand Down
2 changes: 1 addition & 1 deletion Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
options.debug = true

if #available(iOS 16.0, *), !args.contains("--disable-session-replay") {
options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1, redactAllText: true, redactAllImages: true)
options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true)
options.experimental.sessionReplay.quality = .high
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ class SRRedactSampleViewController: UIViewController {
notRedactedView.backgroundColor = .green
notRedactedView.transform = CGAffineTransform(rotationAngle: 45 * .pi / 180.0)

SentrySDK.replay.ignoreView(notRedactedView)
SentrySDK.replay.maskView(notRedactedView)
}
}
20 changes: 11 additions & 9 deletions Samples/iOS-SwiftUI/iOS-SwiftUI/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,16 @@ struct ContentView: View {
return SentryTracedView("Content View Body") {
NavigationView {
VStack(alignment: HorizontalAlignment.center, spacing: 16) {
Text(getCurrentTracer()?.transactionContext.name ?? "NO SPAN")
.accessibilityIdentifier("TRANSACTION_NAME")
Text(getCurrentTracer()?.transactionContext.spanId.sentrySpanIdString ?? "NO ID")
.accessibilityIdentifier("TRANSACTION_ID")

Text(getCurrentTracer()?.transactionContext.origin ?? "NO ORIGIN")
.accessibilityIdentifier("TRACE_ORIGIN")

Group {
Text(getCurrentTracer()?.transactionContext.name ?? "NO SPAN")
.accessibilityIdentifier("TRANSACTION_NAME")
Text(getCurrentTracer()?.transactionContext.spanId.sentrySpanIdString ?? "NO ID")
.accessibilityIdentifier("TRANSACTION_ID")
.sentryReplayMask()

Text(getCurrentTracer()?.transactionContext.origin ?? "NO ORIGIN")
.accessibilityIdentifier("TRACE_ORIGIN")
}.sentryReplayUnmask()
SentryTracedView("Child Span") {
VStack {
Text(getCurrentSpan()?.spanDescription ?? "NO SPAN")
Expand Down Expand Up @@ -199,7 +201,7 @@ struct ContentView: View {
Text("Form Screen")
}
}
.sentryReplayRedact()
.background(Color.white)
}
SecondView()
}
Expand Down
4 changes: 2 additions & 2 deletions Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ struct SwiftUIApp: App {
options.tracesSampleRate = 1.0
options.profilesSampleRate = 1.0
options.experimental.sessionReplay.sessionSampleRate = 1.0
options.experimental.sessionReplay.redactAllImages = false
options.experimental.sessionReplay.redactAllText = false
options.experimental.sessionReplay.maskAllImages = true
options.experimental.sessionReplay.maskAllText = true
options.initialScope = { scope in
scope.injectGitInformation()
return scope
Expand Down
9 changes: 4 additions & 5 deletions Sources/Sentry/Public/SentryReplayApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@ NS_ASSUME_NONNULL_BEGIN
@interface SentryReplayApi : NSObject

/**
* Marks this view to be redacted during replays.
* Marks this view to be masked during replays.
*
* @warning This is an experimental feature and may still have bugs.
*/
- (void)redactView:(UIView *)view NS_SWIFT_NAME(redactView(_:));
- (void)maskView:(UIView *)view NS_SWIFT_NAME(maskView(_:));

/**
* Marks this view to be ignored during redact step of session replay.
* All its content will be visible in the replay.
* Marks this view to not be masked during redact step of session replay.
*
* @warning This is an experimental feature and may still have bugs.
*/
- (void)ignoreView:(UIView *)view NS_SWIFT_NAME(ignoreView(_:));
- (void)unmaskView:(UIView *)view NS_SWIFT_NAME(unmaskView(_:));

/**
* Pauses the replay.
Expand Down
8 changes: 4 additions & 4 deletions Sources/Sentry/SentryReplayApi.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@

@implementation SentryReplayApi

- (void)redactView:(UIView *)view
- (void)maskView:(UIView *)view
{
[SentryRedactViewHelper redactView:view];
[SentryRedactViewHelper maskView:view];
}

- (void)ignoreView:(UIView *)view
- (void)unmaskView:(UIView *)view
{
[SentryRedactViewHelper ignoreView:view];
[SentryRedactViewHelper unmaskView:view];
}

- (void)pause
Expand Down
1 change: 1 addition & 0 deletions Sources/SentrySwiftUI/SentryInternal/SentryInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSInteger, SentryTransactionNameSource);

@class UIView;
@class SentrySpanId;
@protocol SentrySpan;

Expand Down
43 changes: 34 additions & 9 deletions Sources/SentrySwiftUI/SentryReplayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,67 @@
import SwiftUI
import UIKit

#if CARTHAGE || SWIFT_PACKAGE
@_implementationOnly import SentryInternal
#endif

enum MaskBehaviour {
case mask
case unmask
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
struct SentryReplayView: UIViewRepresentable {
let maskBehaviour: MaskBehaviour

class SentryRedactView: UIView {
}

func makeUIView(context: Context) -> UIView {
let result = SentryRedactView()
result.sentryReplayRedact()
return result
let view = SentryRedactView()
view.isUserInteractionEnabled = false
return view

Check warning on line 25 in Sources/SentrySwiftUI/SentryReplayView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SentrySwiftUI/SentryReplayView.swift#L23-L25

Added lines #L23 - L25 were not covered by tests
}

func updateUIView(_ uiView: UIView, context: Context) {
// This is blank on purpose. UIViewRepresentable requires this function.
switch maskBehaviour {
case .mask: SentryRedactViewHelper.maskSwiftUI(uiView)
case .unmask: SentryRedactViewHelper.clipOutView(uiView)

Check warning on line 31 in Sources/SentrySwiftUI/SentryReplayView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SentrySwiftUI/SentryReplayView.swift#L29-L31

Added lines #L29 - L31 were not covered by tests
}
}
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
struct SentryReplayModifier: ViewModifier {
let behaviour: MaskBehaviour
func body(content: Content) -> some View {
content.background(SentryReplayView())
content.overlay(SentryReplayView(maskBehaviour: behaviour))

Check warning on line 40 in Sources/SentrySwiftUI/SentryReplayView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SentrySwiftUI/SentryReplayView.swift#L40

Added line #L40 was not covered by tests
}
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public extension View {

/// Marks the view as containing sensitive information that should be redacted during replays.
/// Marks the view as containing sensitive information that should be masked during replays.
///
/// When this modifier is applied, any sensitive content within the view will be hidden or masked
/// When this modifier is applied, any sensitive content within the view will be masked
/// during session replays to ensure user privacy. This is useful for views containing personal
/// data or confidential information that shouldn't be visible when the replay is reviewed.
///
/// - Returns: A modifier that redacts sensitive information during session replays.
/// - Experiment: This is an experimental feature and may still have bugs.
func sentryReplayRedact() -> some View {
modifier(SentryReplayModifier())
func sentryReplayMask() -> some View {
modifier(SentryReplayModifier(behaviour: .mask))
}

/// Marks the view as safe to not be masked during session replay.
///
/// Anything that is behind this view will also not be masked anymore.
///
/// - Returns: A modifier that prevents a view from being masked in the session replay.
/// - Experiment: This is an experimental feature and may still have bugs.
func sentryReplayUnmask() -> some View {
modifier(SentryReplayModifier(behaviour: .unmask))
}
}
#endif
9 changes: 5 additions & 4 deletions Sources/Swift/Extensions/UIViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ public extension UIView {
* Marks this view to be redacted during replays.
* - experiment: This is an experimental feature and may still have bugs.
*/
func sentryReplayRedact() {
SentryRedactViewHelper.redactView(self)
func sentryReplayMask() {
SentryRedactViewHelper.maskView(self)
}

/**
* Marks this view to be ignored during redact step
* of session replay. All its content will be visible in the replay.
* - experiment: This is an experimental feature and may still have bugs.
*/
func sentryReplayIgnore() {
SentryRedactViewHelper.ignoreView(self)
func sentryReplayUnmask() {
SentryRedactViewHelper.unmaskView(self)
}

}

#endif
Expand Down
20 changes: 10 additions & 10 deletions Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
*
* - note: The default is true
*/
public var redactAllText = true
public var maskAllText = true

/**
* Indicates whether session replay should redact all non-bundled image
* in the app by drawing a black rectangle over it.
*
* - note: The default is true
*/
public var redactAllImages = true
public var maskAllImages = true

/**
* Indicates the quality of the replay.
Expand All @@ -73,15 +73,15 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
* By default Sentry already mask text and image elements from UIKit
* Every child of a view that is redacted will also be redacted.
*/
public var redactViewClasses = [AnyClass]()
public var maskedViewClasses = [AnyClass]()

/**
* A list of custom UIView subclasses to be ignored
* during masking step of the session replay.
* The views of given classes will not be redacted but their children may be.
* This property has precedence over `redactViewTypes`.
*/
public var ignoreViewClasses = [AnyClass]()
public var unmaskedViewClasses = [AnyClass]()

/**
* Defines the quality of the session replay.
Expand Down Expand Up @@ -139,18 +139,18 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
* - errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with
* error events.
*/
public init(sessionSampleRate: Float = 0, onErrorSampleRate: Float = 0, redactAllText: Bool = true, redactAllImages: Bool = true) {
public init(sessionSampleRate: Float = 0, onErrorSampleRate: Float = 0, maskAllText: Bool = true, maskAllImages: Bool = true) {
self.sessionSampleRate = sessionSampleRate
self.onErrorSampleRate = onErrorSampleRate
self.redactAllText = redactAllText
self.redactAllImages = redactAllImages
self.maskAllText = maskAllText
self.maskAllImages = maskAllImages
}

convenience init(dictionary: [String: Any]) {
let sessionSampleRate = (dictionary["sessionSampleRate"] as? NSNumber)?.floatValue ?? 0
let onErrorSampleRate = (dictionary["errorSampleRate"] as? NSNumber)?.floatValue ?? 0
let redactAllText = (dictionary["redactAllText"] as? NSNumber)?.boolValue ?? true
let redactAllImages = (dictionary["redactAllImages"] as? NSNumber)?.boolValue ?? true
self.init(sessionSampleRate: sessionSampleRate, onErrorSampleRate: onErrorSampleRate, redactAllText: redactAllText, redactAllImages: redactAllImages)
let maskAllText = (dictionary["maskAllText"] as? NSNumber)?.boolValue ?? true
let maskAllImages = (dictionary["maskAllImages"] as? NSNumber)?.boolValue ?? true
self.init(sessionSampleRate: sessionSampleRate, onErrorSampleRate: onErrorSampleRate, maskAllText: maskAllText, maskAllImages: maskAllImages)
}
}
8 changes: 4 additions & 4 deletions Sources/Swift/Protocol/SentryRedactOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import Foundation

@objc
protocol SentryRedactOptions {
var redactAllText: Bool { get }
var redactAllImages: Bool { get }
var redactViewClasses: [AnyClass] { get }
var ignoreViewClasses: [AnyClass] { get }
var maskAllText: Bool { get }
var maskAllImages: Bool { get }
var maskedViewClasses: [AnyClass] { get }
var unmaskedViewClasses: [AnyClass] { get }
}
2 changes: 1 addition & 1 deletion Sources/Swift/Tools/SentryViewPhotographer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider {
let path = CGPath(rect: rect, transform: &transform)

switch region.type {
case .redact:
case .redact, .redactSwiftUI:
(region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: rect.applying(region.transform))).setFill()
context.cgContext.addPath(path)
context.cgContext.fillPath()
Expand Down
Loading
Loading