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

ref: Changing the redact logic #4327

Merged
merged 55 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c57613f
wip
brustolin Aug 29, 2024
8459cc9
test in progress
brustolin Sep 2, 2024
364cb77
fix: Session Replay redact for transformed views
brustolin Sep 3, 2024
0bcb7df
Update CHANGELOG.md
brustolin Sep 3, 2024
15f94cd
ref
brustolin Sep 3, 2024
c1fa693
Update SentryViewPhotographerTests.swift
brustolin Sep 3, 2024
ffdf298
Update Main.storyboard
brustolin Sep 3, 2024
10ca7fd
Apply suggestions from code review
brustolin Sep 5, 2024
594d8a6
Update Sources/Swift/Tools/UIRedactBuilder.swift
brustolin Sep 5, 2024
cfb1ec7
ref
brustolin Sep 5, 2024
4cc5ba8
Merge branch 'fix/redact-under-translucent' of https://github.com/get…
brustolin Sep 5, 2024
2fae00a
ref
brustolin Sep 6, 2024
b1d9eec
ref
brustolin Sep 6, 2024
adc9c57
Merge branch 'main' into fix/redact-under-translucent
brustolin Sep 6, 2024
e0f6d24
ref
brustolin Sep 9, 2024
b78b307
fix: Dont redact clipped views
brustolin Sep 11, 2024
fa35276
Update CHANGELOG.md
brustolin Sep 11, 2024
5046131
Update CHANGELOG.md
brustolin Sep 11, 2024
1b5790f
Merge branch 'main' into fix/redact-under-translucent
brustolin Sep 11, 2024
31c1cdc
Merge branch 'fix/redact-under-translucent' into fix/out-of-clip-redact
brustolin Sep 11, 2024
ee68606
Update UIRedactBuilder.swift
brustolin Sep 11, 2024
293027a
feat: Redact logic
brustolin Sep 11, 2024
0b3d916
Update UIRedactBuilder.swift
brustolin Sep 12, 2024
30077ce
Merge branch 'main' into fix/redact-under-translucent
brustolin Sep 12, 2024
35bb1e7
Update SRRedactSampleViewController.swift
brustolin Sep 12, 2024
32774d5
Merge branch 'fix/redact-under-translucent' into fix/redact-logic
brustolin Sep 12, 2024
993390e
Format code
getsentry-bot Sep 12, 2024
33921f0
Update CHANGELOG.md
brustolin Sep 12, 2024
25dc3f8
Merge branch 'fix/redact-logic' of https://github.com/getsentry/sentr…
brustolin Sep 12, 2024
fb56b85
Merge branch 'fix/redact-under-translucent' into fix/out-of-clip-redact
brustolin Sep 12, 2024
6c69214
Merge branch 'fix/out-of-clip-redact' into fix/redact-logic
brustolin Sep 12, 2024
988420b
fix tests
brustolin Sep 12, 2024
878ff3b
some ref
brustolin Sep 12, 2024
19eae17
Format code
getsentry-bot Sep 12, 2024
73d292c
Update UIRedactBuilder.swift
brustolin Sep 12, 2024
87263c7
Merge branch 'fix/redact-logic' of https://github.com/getsentry/sentr…
brustolin Sep 12, 2024
c1ea803
Update UIRedactBuilder.swift
brustolin Sep 12, 2024
5ddbb30
Update UIRedactBuilder.swift
brustolin Sep 12, 2024
895a25e
redact as enforced
brustolin Sep 13, 2024
cf60b5e
fix: Don't redact clipped views (#4325)
brustolin Sep 13, 2024
7878f04
Correct redact UIView with higher zPosition (#4309)
brustolin Sep 13, 2024
48489b5
Merge branch 'main' into fix/redact-under-translucent
brustolin Sep 13, 2024
4d7ab08
Update UIRedactBuilder.swift
brustolin Sep 13, 2024
3bc34ab
Update UIRedactBuilder.swift
brustolin Sep 13, 2024
bd2b4ee
renaming
brustolin Sep 16, 2024
291bbaf
Merge branch 'fix/redact-under-translucent' into fix/redact-logic
brustolin Sep 16, 2024
a8a39c2
Merge branch 'main' into fix/redact-logic
brustolin Sep 17, 2024
d98a655
Format code
getsentry-bot Sep 17, 2024
57a9d0c
Update UIRedactBuilder.swift
brustolin Sep 17, 2024
f634c33
Merge branch 'fix/redact-logic' of https://github.com/getsentry/sentr…
brustolin Sep 17, 2024
2118ebf
Revert "Merge branch 'fix/redact-logic' of https://github.com/getsent…
brustolin Sep 17, 2024
c76c9e0
Update CHANGELOG.md
brustolin Sep 17, 2024
4e42327
Format code
getsentry-bot Sep 17, 2024
677b7f4
Merge branch 'main' into fix/redact-logic
brustolin Sep 17, 2024
d9bce05
Apply suggestions from code review
brustolin Sep 18, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- Don't redact clipped views (#4325)
- Session replay for crash not created because of a race condition (#4314)
- Double-quoted include, expected angle-bracketed instead (#4298)
- Stop using `redactAllText` as an indicator tha redact is enabled (#4327)

### Improvements

Expand Down
7 changes: 3 additions & 4 deletions Sources/Sentry/PrivateSentrySDKOnly.mm
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#import "SentryOptions.h"
#import "SentrySDK+Private.h"
#import "SentrySerialization.h"
#import "SentrySessionReplayIntegration.h"
#import "SentrySessionReplayIntegration+Private.h"
#import "SentrySwift.h"
#import "SentryThreadHandle.hpp"
#import "SentryUser+Private.h"
Expand All @@ -21,7 +21,6 @@
#import <SentryFramesTracker.h>
#import <SentryScope+Private.h>
#import <SentryScreenshot.h>
#import <SentrySessionReplayIntegration.h>
#import <SentryUser.h>

#if SENTRY_TARGET_PROFILING_SUPPORTED
Expand Down Expand Up @@ -351,12 +350,12 @@ + (NSString *__nullable)getReplayId

+ (void)addReplayIgnoreClasses:(NSArray<Class> *_Nonnull)classes
{
[SentryViewPhotographer.shared addIgnoreClasses:classes];
[[PrivateSentrySDKOnly getReplayIntegration].viewPhotographer addIgnoreClasses:classes];
}

+ (void)addReplayRedactClasses:(NSArray<Class> *_Nonnull)classes
{
[SentryViewPhotographer.shared addRedactClasses:classes];
[[PrivateSentrySDKOnly getReplayIntegration].viewPhotographer addRedactClasses:classes];
}
#endif

Expand Down
6 changes: 3 additions & 3 deletions Sources/Sentry/SentrySessionReplayIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
}

_replayOptions = options.experimental.sessionReplay;
_viewPhotographer =
[[SentryViewPhotographer alloc] initWithRedactOptions:options.experimental.sessionReplay];

Check warning on line 66 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L65-L66

Added lines #L65 - L66 were not covered by tests

if (options.enableSwizzling) {
_touchTracker = [[SentryTouchTracker alloc]
Expand All @@ -85,8 +87,6 @@
}];

[SentryDependencyContainer.sharedInstance.reachability addObserver:self];
[SentryViewPhotographer.shared addIgnoreClasses:_replayOptions.ignoreRedactViewTypes];
[SentryViewPhotographer.shared addRedactClasses:_replayOptions.redactViewTypes];

_installedInstance = self;
return YES;
Expand Down Expand Up @@ -235,7 +235,7 @@
fullSession:(BOOL)shouldReplayFullSession
{
[self startWithOptions:replayOptions
screenshotProvider:SentryViewPhotographer.shared
screenshotProvider:_viewPhotographer

Check warning on line 238 in Sources/Sentry/SentrySessionReplayIntegration.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentrySessionReplayIntegration.m#L238

Added line #L238 was not covered by tests
breadcrumbConverter:[[SentrySRDefaultBreadcrumbConverter alloc] init]
fullSession:shouldReplayFullSession];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
#if SENTRY_TARGET_REPLAY_SUPPORTED

@class SentrySessionReplay;
@class SentryViewPhotographer;

@interface SentrySessionReplayIntegration () <SentryIntegrationProtocol, SentrySessionListener,
SentrySessionReplayDelegate>

@property (nonatomic, strong) SentrySessionReplay *sessionReplay;

@property (nonatomic, strong) SentryViewPhotographer *viewPhotographer;

@end

#endif
4 changes: 2 additions & 2 deletions Sources/Swift/Extensions/UIViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public extension UIView {

/**
* Marks this view to be redacted during replays.
* - warning: This is an experimental feature and may still have bugs.
* - experiment: This is an experimental feature and may still have bugs.
*/
func sentryReplayRedact() {
SentryRedactViewHelper.redactView(self)
Expand All @@ -16,7 +16,7 @@ public extension UIView {
/**
* Marks this view to be ignored during redact step
* of session replay. All its content will be visible in the replay.
* - warning: This is an experimental feature and may still have bugs.
* - experiment: This is an experimental feature and may still have bugs.
*/
func sentryReplayIgnore() {
SentryRedactViewHelper.ignoreView(self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,18 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
/**
* A list of custom UIView subclasses that need
* to be masked during session replay.
* By default Sentry already mask text elements from UIKit
* By default Sentry already mask text and image elements from UIKit
* Every child of a view that is redacted will also be redacted.
*/
public var redactViewTypes = [AnyClass]()
public var redactViewClasses = [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.
* The views of given classes will not be redacted but their children may be.
* This property has precedence over `redactViewTypes`.
*/
public var ignoreRedactViewTypes = [AnyClass]()
public var ignoreViewClasses = [AnyClass]()

/**
* Defines the quality of the session replay.
Expand Down
2 changes: 2 additions & 0 deletions Sources/Swift/Protocol/SentryRedactOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ import Foundation
protocol SentryRedactOptions {
var redactAllText: Bool { get }
var redactAllImages: Bool { get }
var redactViewClasses: [AnyClass] { get }
var ignoreViewClasses: [AnyClass] { get }
}
13 changes: 7 additions & 6 deletions Sources/Swift/Tools/SentryViewPhotographer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,26 @@

@objcMembers
class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider {
static let shared = SentryViewPhotographer()
private let redactBuilder = UIRedactBuilder()
private let redactBuilder: UIRedactBuilder
private let dispatchQueue = SentryDispatchQueueWrapper()

var renderer: ViewRenderer

init(renderer: ViewRenderer) {
init(renderer: ViewRenderer, redactOptions: SentryRedactOptions) {
self.renderer = renderer
redactBuilder = UIRedactBuilder(options: redactOptions)
super.init()
}

private convenience override init() {
self.init(renderer: DefaultViewRenderer())
init(redactOptions: SentryRedactOptions) {
self.renderer = DefaultViewRenderer()
self.redactBuilder = UIRedactBuilder(options: redactOptions)

Check warning on line 37 in Sources/Swift/Tools/SentryViewPhotographer.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Tools/SentryViewPhotographer.swift#L36-L37

Added lines #L36 - L37 were not covered by tests
}

func image(view: UIView, options: SentryRedactOptions, onComplete: @escaping ScreenshotCallback ) {
let image = renderer.render(view: view)

let redact = redactBuilder.redactRegionsFor(view: view, options: options)
let redact = redactBuilder.redactRegionsFor(view: view)
let imageSize = view.bounds.size
dispatchQueue.dispatchAsync {
let screenshot = UIGraphicsImageRenderer(size: imageSize, format: .init(for: .init(displayScale: 1))).image { context in
Expand Down
81 changes: 58 additions & 23 deletions Sources/Swift/Tools/UIRedactBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,58 @@
private var ignoreClassesIdentifiers: Set<ObjectIdentifier>
///This is a list of UIView subclasses that need to be redacted from screenshot
private var redactClassesIdentifiers: Set<ObjectIdentifier>

init() {

var redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] +
//this classes are used by SwiftUI to display images.
["_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView",
"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView",
"SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer", "UIWebView"
].compactMap { NSClassFromString($0) }
/**
Initializes a new instance of the redaction process with the specified options.

This initializer configures which `UIView` subclasses should be redacted from screenshots and which should be ignored during the redaction process.

- parameter options: A `SentryRedactOptions` object that specifies the configuration for the redaction process.

- If `options.redactAllText` is `true`, common text-related views such as `UILabel`, `UITextView`, and `UITextField` are redacted.
- If `options.redactAllImages` is `true`, common image-related views such as `UIImageView` and various internal `SwiftUI` image views are redacted.
- The `options.ignoreRedactViewTypes` allows specifying custom view types to be ignored during the redaction process.
- The `options.redactViewTypes` allows specifying additional custom view types to be redacted.

- note: On iOS, views such as `WKWebView` and `UIWebView` are automatically redacted, and controls like `UISlider` and `UISwitch` are ignored.
brustolin marked this conversation as resolved.
Show resolved Hide resolved
*/
init(options: SentryRedactOptions) {
var redactClasses = [AnyClass]()

if options.redactAllText {
redactClasses += [ UILabel.self, UITextView.self, UITextField.self ]
}

if options.redactAllImages {
//this classes are used by SwiftUI to display images.
redactClasses += ["_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView",
"_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView",
"SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer"
].compactMap(NSClassFromString(_:))
brustolin marked this conversation as resolved.
Show resolved Hide resolved

redactClasses.append(UIImageView.self)
}

#if os(iOS)
redactClasses += [ WKWebView.self ]

//If we try to use 'UIWebView.self' it will not compile for macCatalyst, but the class does exists.
brustolin marked this conversation as resolved.
Show resolved Hide resolved
redactClasses += [ "UIWebView" ].compactMap(NSClassFromString(_:))

ignoreClassesIdentifiers = [ ObjectIdentifier(UISlider.self), ObjectIdentifier(UISwitch.self) ]
#else
ignoreClassesIdentifiers = []
#endif

redactClassesIdentifiers = Set(redactClasses.map({ ObjectIdentifier($0) }))

for type in options.ignoreViewClasses {
self.ignoreClassesIdentifiers.insert(ObjectIdentifier(type))

Check warning on line 93 in Sources/Swift/Tools/UIRedactBuilder.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Tools/UIRedactBuilder.swift#L93

Added line #L93 was not covered by tests
}

for type in options.redactViewClasses {
self.redactClassesIdentifiers.insert(ObjectIdentifier(type))

Check warning on line 97 in Sources/Swift/Tools/UIRedactBuilder.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Swift/Tools/UIRedactBuilder.swift#L97

Added line #L97 was not covered by tests
}
}

func containsIgnoreClass(_ ignoreClass: AnyClass) -> Bool {
Expand Down Expand Up @@ -112,13 +147,12 @@

This function returns the redaction regions in reverse order from what was found in the view hierarchy, allowing the processing of regions from top to bottom. This ensures that clip regions are applied first before drawing a redact mask on lower views.
*/
func redactRegionsFor(view: UIView, options: SentryRedactOptions?) -> [RedactRegion] {
func redactRegionsFor(view: UIView) -> [RedactRegion] {
var redactingRegions = [RedactRegion]()

self.mapRedactRegion(fromView: view,
redacting: &redactingRegions,
rootFrame: view.frame,
redactOptions: options ?? SentryReplayOptions(),
transform: CGAffineTransform.identity)

return redactingRegions.reversed()
Expand All @@ -128,14 +162,14 @@
return SentryRedactViewHelper.shouldIgnoreView(view) || containsIgnoreClass(type(of: view))
}

private func shouldRedact(view: UIView, redactOptions: SentryRedactOptions) -> Bool {
private func shouldRedact(view: UIView) -> Bool {
if SentryRedactViewHelper.shouldRedactView(view) {
return true
}
if redactOptions.redactAllImages, let imageView = view as? UIImageView {
if let imageView = view as? UIImageView, containsRedactClass(UIImageView.self) {
return shouldRedact(imageView: imageView)
}
return redactOptions.redactAllText && containsRedactClass(type(of: view))
return containsRedactClass(type(of: view))
}

private func shouldRedact(imageView: UIImageView) -> Bool {
Expand All @@ -145,22 +179,23 @@
return image.imageAsset?.value(forKey: "_containingBundle") == nil
}

private func mapRedactRegion(fromView view: UIView, redacting: inout [RedactRegion], rootFrame: CGRect, redactOptions: SentryRedactOptions, transform: CGAffineTransform) {
guard (redactOptions.redactAllImages || redactOptions.redactAllText) && !view.isHidden && view.alpha != 0 else { return }
private func mapRedactRegion(fromView view: UIView, redacting: inout [RedactRegion], rootFrame: CGRect, transform: CGAffineTransform, forceRedact: Bool = false) {
guard !redactClassesIdentifiers.isEmpty && !view.isHidden && view.alpha != 0 else { return }

let layer = view.layer.presentation() ?? view.layer

let newTransform = concatenateTranform(transform, with: layer)

let ignore = shouldIgnore(view: view)
let redact = shouldRedact(view: view, redactOptions: redactOptions)
let ignore = !forceRedact && shouldIgnore(view: view)
let redact = forceRedact || shouldRedact(view: view)
var enforceRedact = forceRedact

if !ignore && redact {
redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .redact, color: self.color(for: view)))
return
}

if isOpaque(view) {

guard !view.clipsToBounds else { return }
enforceRedact = true
} else if isOpaque(view) {
let finalViewFrame = CGRect(origin: .zero, size: layer.bounds.size).applying(newTransform)
if isAxisAligned(newTransform) && finalViewFrame == rootFrame {
//Because the current view is covering everything we found so far we can clear `redacting` list
Expand All @@ -170,15 +205,15 @@
}
}

guard !ignore else { return }
guard view.subviews.count > 0 else { return }

if view.clipsToBounds {
/// Because the order in which we process the redacted regions is reversed, we add the end of the clip region first.
/// The beginning will be added after all the subviews have been mapped.
redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .clipEnd))
}
for subview in view.subviews.sorted(by: { $0.layer.zPosition < $1.layer.zPosition }) {
mapRedactRegion(fromView: subview, redacting: &redacting, rootFrame: rootFrame, redactOptions: redactOptions, transform: newTransform)
mapRedactRegion(fromView: subview, redacting: &redacting, rootFrame: rootFrame, transform: newTransform, forceRedact: enforceRedact)
}
if view.clipsToBounds {
redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .clipBegin))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,28 +282,30 @@
(sut as? SentryReachabilityObserver)?.connectivityChanged(true, typeDescription: "")
XCTAssertFalse(sut.sessionReplay.isSessionPaused)
}
func testMaskViewFromSDK() {

func testMaskViewFromSDK() throws {
class AnotherLabel: UILabel {
}

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

Check warning on line 291 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L291

Added line #L291 was not covered by tests
}

let redactBuilder = SentryViewPhotographer.shared.getRedactBuild()

let sut = try getSut()
let redactBuilder = sut.viewPhotographer.getRedactBuild()

Check warning on line 295 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L294-L295

Added lines #L294 - L295 were not covered by tests
XCTAssertTrue(redactBuilder.containsRedactClass(AnotherLabel.self))
}

func testIgnoreViewFromSDK() {
func testIgnoreViewFromSDK() throws {
class AnotherLabel: UILabel {
}

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

Check warning on line 304 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L304

Added line #L304 was not covered by tests
}

let redactBuilder = SentryViewPhotographer.shared.getRedactBuild()
let sut = try getSut()
let redactBuilder = sut.viewPhotographer.getRedactBuild()

Check warning on line 308 in Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift#L307-L308

Added lines #L307 - L308 were not covered by tests
XCTAssertTrue(redactBuilder.containsIgnoreClass(AnotherLabel.self))
}

Expand Down
12 changes: 1 addition & 11 deletions Tests/SentryTests/SentryViewPhotographerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,8 @@ class SentryViewPhotographerTests: XCTestCase {
}
}

private class RedactOptions: SentryRedactOptions {
var redactAllText: Bool
var redactAllImages: Bool

init(redactAllText: Bool = true, redactAllImages: Bool = true) {
self.redactAllText = redactAllText
self.redactAllImages = redactAllImages
}
}

func sut() -> SentryViewPhotographer {
return SentryViewPhotographer(renderer: TestViewRenderer())
return SentryViewPhotographer(renderer: TestViewRenderer(), redactOptions: RedactOptions())
}

private func prepare(views: [UIView], options: any SentryRedactOptions = RedactOptions()) -> UIImage? {
Expand Down
Loading
Loading