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 all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Added breadcrumb.origin private field (#4358)
- Custom redact modifier for SwiftUI (#4362, #4392)
- Custom redact modifier for SwiftUI (#4362)
- Add support for arm64e (#3398)
- Add mergeable libraries support to dynamic libraries (#4381)
Expand Down
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")
}
}
.sentryReplayMask()
.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.maskAllImages = false
options.experimental.sessionReplay.maskAllText = false
options.experimental.sessionReplay.maskAllImages = true
options.experimental.sessionReplay.maskAllText = true
options.initialScope = { scope in
scope.injectGitInformation()
return scope
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
37 changes: 31 additions & 6 deletions Sources/SentrySwiftUI/SentryReplayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,41 @@
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.sentryReplayMask()
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
}
}

Expand All @@ -38,7 +53,17 @@
/// - Returns: A modifier that redacts sensitive information during session replays.
/// - Experiment: This is an experimental feature and may still have bugs.
func sentryReplayMask() -> some View {
modifier(SentryReplayModifier())
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
1 change: 1 addition & 0 deletions Sources/Swift/Extensions/UIViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public extension UIView {
func sentryReplayUnmask() {
SentryRedactViewHelper.unmaskView(self)
}

}

#endif
Expand Down
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
54 changes: 45 additions & 9 deletions Sources/Swift/Tools/UIRedactBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
/// Pop the last Pushed region from the drawing context.
/// Used after prossing every child of a view that clip to its bounds.
case clipEnd

/// These regions are redacted first, there is no way to avoid it.
case redactSwiftUI
}

struct RedactRegion {
Expand Down Expand Up @@ -155,7 +158,19 @@
rootFrame: view.frame,
transform: CGAffineTransform.identity)

return redactingRegions.reversed()
var swiftUIRedact = [RedactRegion]()
var otherRegions = [RedactRegion]()

for region in redactingRegions {
if region.type == .redactSwiftUI {
swiftUIRedact.append(region)

Check warning on line 166 in Sources/Swift/Tools/UIRedactBuilder.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Tools/UIRedactBuilder.swift#L166

Added line #L166 was not covered by tests
} else {
otherRegions.append(region)
}
}

//The swiftUI type needs to appear first in the list so it always get masked
return swiftUIRedact + otherRegions.reversed()
}

private func shouldIgnore(view: UIView) -> Bool {
Expand Down Expand Up @@ -187,11 +202,12 @@
let newTransform = concatenateTranform(transform, with: layer)

let ignore = !forceRedact && shouldIgnore(view: view)
let redact = forceRedact || shouldRedact(view: view)
let swiftUI = SentryRedactViewHelper.shouldRedactSwiftUI(view)
let redact = forceRedact || shouldRedact(view: view) || swiftUI
var enforceRedact = forceRedact

if !ignore && redact {
redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .redact, color: self.color(for: view)))
redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: swiftUI ? .redactSwiftUI : .redact, color: self.color(for: view)))

guard !view.clipsToBounds else { return }
enforceRedact = true
Expand Down Expand Up @@ -248,14 +264,22 @@
Indicates whether the view is opaque and will block other view behind it
*/
private func isOpaque(_ view: UIView) -> Bool {
return view.alpha == 1 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1
return SentryRedactViewHelper.shouldClipOut(view) || (view.alpha == 1 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1)
}
}

@objcMembers
class SentryRedactViewHelper: NSObject {
public class SentryRedactViewHelper: NSObject {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’m not super happy about making this class public, but SentrySwiftUI is an external module that needs access to it. Also, it’s impossible to misuse it in a way that would cause a crash.

Copy link
Member

Choose a reason for hiding this comment

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

I see that you added some new methods. Maybe you can create an extra SentryRedactViewHelper just for SentrySwiftUI so you don't need to make it public?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not possible, I need to read it from RedactBuilder class that needs to be in the Sentry SDK.
Maybe if we split SR into a different module already we would need to worry less about this being public

private static var associatedRedactObjectHandle: UInt8 = 0
private static var associatedIgnoreObjectHandle: UInt8 = 0
private static var associatedClipOutObjectHandle: UInt8 = 0
private static var associatedSwiftUIRedactObjectHandle: UInt8 = 0

override private init() {}

static func maskView(_ view: UIView) {
objc_setAssociatedObject(view, &associatedRedactObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN)
}

static func shouldMaskView(_ view: UIView) -> Bool {
(objc_getAssociatedObject(view, &associatedRedactObjectHandle) as? NSNumber)?.boolValue ?? false
Expand All @@ -265,13 +289,25 @@
(objc_getAssociatedObject(view, &associatedIgnoreObjectHandle) as? NSNumber)?.boolValue ?? false
}

static func maskView(_ view: UIView) {
objc_setAssociatedObject(view, &associatedRedactObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN)
}

static func unmaskView(_ view: UIView) {
objc_setAssociatedObject(view, &associatedIgnoreObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN)
}

static func shouldClipOut(_ view: UIView) -> Bool {
(objc_getAssociatedObject(view, &associatedClipOutObjectHandle) as? NSNumber)?.boolValue ?? false
}

static public func clipOutView(_ view: UIView) {
objc_setAssociatedObject(view, &associatedClipOutObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN)

Check warning on line 301 in Sources/Swift/Tools/UIRedactBuilder.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Tools/UIRedactBuilder.swift#L301

Added line #L301 was not covered by tests
}

static func shouldRedactSwiftUI(_ view: UIView) -> Bool {
(objc_getAssociatedObject(view, &associatedSwiftUIRedactObjectHandle) as? NSNumber)?.boolValue ?? false
brustolin marked this conversation as resolved.
Show resolved Hide resolved
}

static public func maskSwiftUI(_ view: UIView) {
objc_setAssociatedObject(view, &associatedSwiftUIRedactObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN)

Check warning on line 309 in Sources/Swift/Tools/UIRedactBuilder.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Tools/UIRedactBuilder.swift#L309

Added line #L309 was not covered by tests
}
}

#endif
Expand Down
9 changes: 8 additions & 1 deletion Tests/SentryTests/SwiftUI/SentryRedactModifierTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ import XCTest

class SentryRedactModifierTests: XCTestCase {

func testViewRedacted() throws {
func testViewMask() throws {
let text = Text("Hello, World!")
let redactedText = text.sentryReplayMask()

XCTAssertTrue(redactedText is ModifiedContent<Text, SentryReplayModifier>)
}

func testViewUnmask() throws {
let text = Text("Hello, World!")
let redactedText = text.sentryReplayUnmask()

XCTAssertTrue(redactedText is ModifiedContent<Text, SentryReplayModifier>)
}

}

Expand Down
Loading