Skip to content

Commit 4d3f509

Browse files
authored
Merge 0b11da7 into a646e5c
2 parents a646e5c + 0b11da7 commit 4d3f509

File tree

9 files changed

+102
-30
lines changed

9 files changed

+102
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
### Features
66

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

1010
### Improvements
1111

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,16 @@ struct ContentView: View {
118118
return SentryTracedView("Content View Body") {
119119
NavigationView {
120120
VStack(alignment: HorizontalAlignment.center, spacing: 16) {
121-
Text(getCurrentTracer()?.transactionContext.name ?? "NO SPAN")
122-
.accessibilityIdentifier("TRANSACTION_NAME")
123-
Text(getCurrentTracer()?.transactionContext.spanId.sentrySpanIdString ?? "NO ID")
124-
.accessibilityIdentifier("TRANSACTION_ID")
125-
126-
Text(getCurrentTracer()?.transactionContext.origin ?? "NO ORIGIN")
127-
.accessibilityIdentifier("TRACE_ORIGIN")
128-
121+
Group {
122+
Text(getCurrentTracer()?.transactionContext.name ?? "NO SPAN")
123+
.accessibilityIdentifier("TRANSACTION_NAME")
124+
Text(getCurrentTracer()?.transactionContext.spanId.sentrySpanIdString ?? "NO ID")
125+
.accessibilityIdentifier("TRANSACTION_ID")
126+
.sentryReplayMask()
127+
128+
Text(getCurrentTracer()?.transactionContext.origin ?? "NO ORIGIN")
129+
.accessibilityIdentifier("TRACE_ORIGIN")
130+
}.sentryReplayUnmask()
129131
SentryTracedView("Child Span") {
130132
VStack {
131133
Text(getCurrentSpan()?.spanDescription ?? "NO SPAN")
@@ -199,7 +201,7 @@ struct ContentView: View {
199201
Text("Form Screen")
200202
}
201203
}
202-
.sentryReplayRedact()
204+
.background(Color.white)
203205
}
204206
SecondView()
205207
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ struct SwiftUIApp: App {
1111
options.tracesSampleRate = 1.0
1212
options.profilesSampleRate = 1.0
1313
options.experimental.sessionReplay.sessionSampleRate = 1.0
14-
options.experimental.sessionReplay.maskAllImages = false
15-
options.experimental.sessionReplay.maskAllText = false
14+
options.experimental.sessionReplay.maskAllImages = true
15+
options.experimental.sessionReplay.maskAllText = true
1616
options.initialScope = { scope in
1717
scope.injectGitInformation()
1818
return scope

Sources/SentrySwiftUI/SentryInternal/SentryInternal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
1111

1212
typedef NS_ENUM(NSInteger, SentryTransactionNameSource);
1313

14+
@class UIView;
1415
@class SentrySpanId;
1516
@protocol SentrySpan;
1617

Sources/SentrySwiftUI/SentryReplayView.swift

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,41 @@ import Sentry
33
import SwiftUI
44
import UIKit
55

6+
#if CARTHAGE || SWIFT_PACKAGE
7+
@_implementationOnly import SentryInternal
8+
#endif
9+
10+
enum MaskBehaviour {
11+
case mask
12+
case unmask
13+
}
14+
615
@available(iOS 13, macOS 10.15, tvOS 13, *)
716
struct SentryReplayView: UIViewRepresentable {
17+
let maskBehaviour: MaskBehaviour
18+
819
class SentryRedactView: UIView {
920
}
1021

1122
func makeUIView(context: Context) -> UIView {
12-
let result = SentryRedactView()
13-
result.sentryReplayMask()
14-
return result
23+
let view = SentryRedactView()
24+
view.isUserInteractionEnabled = false
25+
return view
1526
}
1627

1728
func updateUIView(_ uiView: UIView, context: Context) {
18-
// This is blank on purpose. UIViewRepresentable requires this function.
29+
switch maskBehaviour {
30+
case .mask: SentryRedactViewHelper.maskSwiftUI(uiView)
31+
case .unmask: SentryRedactViewHelper.clipOutView(uiView)
32+
}
1933
}
2034
}
2135

2236
@available(iOS 13, macOS 10.15, tvOS 13, *)
2337
struct SentryReplayModifier: ViewModifier {
38+
let behaviour: MaskBehaviour
2439
func body(content: Content) -> some View {
25-
content.background(SentryReplayView())
40+
content.overlay(SentryReplayView(maskBehaviour: behaviour))
2641
}
2742
}
2843

@@ -38,7 +53,17 @@ public extension View {
3853
/// - Returns: A modifier that redacts sensitive information during session replays.
3954
/// - Experiment: This is an experimental feature and may still have bugs.
4055
func sentryReplayMask() -> some View {
41-
modifier(SentryReplayModifier())
56+
modifier(SentryReplayModifier(behaviour: .mask))
57+
}
58+
59+
/// Marks the view as safe to not be masked during session replay.
60+
///
61+
/// Anything that is behind this view will also not be masked anymore.
62+
///
63+
/// - Returns: A modifier that prevents a view from being masked in the session replay.
64+
/// - Experiment: This is an experimental feature and may still have bugs.
65+
func sentryReplayUnmask() -> some View {
66+
modifier(SentryReplayModifier(behaviour: .unmask))
4267
}
4368
}
4469
#endif

Sources/Swift/Extensions/UIViewExtensions.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public extension UIView {
2121
func sentryReplayUnmask() {
2222
SentryRedactViewHelper.unmaskView(self)
2323
}
24+
2425
}
2526

2627
#endif

Sources/Swift/Tools/SentryViewPhotographer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider {
5858
let path = CGPath(rect: rect, transform: &transform)
5959

6060
switch region.type {
61-
case .redact:
61+
case .redact, .redactSwiftUI:
6262
(region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: rect.applying(region.transform))).setFill()
6363
context.cgContext.addPath(path)
6464
context.cgContext.fillPath()

Sources/Swift/Tools/UIRedactBuilder.swift

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ enum RedactRegionType {
2222
/// Pop the last Pushed region from the drawing context.
2323
/// Used after prossing every child of a view that clip to its bounds.
2424
case clipEnd
25+
26+
/// These regions are redacted first, there is no way to avoid it.
27+
case redactSwiftUI
2528
}
2629

2730
struct RedactRegion {
@@ -155,7 +158,19 @@ class UIRedactBuilder {
155158
rootFrame: view.frame,
156159
transform: CGAffineTransform.identity)
157160

158-
return redactingRegions.reversed()
161+
var swiftUIRedact = [RedactRegion]()
162+
var otherRegions = [RedactRegion]()
163+
164+
for region in redactingRegions {
165+
if region.type == .redactSwiftUI {
166+
swiftUIRedact.append(region)
167+
} else {
168+
otherRegions.append(region)
169+
}
170+
}
171+
172+
//The swiftUI type needs to appear first in the list so it always get masked
173+
return swiftUIRedact + otherRegions.reversed()
159174
}
160175

161176
private func shouldIgnore(view: UIView) -> Bool {
@@ -187,11 +202,12 @@ class UIRedactBuilder {
187202
let newTransform = concatenateTranform(transform, with: layer)
188203

189204
let ignore = !forceRedact && shouldIgnore(view: view)
190-
let redact = forceRedact || shouldRedact(view: view)
205+
let swiftUI = SentryRedactViewHelper.shouldRedactSwiftUI(view)
206+
let redact = forceRedact || shouldRedact(view: view) || swiftUI
191207
var enforceRedact = forceRedact
192208

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

196212
guard !view.clipsToBounds else { return }
197213
enforceRedact = true
@@ -248,14 +264,22 @@ class UIRedactBuilder {
248264
Indicates whether the view is opaque and will block other view behind it
249265
*/
250266
private func isOpaque(_ view: UIView) -> Bool {
251-
return view.alpha == 1 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1
267+
return SentryRedactViewHelper.shouldClipOut(view) || (view.alpha == 1 && view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) == 1)
252268
}
253269
}
254270

255271
@objcMembers
256-
class SentryRedactViewHelper: NSObject {
272+
public class SentryRedactViewHelper: NSObject {
257273
private static var associatedRedactObjectHandle: UInt8 = 0
258274
private static var associatedIgnoreObjectHandle: UInt8 = 0
275+
private static var associatedClipOutObjectHandle: UInt8 = 0
276+
private static var associatedSwiftUIRedactObjectHandle: UInt8 = 0
277+
278+
override private init() {}
279+
280+
static func maskView(_ view: UIView) {
281+
objc_setAssociatedObject(view, &associatedRedactObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN)
282+
}
259283

260284
static func shouldMaskView(_ view: UIView) -> Bool {
261285
(objc_getAssociatedObject(view, &associatedRedactObjectHandle) as? NSNumber)?.boolValue ?? false
@@ -265,13 +289,25 @@ class SentryRedactViewHelper: NSObject {
265289
(objc_getAssociatedObject(view, &associatedIgnoreObjectHandle) as? NSNumber)?.boolValue ?? false
266290
}
267291

268-
static func maskView(_ view: UIView) {
269-
objc_setAssociatedObject(view, &associatedRedactObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN)
270-
}
271-
272292
static func unmaskView(_ view: UIView) {
273293
objc_setAssociatedObject(view, &associatedIgnoreObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN)
274294
}
295+
296+
static func shouldClipOut(_ view: UIView) -> Bool {
297+
(objc_getAssociatedObject(view, &associatedClipOutObjectHandle) as? NSNumber)?.boolValue ?? false
298+
}
299+
300+
static public func clipOutView(_ view: UIView) {
301+
objc_setAssociatedObject(view, &associatedClipOutObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN)
302+
}
303+
304+
static func shouldRedactSwiftUI(_ view: UIView) -> Bool {
305+
(objc_getAssociatedObject(view, &associatedSwiftUIRedactObjectHandle) as? NSNumber)?.boolValue ?? false
306+
}
307+
308+
static public func maskSwiftUI(_ view: UIView) {
309+
objc_setAssociatedObject(view, &associatedSwiftUIRedactObjectHandle, true, .OBJC_ASSOCIATION_ASSIGN)
310+
}
275311
}
276312

277313
#endif

Tests/SentryTests/SwiftUI/SentryRedactModifierTests.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@ import XCTest
66

77
class SentryRedactModifierTests: XCTestCase {
88

9-
func testViewRedacted() throws {
9+
func testViewMask() throws {
1010
let text = Text("Hello, World!")
11-
let redactedText = text.sentryReplayRedact()
11+
let redactedText = text.sentryReplayMask()
12+
13+
XCTAssertTrue(redactedText is ModifiedContent<Text, SentryReplayModifier>)
14+
}
15+
16+
func testViewUnmask() throws {
17+
let text = Text("Hello, World!")
18+
let redactedText = text.sentryReplayUnmask()
1219

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

0 commit comments

Comments
 (0)