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

internal(replay): Add IgnoreWrapper class for hybrid SDKs #4522

Merged
merged 5 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
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)
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
}

#if TEST || TESTCI
func getRedactBuild() -> UIRedactBuilder {
redactBuilder
Expand Down
51 changes: 47 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,28 @@ 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 {
if isRedactContainerClass(type(of: view)) {
return false
}

guard let parent = view.superview else { return false }
return isIgnoreContainerClass(type(of: parent))
krystofwoldrich marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading