Skip to content

Commit

Permalink
Merge 455ab58 into 817009f
Browse files Browse the repository at this point in the history
  • Loading branch information
brustolin authored Aug 5, 2024
2 parents 817009f + 455ab58 commit e35cf54
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Redact web view from replay (#4203)
- Add beforeCaptureViewHierarchy callback (#4210)
- Rename session replay `errorSampleRate` property to `onErrorSampleRate` (#4218)
- Add options to redact or ignore view for Replay (#4228)

### Fixes

Expand Down
4 changes: 4 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,7 @@
D8AE48B02C5782EC0092A2A6 /* SentryLogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48AF2C5782EC0092A2A6 /* SentryLogOutput.swift */; };
D8AE48BF2C578D540092A2A6 /* SentryLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48BE2C578D540092A2A6 /* SentryLog.swift */; };
D8AE48C12C57B1550092A2A6 /* SentryLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48C02C57B1550092A2A6 /* SentryLevelTests.swift */; };
D8AE49182C5D09720092A2A6 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE49172C5D09720092A2A6 /* UIViewExtensions.swift */; };
D8AFC0012BD252B900118BE1 /* SentryOnDemandReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */; };
D8AFC01A2BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */; };
D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */; };
Expand Down Expand Up @@ -1931,6 +1932,7 @@
D8AE48B12C5786AA0092A2A6 /* SentryLogC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryLogC.h; path = include/SentryLogC.h; sourceTree = "<group>"; };
D8AE48BE2C578D540092A2A6 /* SentryLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLog.swift; sourceTree = "<group>"; };
D8AE48C02C57B1550092A2A6 /* SentryLevelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLevelTests.swift; sourceTree = "<group>"; };
D8AE49172C5D09720092A2A6 /* UIViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = "<group>"; };
D8AFC0002BD252B900118BE1 /* SentryOnDemandReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplayTests.swift; sourceTree = "<group>"; };
D8AFC0192BD7A20B00118BE1 /* SentryViewScreenshotProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewScreenshotProvider.swift; sourceTree = "<group>"; };
D8AFC03C2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayVideoMaker.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3886,6 +3888,7 @@
D8F016B52B962548007B9AFB /* StringExtensions.swift */,
62872B5E2BA1B7F300A4FA7D /* NSLock.swift */,
D8BC28C92BFF68CA0054DA4D /* NumberExtensions.swift */,
D8AE49172C5D09720092A2A6 /* UIViewExtensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -4683,6 +4686,7 @@
0A2D8DA9289BC905008720F6 /* SentryViewHierarchy.m in Sources */,
D84D2CDD2C2BF7370011AF8A /* SentryReplayEvent.swift in Sources */,
D8BC28CC2BFF78220054DA4D /* SentryRRWebTouchEvent.swift in Sources */,
D8AE49182C5D09720092A2A6 /* UIViewExtensions.swift in Sources */,
8EA1ED0B2668F8C400E62B98 /* SentryUIViewControllerSwizzling.m in Sources */,
7B98D7CF25FB650F00C5A389 /* SentryWatchdogTerminationTrackingIntegration.m in Sources */,
8E5D38DD261D4A3E000D363D /* SentryPerformanceTrackingIntegration.m in Sources */,
Expand Down
20 changes: 20 additions & 0 deletions Sources/Sentry/Public/SentrySDK.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@class SentryOptions, SentryEvent, SentryBreadcrumb, SentryScope, SentryUser, SentryId,
SentryUserFeedback, SentryTransactionContext;
@class SentryMetricsAPI;
@class UIView;

NS_ASSUME_NONNULL_BEGIN

Expand Down Expand Up @@ -333,6 +334,25 @@ SENTRY_NO_INIT
*/
+ (void)close;

#if SENTRY_HAS_UIKIT

/**
* @warning This is an experimental feature and may still have bugs.
*
* Marks this view to be redacted during replays.
*/
+ (void)replayRedactView:(UIView *)view;

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

#endif

@end

NS_ASSUME_NONNULL_END
12 changes: 12 additions & 0 deletions Sources/Sentry/SentrySDK.m
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,18 @@ + (void)stopProfiler
}
#endif // SENTRY_TARGET_PROFILING_SUPPORTED

#if SENTRY_HAS_UIKIT
+ (void)replayRedactView:(UIView *)view
{
[SentryRedactViewHelper redactView:view];
}

+ (void)replayIgnoreView:(UIView *)view
{
[SentryRedactViewHelper ignoreView:view];
}
#endif

@end

NS_ASSUME_NONNULL_END
3 changes: 3 additions & 0 deletions Sources/Sentry/SentrySessionReplayIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options
return event;
}];

[SentryViewPhotographer.shared addIgnoreClasses:_replayOptions.ignoreRedactViewTypes];
[SentryViewPhotographer.shared addRedactClasses:_replayOptions.redactViewTypes];

return YES;
}

Expand Down
25 changes: 25 additions & 0 deletions Sources/Swift/Extensions/UIViewExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#if canImport(UIKit) && !SENTRY_NO_UIKIT
#if os(iOS) || os(tvOS)
import Foundation
import UIKit

public extension UIView {

/**
* Marks this view to be redacted during replays.
*/
func sentryReplayRedact() {
SentryRedactViewHelper.redactView(self)
}

/**
* Marks this view to be ignored during redact step
* of session replay. All its content will be visible in the replay.
*/
func sentryReplayIgnore() {
SentryRedactViewHelper.ignoreView(self)
}
}

#endif
#endif
14 changes: 14 additions & 0 deletions Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
*/
public var quality = SentryReplayQuality.low

/**
* A list of custom UIView subclasses that need
* to be masked during session replay.
* By default Sentry already mask text elements from UIKit
*/
public var redactViewTypes = [AnyClass]()

/**
* A list of custom UIView subclasses to be ignored
* during masking step of the session replay.
* The view itself and any child will be ignored and not masked.
*/
public var ignoreRedactViewTypes = [AnyClass]()

/**
* Defines the quality of the session replay.
* Higher bit rates better quality, but also bigger files to transfer.
Expand Down
14 changes: 10 additions & 4 deletions Sources/Swift/Tools/SentryViewPhotographer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider {

static let shared = SentryViewPhotographer()

//This is a list of UIView subclasses that will be ignored during redact process
private var redactBuilder = UIRedactBuilder()
private let redactBuilder = UIRedactBuilder()

func image(view: UIView, options: SentryRedactOptions, onComplete: @escaping ScreenshotCallback ) {
let image = UIGraphicsImageRenderer(size: view.bounds.size).image { _ in
Expand All @@ -36,13 +35,20 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider {

@objc(addIgnoreClasses:)
func addIgnoreClasses(classes: [AnyClass]) {
redactBuilder.ignoreClasses += classes
redactBuilder.addIgnoreClasses(classes)
}

@objc(addRedactClasses:)
func addRedactClasses(classes: [AnyClass]) {
redactBuilder.redactClasses += classes
redactBuilder.addRedactClasses(classes)
}

#if TEST || TESTCI
func getRedactBuild() -> UIRedactBuilder {
redactBuilder
}
#endif

}

#endif // os(iOS) || os(tvOS)
Expand Down
67 changes: 59 additions & 8 deletions Sources/Swift/Tools/UIRedactBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#if canImport(UIKit) && !SENTRY_NO_UIKIT
#if os(iOS) || os(tvOS)
import Foundation
import ObjectiveC.NSObjCRuntime
import UIKit
#if os(iOS)
import WebKit
Expand Down Expand Up @@ -53,13 +54,13 @@ struct RedactRegion {
class UIRedactBuilder {

//This is a list of UIView subclasses that will be ignored during redact process
var ignoreClasses: [AnyClass]
private var ignoreClassesIdentifiers: Set<ObjectIdentifier>
//This is a list of UIView subclasses that need to be redacted from screenshot
var redactClasses: [AnyClass]
private var redactClassesIdentifiers: Set<ObjectIdentifier>

init() {

redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] +
var redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] +
//this classes are used by SwiftUI to display images.
["_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView",
"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView",
Expand All @@ -68,10 +69,35 @@ class UIRedactBuilder {

#if os(iOS)
redactClasses += [ WKWebView.self ]
ignoreClasses = [ UISlider.self, UISwitch.self ]
ignoreClassesIdentifiers = [ ObjectIdentifier(UISlider.self), ObjectIdentifier(UISwitch.self) ]
#else
ignoreClasses = []
ignoreClassesIdentifiers = []
#endif
redactClassesIdentifiers = Set(redactClasses.map( { ObjectIdentifier($0) }))
}

func containsIgnoreClass(_ ignoreClass: AnyClass) -> Bool {
return ignoreClassesIdentifiers.contains(ObjectIdentifier(ignoreClass))
}

func containsRedactClass(_ redactClass: AnyClass) -> Bool {
return redactClassesIdentifiers.contains(ObjectIdentifier(redactClass))
}

func addIgnoreClass(_ ignoreClass: AnyClass) {
ignoreClassesIdentifiers.insert(ObjectIdentifier(ignoreClass))
}

func addRedactClass(_ redactClass: AnyClass) {
redactClassesIdentifiers.insert(ObjectIdentifier(redactClass))
}

func addIgnoreClasses(_ ignoreClasses: [AnyClass]) {
ignoreClasses.forEach(addIgnoreClass(_:))
}

func addRedactClasses(_ redactClasses: [AnyClass]) {
redactClasses.forEach(addRedactClass(_:))
}

func redactRegionsFor(view: UIView, options: SentryRedactOptions?) -> [RedactRegion] {
Expand All @@ -86,16 +112,19 @@ class UIRedactBuilder {

return redactingRegions
}

private func shouldIgnore(view: UIView) -> Bool {
ignoreClasses.contains { view.isKind(of: $0) }
return SentryRedactViewHelper.shouldIgnoreView(view) || containsIgnoreClass(type(of: view))
}

private func shouldRedact(view: UIView, redactText: Bool, redactImage: Bool) -> Bool {
if SentryRedactViewHelper.shouldRedactView(view) {
return true
}
if redactImage, let imageView = view as? UIImageView {
return shouldRedact(imageView: imageView)
}
return redactText && redactClasses.contains { view.isKind(of: $0) }
return redactText && containsRedactClass(type(of: view))
}

private func shouldRedact(imageView: UIImageView) -> Bool {
Expand Down Expand Up @@ -140,5 +169,27 @@ class UIRedactBuilder {
}
}

@objcMembers
class SentryRedactViewHelper: NSObject {
private static var associatedRedactObjectHandle: UInt8 = 0
private static var associatedIgnoreObjectHandle: UInt8 = 0

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

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

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

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

#endif
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
return try XCTUnwrap(SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration)
}

private func startSDK(sessionSampleRate: Float, errorSampleRate: Float, enableSwizzling: Bool = true) {
private func startSDK(sessionSampleRate: Float, errorSampleRate: Float, enableSwizzling: Bool = true, configure: ((Options) -> Void)? = nil) {
SentrySDK.start {
$0.dsn = "https://[email protected]/test"
$0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: sessionSampleRate, onErrorSampleRate: errorSampleRate)
$0.setIntegrations([SentrySessionReplayIntegration.self])
$0.enableSwizzling = enableSwizzling
$0.cacheDirectoryPath = FileManager.default.temporaryDirectory.path
configure?($0)
}
SentrySDK.currentHub().startSession()
}
Expand Down Expand Up @@ -272,6 +273,30 @@ class SentrySessionReplayIntegrationTests: XCTestCase {
XCTAssertEqual(hub.capturedReplayRecordingVideo.count, 0)
}

func testMaskViewFromSDK() {
class AnotherLabel: UILabel {
}

startSDK(sessionSampleRate: 1, errorSampleRate: 1) { options in
options.experimental.sessionReplay.redactViewTypes = [AnotherLabel.self]
}

let redactBuilder = SentryViewPhotographer.shared.getRedactBuild()
XCTAssertTrue(redactBuilder.containsRedactClass(AnotherLabel.self))
}

func testIgnoreViewFromSDK() {
class AnotherLabel: UILabel {
}

startSDK(sessionSampleRate: 1, errorSampleRate: 1) { options in
options.experimental.sessionReplay.ignoreRedactViewTypes = [AnotherLabel.self]
}

let redactBuilder = SentryViewPhotographer.shared.getRedactBuild()
XCTAssertTrue(redactBuilder.containsIgnoreClass(AnotherLabel.self))
}

func createLastSessionReplay(writeSessionInfo: Bool = true, errorSampleRate: Double = 1) throws {
let replayFolder = SentryDependencyContainer.sharedInstance().fileManager.sentryPath + "/replay"
let jsonPath = replayFolder + "/lastreplay"
Expand Down
Loading

0 comments on commit e35cf54

Please sign in to comment.