Skip to content

Commit

Permalink
internal(replay): Add IgnoreWrapper class for hybrid SDKs (#4522)
Browse files Browse the repository at this point in the history
  • Loading branch information
krystofwoldrich authored Nov 12, 2024
1 parent 3396be1 commit 656fdf1
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 6 deletions.
13 changes: 13 additions & 0 deletions Sources/Sentry/PrivateSentrySDKOnly.mm
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,19 @@ + (void)addReplayRedactClasses:(NSArray<Class> *_Nonnull)classes
{
[[PrivateSentrySDKOnly getReplayIntegration].viewPhotographer addRedactClasses:classes];
}

+ (void)setIgnoreContainerClass:(Class _Nonnull)containerClass
{
[[PrivateSentrySDKOnly getReplayIntegration].viewPhotographer
setIgnoreContainerClass:containerClass];
}

+ (void)setRedactContainerClass:(Class _Nonnull)containerClass
{
[[PrivateSentrySDKOnly getReplayIntegration].viewPhotographer
setRedactContainerClass:containerClass];
}

#endif

@end
2 changes: 2 additions & 0 deletions Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ typedef void (^SentryOnAppStartMeasurementAvailable)(
+ (NSString *__nullable)getReplayId;
+ (void)addReplayIgnoreClasses:(NSArray<Class> *_Nonnull)classes;
+ (void)addReplayRedactClasses:(NSArray<Class> *_Nonnull)classes;
+ (void)setIgnoreContainerClass:(Class _Nonnull)containerClass;
+ (void)setRedactContainerClass:(Class _Nonnull)containerClass;

#endif
+ (nullable NSDictionary<NSString *, id> *)appStartMeasurementWithSpans;
Expand Down
12 changes: 11 additions & 1 deletion Sources/Swift/Tools/SentryViewPhotographer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,17 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider {
func addRedactClasses(classes: [AnyClass]) {
redactBuilder.addRedactClasses(classes)
}


@objc(setIgnoreContainerClass:)
func setIgnoreContainerClass(_ containerClass: AnyClass) {
redactBuilder.setIgnoreContainerClass(containerClass)
}

@objc(setRedactContainerClass:)
func setRedactContainerClass(_ containerClass: AnyClass) {
redactBuilder.setRedactContainerClass(containerClass)
}

#if TEST || TESTCI
func getRedactBuild() -> UIRedactBuilder {
redactBuilder
Expand Down
47 changes: 43 additions & 4 deletions Sources/Swift/Tools/UIRedactBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ struct RedactRegion {
}

class UIRedactBuilder {

///This is a wrapper which marks it's direct children to be ignored
private var ignoreContainerClassIdentifier: ObjectIdentifier?
///This is a wrapper which marks it's direct children to be redacted
private var redactContainerClassIdentifier: ObjectIdentifier?

///This is a list of UIView subclasses that will be ignored during redact process
private var ignoreClassesIdentifiers: Set<ObjectIdentifier>
///This is a list of UIView subclasses that need to be redacted from screenshot
Expand Down Expand Up @@ -135,7 +139,27 @@ class UIRedactBuilder {
func addRedactClasses(_ redactClasses: [AnyClass]) {
redactClasses.forEach(addRedactClass(_:))
}


func setIgnoreContainerClass(_ containerClass: AnyClass) {
ignoreContainerClassIdentifier = ObjectIdentifier(containerClass)
}

func setRedactContainerClass(_ containerClass: AnyClass) {
let id = ObjectIdentifier(containerClass)
redactContainerClassIdentifier = id
redactClassesIdentifiers.insert(id)
}

#if TEST || TESTCI
func isIgnoreContainerClassTestOnly(_ containerClass: AnyClass) -> Bool {
return isIgnoreContainerClass(containerClass)
}

func isRedactContainerClassTestOnly(_ containerClass: AnyClass) -> Bool {
return isRedactContainerClass(containerClass)
}
#endif

/**
This function identifies and returns the regions within a given UIView that need to be redacted, based on the specified redaction options.
Expand Down Expand Up @@ -178,9 +202,24 @@ class UIRedactBuilder {
}

private func shouldIgnore(view: UIView) -> Bool {
return SentryRedactViewHelper.shouldUnmask(view) || containsIgnoreClass(type(of: view))
return SentryRedactViewHelper.shouldUnmask(view) || containsIgnoreClass(type(of: view)) || shouldIgnoreParentContainer(view)
}


private func shouldIgnoreParentContainer(_ view: UIView) -> Bool {
guard !isRedactContainerClass(type(of: view)), let parent = view.superview else { return false }
return isIgnoreContainerClass(type(of: parent))
}

private func isIgnoreContainerClass(_ containerClass: AnyClass) -> Bool {
guard ignoreContainerClassIdentifier != nil else { return false }
return ObjectIdentifier(containerClass) == ignoreContainerClassIdentifier
}

private func isRedactContainerClass(_ containerClass: AnyClass) -> Bool {
guard redactContainerClassIdentifier != nil else { return false }
return ObjectIdentifier(containerClass) == redactContainerClassIdentifier
}

private func shouldRedact(view: UIView) -> Bool {
if SentryRedactViewHelper.shouldMaskView(view) {
return true
Expand Down
37 changes: 37 additions & 0 deletions Tests/SentryTests/PrivateSentrySDKOnlyTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@testable import Sentry
import SentryTestUtils
import XCTest

Expand Down Expand Up @@ -369,6 +370,42 @@ class PrivateSentrySDKOnlyTests: XCTestCase {
PrivateSentrySDKOnly.addReplayRedactClasses([UILabel.self])
}

func testAddIgnoreContainer() throws {
class IgnoreContainer: UIView {}

SentrySDK.start {
$0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1)
$0.setIntegrations([SentrySessionReplayIntegration.self])
}

PrivateSentrySDKOnly.setIgnoreContainerClass(IgnoreContainer.self)

let replayIntegration = try getFirstIntegrationAsReplay()

let redactBuilder = replayIntegration.viewPhotographer.getRedactBuild()
XCTAssertTrue(redactBuilder.isIgnoreContainerClassTestOnly(IgnoreContainer.self))
}

func testAddRedactContainer() throws {
class RedactContainer: UIView {}

SentrySDK.start {
$0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1)
$0.setIntegrations([SentrySessionReplayIntegration.self])
}

PrivateSentrySDKOnly.setRedactContainerClass(RedactContainer.self)

let replayIntegration = try getFirstIntegrationAsReplay()

let redactBuilder = replayIntegration.viewPhotographer.getRedactBuild()
XCTAssertTrue(redactBuilder.isRedactContainerClassTestOnly(RedactContainer.self))
}

private func getFirstIntegrationAsReplay() throws -> SentrySessionReplayIntegration {
return try XCTUnwrap(SentrySDK.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration)
}

let VALID_REPLAY_ID = "0eac7ab503354dd5819b03e263627a29"

private class TestSentrySessionReplayIntegration: SentrySessionReplayIntegration {
Expand Down
108 changes: 107 additions & 1 deletion Tests/SentryTests/UIRedactBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,113 @@ class UIRedactBuilderTests: XCTestCase {
let result = sut.redactRegionsFor(view: rootView)
XCTAssertEqual(result.count, 1)
}


func testIgnoreContainerChildView() {
class IgnoreContainer: UIView {}
class AnotherLabel: UILabel {}

let sut = getSut()
sut.setIgnoreContainerClass(IgnoreContainer.self)

let ignoreContainer = IgnoreContainer(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
let wrappedLabel = AnotherLabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
ignoreContainer.addSubview(wrappedLabel)
rootView.addSubview(ignoreContainer)

let result = sut.redactRegionsFor(view: rootView)
XCTAssertEqual(result.count, 0)
}

func testIgnoreContainerDirectChildView() {
class IgnoreContainer: UIView {}
class AnotherLabel: UILabel {}

let sut = getSut()
sut.setIgnoreContainerClass(IgnoreContainer.self)

let ignoreContainer = IgnoreContainer(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
let wrappedLabel = AnotherLabel(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
let redactedLabel = AnotherLabel(frame: CGRect(x: 10, y: 10, width: 10, height: 10))
wrappedLabel.addSubview(redactedLabel)
ignoreContainer.addSubview(wrappedLabel)
rootView.addSubview(ignoreContainer)

let result = sut.redactRegionsFor(view: rootView)
XCTAssertEqual(result.count, 1)
}

func testRedactIgnoreContainerAsChildOfMaskedView() {
class IgnoreContainer: UIView {}

let sut = getSut()
sut.setIgnoreContainerClass(IgnoreContainer.self)

let redactedLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
let ignoreContainer = IgnoreContainer(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
let redactedChildLabel = UILabel(frame: CGRect(x: 10, y: 10, width: 10, height: 10))
ignoreContainer.addSubview(redactedChildLabel)
redactedLabel.addSubview(ignoreContainer)
rootView.addSubview(redactedLabel)

let result = sut.redactRegionsFor(view: rootView)
XCTAssertEqual(result.count, 3)
}

func testRedactChildrenOfRedactContainer() {
class RedactContainer: UIView {}
class AnotherView: UIView {}

let sut = getSut()
sut.setRedactContainerClass(RedactContainer.self)

let redactContainer = RedactContainer(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
let redactedView = AnotherView(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
let redactedView2 = AnotherView(frame: CGRect(x: 10, y: 10, width: 10, height: 10))
redactedView.addSubview(redactedView2)
redactContainer.addSubview(redactedView)
rootView.addSubview(redactContainer)

let result = sut.redactRegionsFor(view: rootView)
XCTAssertEqual(result.count, 3)
}

func testRedactChildrenOfRedactedView() {
class AnotherView: UIView {}

let sut = getSut()

let redactedLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
let redactedView = AnotherView(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
redactedLabel.addSubview(redactedView)
rootView.addSubview(redactedLabel)

let result = sut.redactRegionsFor(view: rootView)
XCTAssertEqual(result.count, 2)
}

func testRedactContainerHasPriorityOverIgnoreContainer() {
class IgnoreContainer: UIView {}
class RedactContainer: UIView {}
class AnotherView: UIView {}

let sut = getSut()
sut.setRedactContainerClass(RedactContainer.self)

let ignoreContainer = IgnoreContainer(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
let redactContainer = RedactContainer(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
let redactedView = AnotherView(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
let ignoreContainer2 = IgnoreContainer(frame: CGRect(x: 10, y: 10, width: 10, height: 10))
let redactedView2 = AnotherView(frame: CGRect(x: 15, y: 15, width: 5, height: 5))
ignoreContainer2.addSubview(redactedView2)
redactedView.addSubview(ignoreContainer2)
redactContainer.addSubview(redactedView)
ignoreContainer.addSubview(redactContainer)
rootView.addSubview(ignoreContainer)

let result = sut.redactRegionsFor(view: rootView)
XCTAssertEqual(result.count, 4)
}

func testIgnoreView() {
class AnotherLabel: UILabel {
}
Expand Down

0 comments on commit 656fdf1

Please sign in to comment.