From 439772c86682d6050723b420119bd97a5e7f7d80 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 12 Nov 2024 11:10:23 +0100 Subject: [PATCH 01/11] wip --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- .../Swift/Tools/SentryViewPhotographer.swift | 38 ++++++++++++++----- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index e6b7977da0..b334cf82dd 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -43,7 +43,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.debug = true if #available(iOS 16.0, *), !args.contains("--disable-session-replay") { - options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true) + options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true) options.experimental.sessionReplay.quality = .high } diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift index d553e4ea17..0a216a4395 100644 --- a/Sources/Swift/Tools/SentryViewPhotographer.swift +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -36,7 +36,7 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { self.renderer = DefaultViewRenderer() self.redactBuilder = UIRedactBuilder(options: redactOptions) } - + func image(view: UIView, options: SentryRedactOptions, onComplete: @escaping ScreenshotCallback ) { let image = renderer.render(view: view) @@ -45,6 +45,9 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { dispatchQueue.dispatchAsync { let screenshot = UIGraphicsImageRenderer(size: imageSize, format: .init(for: .init(displayScale: 1))).image { context in + let clipOutPath = CGMutablePath(rect: CGRect(origin: .zero, size: imageSize), transform: nil) + var clipPaths = [CGPath]() + let imageRect = CGRect(origin: .zero, size: imageSize) context.cgContext.addRect(CGRect(origin: CGPoint.zero, size: imageSize)) context.cgContext.clip(using: .evenOdd) @@ -62,23 +65,27 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { defer { latestRegion = region } guard latestRegion?.canReplace(as: region) != true && imageRect.intersects(path.boundingBoxOfPath) else { continue } - + switch region.type { case .redact, .redactSwiftUI: (region.color ?? UIImageHelper.averageColor(of: context.currentImage, at: rect.applying(region.transform))).setFill() context.cgContext.addPath(path) context.cgContext.fillPath() case .clipOut: - context.cgContext.addRect(context.cgContext.boundingBoxOfClipPath) - context.cgContext.addPath(path) - context.cgContext.clip(using: .evenOdd) + clipOutPath.addPath(path) + self.updateClipping(for: context.cgContext, + clipPaths: clipPaths, + clipOutPath: clipOutPath) case .clipBegin: - context.cgContext.saveGState() - context.cgContext.resetClip() - context.cgContext.addPath(path) - context.cgContext.clip() + clipPaths.append(path) + self.updateClipping(for: context.cgContext, + clipPaths: clipPaths, + clipOutPath: clipOutPath) case .clipEnd: - context.cgContext.restoreGState() + clipPaths.removeLast() + self.updateClipping(for: context.cgContext, + clipPaths: clipPaths, + clipOutPath: clipOutPath) } } } @@ -86,6 +93,17 @@ class SentryViewPhotographer: NSObject, SentryViewScreenshotProvider { } } + private func updateClipping(for context: CGContext, clipPaths: [CGPath], clipOutPath: CGPath) { + context.resetClip() + clipPaths.reversed().forEach { + context.addPath($0) + context.clip() + } + + context.addPath(clipOutPath) + context.clip(using: .evenOdd) + } + @objc(addIgnoreClasses:) func addIgnoreClasses(classes: [AnyClass]) { redactBuilder.addIgnoreClasses(classes) From 2ba3d55ae6dfe232a0b5a8ac07487cb4a0a369ab Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 12 Nov 2024 15:22:06 +0100 Subject: [PATCH 02/11] FIx: Masking not working with transformed view --- .../iOS-Swift/Base.lproj/Main.storyboard | 9 +++++---- .../SRRedactSampleViewController.swift | 4 ++-- Sources/Swift/Tools/UIRedactBuilder.swift | 16 ++++++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index 971b70b2a4..30fe801f7c 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -1194,7 +1194,8 @@ - + + @@ -1279,7 +1280,7 @@ - + diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift index d14416638d..9f662ca154 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/SRRedactSampleViewController.swift @@ -3,6 +3,7 @@ import Foundation class SRRedactSampleViewController: UIViewController { @IBOutlet var notRedactedView: UIView! + @IBOutlet var notRedactedLabel: UILabel! @IBOutlet var label: UILabel! @@ -11,7 +12,6 @@ class SRRedactSampleViewController: UIViewController { notRedactedView.backgroundColor = .green notRedactedView.transform = CGAffineTransform(rotationAngle: 45 * .pi / 180.0) - - SentrySDK.replay.maskView(notRedactedView) + SentrySDK.replay.unmaskView(notRedactedLabel) } } diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index 45635df3a9..c5aa6c7978 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -160,7 +160,8 @@ class UIRedactBuilder { self.mapRedactRegion(fromView: view, relativeTo: nil, redacting: &redactingRegions, - rootFrame: view.frame) + rootFrame: view.frame, + transform: .identity) var swiftUIRedact = [RedactRegion]() var otherRegions = [RedactRegion]() @@ -198,12 +199,12 @@ class UIRedactBuilder { return image.imageAsset?.value(forKey: "_containingBundle") == nil } - private func mapRedactRegion(fromView view: UIView, relativeTo parentLayer: CALayer?, redacting: inout [RedactRegion], rootFrame: CGRect, forceRedact: Bool = false) { + private func mapRedactRegion(fromView view: UIView, relativeTo parentLayer: CALayer?, 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 = getTranform(from: layer, withParent: parentLayer) + let newTransform = concatenateTranform(transform, from: layer, withParent: parentLayer) let ignore = !forceRedact && shouldIgnore(view: view) let swiftUI = SentryRedactViewHelper.shouldRedactSwiftUI(view) @@ -233,7 +234,7 @@ class UIRedactBuilder { 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, relativeTo: layer, redacting: &redacting, rootFrame: rootFrame, forceRedact: enforceRedact) + mapRedactRegion(fromView: subview, relativeTo: layer, redacting: &redacting, rootFrame: rootFrame, transform: newTransform, forceRedact: enforceRedact) } if view.clipsToBounds { redacting.append(RedactRegion(size: layer.bounds.size, transform: newTransform, type: .clipBegin)) @@ -243,12 +244,15 @@ class UIRedactBuilder { /** Gets a transform that represents the layer global position. */ - private func getTranform(from layer: CALayer, withParent parentLayer: CALayer?) -> CGAffineTransform { + private func concatenateTranform(_ transform: CGAffineTransform, from layer: CALayer, withParent parentLayer: CALayer?) -> CGAffineTransform { let size = layer.bounds.size let anchorPoint = CGPoint(x: size.width * layer.anchorPoint.x, y: size.height * layer.anchorPoint.y) let position = parentLayer?.convert(layer.position, to: nil) ?? layer.position - var newTransform = CGAffineTransform(translationX: position.x, y: position.y) + + var newTransform = transform + newTransform.tx = position.x + newTransform.ty = position.y newTransform = CATransform3DGetAffineTransform(layer.transform).concatenating(newTransform) return newTransform.translatedBy(x: -anchorPoint.x, y: -anchorPoint.y) } From b11355d2dce0ecba8b7a3a5c30e2bc7f477e8256 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 12 Nov 2024 15:58:24 +0100 Subject: [PATCH 03/11] Update SentryViewPhotographerTests.swift --- .../SentryViewPhotographerTests.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift index fd85fe559f..10f8f5c4fa 100644 --- a/Tests/SentryTests/SentryViewPhotographerTests.swift +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -144,6 +144,22 @@ class SentryViewPhotographerTests: XCTestCase { assertColor(pixel2, .green) } + func testRedactLabelWithParentTransformed() throws { + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 25)) + label.text = "Test" + let parentView = UIView(frame: CGRect(x: 0, y: 17, width: 50, height: 25)) + parentView.backgroundColor = .green + parentView.transform = CGAffineTransform(rotationAngle: 90 * .pi / 180.0) + parentView.addSubview(label) + + let image = try XCTUnwrap(prepare(views: [parentView] )) + let pixel1 = color(at: CGPoint(x: 10, y: 10), in: image) + assertColor(pixel1, .white) + + let pixel2 = color(at: CGPoint(x: 22, y: 10), in: image) + assertColor(pixel2, .black) + } + func testDontRedactClippedLabel() throws { let label = UILabel(frame: CGRect(x: 0, y: 25, width: 50, height: 25)) label.text = "Test" From 069c0eb7e76479ab4f4123887147397608e5c85a Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 12 Nov 2024 15:58:53 +0100 Subject: [PATCH 04/11] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 561215f887..bb63599569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes - Keep PropagationContext when cloning scope (#4518) +- Session replay transformed view masking () ## 8.40.1 From 34ba7d04b89ca1f47eb5f23f5d430bdf64f48947 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 12 Nov 2024 16:00:44 +0100 Subject: [PATCH 05/11] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb63599569..daa2b47a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ### Fixes - Keep PropagationContext when cloning scope (#4518) -- Session replay transformed view masking () +- Session replay transformed view masking (#4529) ## 8.40.1 From 4a8f0f336dd4fd5eef7625ec91c0485c94c64311 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 12 Nov 2024 16:01:35 +0100 Subject: [PATCH 06/11] Update CHANGELOG.md --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2e4db9a9..ac41e0486d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,13 @@ ## Unreleased -### Fixes - -- Make `Scope.span` fully thread safe (#4519) ### Features - Transactions for crashes (#4504): Finish the transaction bound to the scope when the app crashes. This __experimental__ feature is disabled by default. You can enable it via the option `enablePersistingTracesWhenCrashing`. ### Fixes +- Make `Scope.span` fully thread safe (#4519) - Keep PropagationContext when cloning scope (#4518) - Session replay transformed view masking (#4529) - UIViewController with Xcode 16 in debug (#4523). The Xcode 16 build setting [ENABLE_DEBUG_DYLIB](https://developer.apple.com/documentation/xcode/build-settings-reference#Enable-Debug-Dylib-Support), which is turned on by default only in debug, could lead to missing UIViewController traces. From 4410c8ca5de14f12fb2f21d9c430eb9d490a5b1a Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 12 Nov 2024 15:02:22 +0000 Subject: [PATCH 07/11] Format code --- Sources/Swift/Tools/UIRedactBuilder.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Swift/Tools/UIRedactBuilder.swift b/Sources/Swift/Tools/UIRedactBuilder.swift index cfa844b1e2..d5a3015875 100644 --- a/Sources/Swift/Tools/UIRedactBuilder.swift +++ b/Sources/Swift/Tools/UIRedactBuilder.swift @@ -287,7 +287,6 @@ class UIRedactBuilder { let size = layer.bounds.size let anchorPoint = CGPoint(x: size.width * layer.anchorPoint.x, y: size.height * layer.anchorPoint.y) let position = parentLayer?.convert(layer.position, to: nil) ?? layer.position - var newTransform = transform newTransform.tx = position.x From 37f8cdabb47d29d8e842e080b653870573973ea3 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 13 Nov 2024 12:24:07 +0100 Subject: [PATCH 08/11] tests --- .../SentryViewPhotographerTests.swift | 65 +++++++++++++++++-- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift index 10f8f5c4fa..7de6782a18 100644 --- a/Tests/SentryTests/SentryViewPhotographerTests.swift +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -147,17 +147,28 @@ class SentryViewPhotographerTests: XCTestCase { func testRedactLabelWithParentTransformed() throws { let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 25)) label.text = "Test" - let parentView = UIView(frame: CGRect(x: 0, y: 17, width: 50, height: 25)) + + let parentView = UIView(frame: CGRect(x: 0, y:12.5, width: 50, height: 25)) parentView.backgroundColor = .green - parentView.transform = CGAffineTransform(rotationAngle: 90 * .pi / 180.0) + parentView.transform = CGAffineTransform(rotationAngle: .pi / 2) parentView.addSubview(label) let image = try XCTUnwrap(prepare(views: [parentView] )) - let pixel1 = color(at: CGPoint(x: 10, y: 10), in: image) - assertColor(pixel1, .white) - - let pixel2 = color(at: CGPoint(x: 22, y: 10), in: image) - assertColor(pixel2, .black) + assertColor(.white, in: image, at: [ + CGPoint(x: 2, y: 2), + CGPoint(x: 10, y: 2), + CGPoint(x: 2, y: 47), + CGPoint(x: 10, y: 47), + CGPoint(x: 39, y: 2), + CGPoint(x: 39, y: 47), + ]) + + assertColor(.black, in: image, at: [ + CGPoint(x: 13, y: 2), + CGPoint(x: 35, y: 2), + CGPoint(x: 13, y: 47), + CGPoint(x: 35, y: 47), + ]) } func testDontRedactClippedLabel() throws { @@ -226,6 +237,46 @@ class SentryViewPhotographerTests: XCTestCase { assertColor(pixel1, .green) } + func testNotMaskingLabelInsideclippedViewHiddenByAnOpaqueExternalView() throws { + let topView = UIView(frame: CGRect(x: 25, y: 0, width: 25, height: 25)) + topView.backgroundColor = .green + + + let label1 = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 25)) + label1.text = "Test" + label1.textColor = .black + + let parentView = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 25)) + parentView.addSubview(label1) + parentView.clipsToBounds = true + + let image = try XCTUnwrap(prepare(views: [parentView, topView])) + + assertColor(.green, in: image, at: [ + CGPoint(x: 27, y: 3), + CGPoint(x: 27, y: 22), + CGPoint(x: 35, y: 12), + CGPoint(x: 47, y: 3), + CGPoint(x: 47, y: 22), + ]) + + assertColor(.black, in: image, at: [ + CGPoint(x: 3, y: 3), + CGPoint(x: 3, y: 22), + CGPoint(x: 12, y: 12), + CGPoint(x: 22, y: 3), + CGPoint(x: 22, y: 22), + ]) + } + + private func assertColor(_ color: UIColor, in image: UIImage, at points: [CGPoint]) { + points.forEach { + let pixel = self.color(at: $0, in: image) + assertColor(color, pixel) + } + } + + private func assertColor(_ color1: UIColor, _ color2: UIColor) { let sRGBColor1 = color1.cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil) let sRGBColor2 = color2.cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil) From 772db4d3ad2e0710198b33344e5c70937744da5e Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 13 Nov 2024 11:25:29 +0000 Subject: [PATCH 09/11] Format code --- Tests/SentryTests/SentryViewPhotographerTests.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift index 7de6782a18..10ea794a29 100644 --- a/Tests/SentryTests/SentryViewPhotographerTests.swift +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -148,7 +148,7 @@ class SentryViewPhotographerTests: XCTestCase { let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 25)) label.text = "Test" - let parentView = UIView(frame: CGRect(x: 0, y:12.5, width: 50, height: 25)) + let parentView = UIView(frame: CGRect(x: 0, y: 12.5, width: 50, height: 25)) parentView.backgroundColor = .green parentView.transform = CGAffineTransform(rotationAngle: .pi / 2) parentView.addSubview(label) @@ -160,14 +160,14 @@ class SentryViewPhotographerTests: XCTestCase { CGPoint(x: 2, y: 47), CGPoint(x: 10, y: 47), CGPoint(x: 39, y: 2), - CGPoint(x: 39, y: 47), + CGPoint(x: 39, y: 47) ]) assertColor(.black, in: image, at: [ CGPoint(x: 13, y: 2), CGPoint(x: 35, y: 2), CGPoint(x: 13, y: 47), - CGPoint(x: 35, y: 47), + CGPoint(x: 35, y: 47) ]) } @@ -241,7 +241,6 @@ class SentryViewPhotographerTests: XCTestCase { let topView = UIView(frame: CGRect(x: 25, y: 0, width: 25, height: 25)) topView.backgroundColor = .green - let label1 = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 25)) label1.text = "Test" label1.textColor = .black @@ -257,7 +256,7 @@ class SentryViewPhotographerTests: XCTestCase { CGPoint(x: 27, y: 22), CGPoint(x: 35, y: 12), CGPoint(x: 47, y: 3), - CGPoint(x: 47, y: 22), + CGPoint(x: 47, y: 22) ]) assertColor(.black, in: image, at: [ @@ -265,7 +264,7 @@ class SentryViewPhotographerTests: XCTestCase { CGPoint(x: 3, y: 22), CGPoint(x: 12, y: 12), CGPoint(x: 22, y: 3), - CGPoint(x: 22, y: 22), + CGPoint(x: 22, y: 22) ]) } @@ -275,7 +274,6 @@ class SentryViewPhotographerTests: XCTestCase { assertColor(color, pixel) } } - private func assertColor(_ color1: UIColor, _ color2: UIColor) { let sRGBColor1 = color1.cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil) From 171c2b27402ee60ef4fb8fb0ac92f1b838d99c58 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Wed, 13 Nov 2024 16:34:09 +0100 Subject: [PATCH 10/11] Apply suggestions from code review Co-authored-by: Philipp Hofmann --- Tests/SentryTests/SentryViewPhotographerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SentryTests/SentryViewPhotographerTests.swift b/Tests/SentryTests/SentryViewPhotographerTests.swift index 10ea794a29..af83a45656 100644 --- a/Tests/SentryTests/SentryViewPhotographerTests.swift +++ b/Tests/SentryTests/SentryViewPhotographerTests.swift @@ -237,7 +237,7 @@ class SentryViewPhotographerTests: XCTestCase { assertColor(pixel1, .green) } - func testNotMaskingLabelInsideclippedViewHiddenByAnOpaqueExternalView() throws { + func testNotMaskingLabelInsideClippedViewHiddenByAnOpaqueExternalView() throws { let topView = UIView(frame: CGRect(x: 25, y: 0, width: 25, height: 25)) topView.backgroundColor = .green From 82a64b48265a7b8707e51f2b572f6d0c16ccb798 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Mon, 25 Nov 2024 15:06:56 +0100 Subject: [PATCH 11/11] Update CHANGELOG.md --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d473ba4d9..08c75cf590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Session replay touch tracking race condition (#4548) +- Session replay transformed view masking (#4529) ### Features @@ -24,9 +25,7 @@ ### Fixes -- Make `Scope.span` fully thread safe (#4519) - Keep PropagationContext when cloning scope (#4518) -- Session replay transformed view masking (#4529) - UIViewController with Xcode 16 in debug (#4523). The Xcode 16 build setting [ENABLE_DEBUG_DYLIB](https://developer.apple.com/documentation/xcode/build-settings-reference#Enable-Debug-Dylib-Support), which is turned on by default only in debug, could lead to missing UIViewController traces. - Concurrency crash with Swift 6 (#4512) - Make `Scope.span` fully thread safe (#4519)