Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@

- Fixed a build error in `SentryFeedback.swift` when building with cocoapods on Xcode 14.2 (#5917)

### Fixes

- Add masking for AVPlayerView (#5910)

## 8.54.1-alpha.1

- No documented changes.
Expand Down
75 changes: 50 additions & 25 deletions Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard

Large diffs are not rendered by default.

Binary file added Samples/iOS-Swift/iOS-Swift/Sample.mp4
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import AVKit
import UIKit

/// Video view controller for displaying video using the ``AVKit`` framework.
///
/// See the expo-video video view for reference:
/// https://github.com/expo/expo/blob/sdk-53/packages/expo-video/ios/VideoView.swift
class SentryVideoViewController: UIViewController {
lazy var playerViewController = AVPlayerViewController()

weak var player: AVPlayer? {
didSet {
playerViewController.player = player
}
}

override func viewDidLoad() {
super.viewDidLoad()

setupPlayerUI()
setupPlayer()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// Start playing the video when the view appears.
player?.play()
}

func setupPlayerUI() {
// Use a distinct color to clearly indicate when the video content not being displayed.
playerViewController.view.backgroundColor = .systemOrange

// Disable updates to the Now Playing Info Center, to increase isolation of app to global system state.
playerViewController.updatesNowPlayingInfoCenter = false

// Reference for the correct life cycle calls:
// https://developer.apple.com/documentation/uikit/creating-a-custom-container-view-controller#Add-a-child-view-controller-programmatically-to-your-content
addChild(playerViewController)
view.addSubview(playerViewController.view)

playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
playerViewController.view.topAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.topAnchor),
playerViewController.view.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),

playerViewController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
playerViewController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])

playerViewController.didMove(toParent: self)
}

func setupPlayer() {
guard let videoUrl = Bundle.main.url(forResource: "Sample", withExtension: "mp4") else {
preconditionFailure("Sample video not found in main bundle")
}
let player = AVPlayer(url: videoUrl)
player.isMuted = true
self.player = player
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ final class SentryUIRedactBuilder {

///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
private var redactClassesIdentifiers: Set<ObjectIdentifier>


/// This is a list of UIView subclasses that need to be redacted from screenshot
///
/// This set is configured as `private(set)` to allow modification only from within this class,
/// while still allowing read access from tests.
private(set) var redactClassesIdentifiers: Set<ObjectIdentifier>

/**
Initializes a new instance of the redaction process with the specified options.

Expand Down Expand Up @@ -66,7 +70,10 @@ final class SentryUIRedactBuilder {
// Used by:
// - https://developer.apple.com/documentation/SafariServices/SFSafariViewController
// - https://developer.apple.com/documentation/AuthenticationServices/ASWebAuthenticationSession
"SFSafariView"
"SFSafariView",
// Used by:
// - https://developer.apple.com/documentation/avkit/avplayerviewcontroller
"AVPlayerView"
].compactMap(NSClassFromString(_:))

ignoreClassesIdentifiers = [ ObjectIdentifier(UISlider.self), ObjectIdentifier(UISwitch.self) ]
Expand All @@ -86,7 +93,7 @@ final class SentryUIRedactBuilder {
}

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

func containsRedactClass(_ redactClass: AnyClass) -> Bool {
Expand Down
103 changes: 96 additions & 7 deletions Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#if os(iOS)

Check failure on line 1 in Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests.swift

View workflow job for this annotation

GitHub Actions / JUnit Test Report

SentryUIRedactBuilderTests.testDefaultRedactList_shouldContainAllPlatformSpecificClasses

/Users/runner/work/sentry-cocoa/sentry-cocoa/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests.swift:501 - XCTAssertEqual failed: ("16") is not equal to ("15") - Expected 15 classes but got 16 instead
import AVKit
import Foundation
import PDFKit
import SafariServices
Expand Down Expand Up @@ -461,16 +462,43 @@
XCTAssertEqual(result.count, 0)
}

func testRedactList() {
let expectedList = ["_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView",
func testDefaultRedactList_shouldContainAllPlatformSpecificClasses() {
// -- Arrange --
let expectedListClassNames = [
// SwiftUI Views
"_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView",
"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView",
"SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer", "UIWebView", "SFSafariView", "UILabel", "UITextView", "UITextField", "WKWebView", "PDFView"
].compactMap { NSClassFromString($0) }

"SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer",
// Web Views
"UIWebView", "SFSafariView", "WKWebView",
// Text Views
"UILabel", "UITextView", "UITextField", "RCTParagraphComponentView",
// Document Views
"PDFView",
// Image Views
"UIImageView", "RCTImageView",
// Audio / Video Views
"AVPlayerView"
]
let expectedList = expectedListClassNames.compactMap { className -> (String, ObjectIdentifier?) in
guard let classType = NSClassFromString(className) else {
print("Class \(className) not found, skipping test")
return (className, nil)
}
return (className, ObjectIdentifier(classType))
}

// -- Act --
let sut = getSut()
expectedList.forEach { element in
XCTAssertTrue(sut.containsRedactClass(element), "\(element) not found")

// -- Assert --
for (expectedClassName, expectedNullableIdentifier) in expectedList {
// If mapping a class name to an identifier fails, we expect it not to be in the list of redacted class identifiers as well
if let expectedIdentifier = expectedNullableIdentifier {
XCTAssertTrue(sut.redactClassesIdentifiers.contains(where: { $0 == expectedIdentifier }), "Expected class \(expectedClassName) not found in redact list")
}
}
XCTAssertEqual(sut.redactClassesIdentifiers.count, expectedList.count, "Expected \(expectedList.count) classes but got \(sut.redactClassesIdentifiers.count) instead")

Check failure on line 501 in Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests.swift

View workflow job for this annotation

GitHub Actions / Unit iOS 16 Sentry

testDefaultRedactList_shouldContainAllPlatformSpecificClasses, XCTAssertEqual failed: ("16") is not equal to ("15") - Expected 15 classes but got 16 instead

Check failure on line 501 in Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests.swift

View workflow job for this annotation

GitHub Actions / Unit Catalyst 15 Sentry

testDefaultRedactList_shouldContainAllPlatformSpecificClasses, XCTAssertEqual failed: ("14") is not equal to ("15") - Expected 15 classes but got 14 instead

Check failure on line 501 in Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests.swift

View workflow job for this annotation

GitHub Actions / Unit iOS 17 Sentry

testDefaultRedactList_shouldContainAllPlatformSpecificClasses, XCTAssertEqual failed: ("16") is not equal to ("15") - Expected 15 classes but got 16 instead
}

func testIgnoreList() {
Expand Down Expand Up @@ -638,6 +666,67 @@
// -- Act & Assert --
XCTAssertTrue(sut.containsRedactClass(PDFView.self), "PDFView should be in the redact class list")
}

func testRedactAVPlayerViewController() throws {
// -- Arrange --
let sut = getSut()
let avPlayerViewController = AVPlayerViewController()
let avPlayerView = try XCTUnwrap(avPlayerViewController.view)
avPlayerView.frame = CGRect(x: 20, y: 20, width: 40, height: 40)
rootView.addSubview(avPlayerView)

// -- Act --
let result = sut.redactRegionsFor(view: rootView)

// -- Assert --
// Root View
// └ AVPlayerViewController.view (Public API)
// └ AVPlayerView (Private API)
XCTAssertGreaterThanOrEqual(result.count, 1)
let avPlayerRegion = try XCTUnwrap(result.first)
XCTAssertEqual(avPlayerRegion.size, CGSize(width: 40, height: 40))
XCTAssertEqual(avPlayerRegion.type, .redact)
XCTAssertEqual(avPlayerRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20))
XCTAssertNil(avPlayerRegion.color)
}

func testRedactAVPlayerViewControllerEvenWithMaskingDisabled() throws {
// -- Arrange --
// AVPlayerViewController should always be redacted for security reasons,
// regardless of maskAllText and maskAllImages settings
let sut = getSut(TestRedactOptions(maskAllText: false, maskAllImages: false))
let avPlayerViewController = AVPlayerViewController()
let avPlayerView = try XCTUnwrap(avPlayerViewController.view)
avPlayerView.frame = CGRect(x: 20, y: 20, width: 40, height: 40)
rootView.addSubview(avPlayerView)

// -- Act --
let result = sut.redactRegionsFor(view: rootView)

// -- Assert --
// Root View
// └ AVPlayerViewController.view (Public API)
// └ AVPlayerView (Private API)
XCTAssertGreaterThanOrEqual(result.count, 1)
let avPlayerRegion = try XCTUnwrap(result.first)
XCTAssertEqual(avPlayerRegion.size, CGSize(width: 40, height: 40))
XCTAssertEqual(avPlayerRegion.type, .redact)
XCTAssertEqual(avPlayerRegion.transform, CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 20, ty: 20))
XCTAssertNil(avPlayerRegion.color)
}

func testAVPlayerViewInRedactList() throws {
// -- Arrange --
let sut = getSut()

// -- Act & Assert --
// Note: The redaction system uses "AVPlayerView" as the class name string
// which should resolve to the internal view hierarchy of AVPlayerViewController
guard let avPlayerViewClass = NSClassFromString("AVPlayerView") else {
throw XCTSkip("AVPlayerView class not found, skipping test")
}
XCTAssertTrue(sut.containsRedactClass(avPlayerViewClass), "AVPlayerView should be in the redact class list")
}
}

#endif
Loading