From bbb4de0514091fa427e95a3ad7cc0e4fc25e30b8 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 1 Dec 2025 15:40:38 +0100 Subject: [PATCH 1/6] feat: Add isiOSAppOnVisionOS, isiOSAppOnMac, isMacCatalystApp to device context --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- Sentry.xcodeproj/project.pbxproj | 4 + .../SentryTestUtils-ObjC-BridgingHeader.h | 6 -- .../Sources/TestDisplayLinkWrapper.swift | 8 +- .../TestSentryNSProcessInfoWrapper.swift | 5 + Sources/Swift/Helper/SentryProcessInfo.swift | 14 +++ .../SentryCrash/SentryCrashWrapper.swift | 9 +- Tests/SentryTests/Helper/SentryDeviceTests.m | 18 ++++ .../Helper/SentryProcessInfoTests.swift | 20 ++++ .../SentryANRTrackingIntegrationTests.swift | 2 +- .../SentryTimeToDisplayTrackerTest.swift | 5 +- .../Session/SentrySessionTrackerTests.swift | 22 ++--- .../PrivateSentrySDKOnlyTests.swift | 14 +-- Tests/SentryTests/SentryClientTests.swift | 4 +- .../SentryCrash/SentryCrashWrapperTests.swift | 93 ++++++++++++++++++- Tests/SentryTests/SentryHubTests.swift | 2 +- Tests/SentryTests/SentryOptionsTest.m | 5 +- Tests/SentryTests/SentrySDKTests.swift | 2 +- .../SentryTests/SentryTests-Bridging-Header.h | 6 +- .../SentryTests/TestSentryNSApplication.swift | 2 +- .../Transaction/SentrySpanTests.swift | 10 +- .../Transaction/SentryTracerTests.swift | 2 +- Tests/SentryTests/UIImageHelperTests.swift | 4 +- 23 files changed, 211 insertions(+), 48 deletions(-) create mode 100644 Tests/SentryTests/Helper/SentryProcessInfoTests.swift diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index b685e071d6a..17d87f9778b 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -26,7 +26,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { SampleAppDebugMenu.shared.display() metricKit.receiveReports() - + return true } diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 54332885150..e870aeea473 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -817,6 +817,7 @@ D4D0E1E82E9D040A00358814 /* SentrySessionReplayEnvironmentCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D0E1E22E9D040800358814 /* SentrySessionReplayEnvironmentCheckerTests.swift */; }; D4D12E7A2DFC608800DC45C4 /* SentryScreenshotOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D12E792DFC607F00DC45C4 /* SentryScreenshotOptionsTests.swift */; }; D4DEE6592E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */; }; + D4E1CE6A2EDDCBD900D2EAC1 /* SentryProcessInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E1CE642EDDCBD300D2EAC1 /* SentryProcessInfoTests.swift */; }; D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */; }; D4E3F35E2D4A877300F79E2B /* SentryNSDictionarySanitize+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */; }; D4E9420A2E9D1CFB00DB7521 /* TestSessionReplayEnvironmentChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentChecker.swift */; }; @@ -2183,6 +2184,7 @@ D4D0E1E22E9D040800358814 /* SentrySessionReplayEnvironmentCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayEnvironmentCheckerTests.swift; sourceTree = ""; }; D4D12E792DFC607F00DC45C4 /* SentryScreenshotOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenshotOptionsTests.swift; sourceTree = ""; }; D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProfileTimeseriesTests.m; sourceTree = ""; }; + D4E1CE642EDDCBD300D2EAC1 /* SentryProcessInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryProcessInfoTests.swift; sourceTree = ""; }; D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSessionReplayEnvironmentChecker.swift; sourceTree = ""; }; D4E9420B2E9D1D7600DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSessionReplayEnvironmentCheckerTests.swift; sourceTree = ""; }; D4ECA3FF2E3CBEDE00C757EA /* SentryDummyPrivateEmptyClass.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDummyPrivateEmptyClass.m; sourceTree = ""; }; @@ -3596,6 +3598,7 @@ 7BD7299B24654CD500EA3610 /* Helper */ = { isa = PBXGroup; children = ( + D4E1CE642EDDCBD300D2EAC1 /* SentryProcessInfoTests.swift */, D4A0C22A2E9E3CE100791353 /* InfoPlist */, F4A930242E661856006DA6EF /* SentryMobileProvisionParserTests.swift */, D4F7BD7C2E4373BB004A2D77 /* SentryLevelMapperTests.swift */, @@ -6284,6 +6287,7 @@ D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */, D885266427739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift in Sources */, 7BBD18992449DE9D00427C76 /* TestRateLimits.swift in Sources */, + D4E1CE6A2EDDCBD900D2EAC1 /* SentryProcessInfoTests.swift in Sources */, 7B04A9AB24EA5F8D00E710B1 /* SentryUserTests.swift in Sources */, 7BA61CCF247EB59500C130A8 /* SentryCrashUUIDConversionTests.swift in Sources */, 7BBD188D2448453600427C76 /* SentryHttpDateParserTests.swift in Sources */, diff --git a/SentryTestUtils/Headers/SentryTestUtils-ObjC-BridgingHeader.h b/SentryTestUtils/Headers/SentryTestUtils-ObjC-BridgingHeader.h index cc2903f0a4e..bec6b1eb5b2 100644 --- a/SentryTestUtils/Headers/SentryTestUtils-ObjC-BridgingHeader.h +++ b/SentryTestUtils/Headers/SentryTestUtils-ObjC-BridgingHeader.h @@ -1,11 +1,5 @@ #import "SentryDefines.h" -#if TARGET_OS_IOS || TARGET_OS_TV -# define SENTRY_UIKIT_AVAILABLE 1 -#else -# define SENTRY_UIKIT_AVAILABLE 0 -#endif - #if SENTRY_HAS_UIKIT # import "SentryAppStartTracker.h" # import "SentryDefaultUIViewControllerPerformanceTracker.h" diff --git a/SentryTestUtils/Sources/TestDisplayLinkWrapper.swift b/SentryTestUtils/Sources/TestDisplayLinkWrapper.swift index 54f207e54e4..81f75a33f90 100644 --- a/SentryTestUtils/Sources/TestDisplayLinkWrapper.swift +++ b/SentryTestUtils/Sources/TestDisplayLinkWrapper.swift @@ -1,7 +1,7 @@ import Foundation @_spi(Private) @testable import Sentry -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) public enum GPUFrame { case normal @@ -18,7 +18,7 @@ public enum FrameRate: UInt64 { } } -@_spi(Private) public class TestDisplayLinkWrapper: SentryDisplayLinkWrapper, SentryReplayDisplayLinkWrapper { +@_spi(Private) public class TestDisplayLinkWrapper: SentryDisplayLinkWrapper { public var target: AnyObject! public var selector: Selector! public var currentFrameRate: FrameRate = .low @@ -146,3 +146,7 @@ public enum FrameRate: UInt64 { } #endif + +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +extension TestDisplayLinkWrapper: SentryReplayDisplayLinkWrapper {} +#endif diff --git a/SentryTestUtils/Sources/TestSentryNSProcessInfoWrapper.swift b/SentryTestUtils/Sources/TestSentryNSProcessInfoWrapper.swift index 30d189c779e..c4e411cf49b 100644 --- a/SentryTestUtils/Sources/TestSentryNSProcessInfoWrapper.swift +++ b/SentryTestUtils/Sources/TestSentryNSProcessInfoWrapper.swift @@ -12,6 +12,7 @@ public var environment: [String: String]? public var isiOSAppOnMac: Bool? public var isMacCatalystApp: Bool? + public var isiOSAppOnVisionOS: Bool? } public var overrides = Override() @@ -45,4 +46,8 @@ public var isMacCatalystApp: Bool { return overrides.isMacCatalystApp ?? ProcessInfo.processInfo.isMacCatalystApp } + + public var isiOSAppOnVisionOS: Bool { + return overrides.isiOSAppOnVisionOS ?? ProcessInfo.processInfo.isiOSAppOnVisionOS + } } diff --git a/Sources/Swift/Helper/SentryProcessInfo.swift b/Sources/Swift/Helper/SentryProcessInfo.swift index 180b0b8f4f9..db48e1c882b 100644 --- a/Sources/Swift/Helper/SentryProcessInfo.swift +++ b/Sources/Swift/Helper/SentryProcessInfo.swift @@ -10,6 +10,8 @@ @available(macOS 12.0, *) var isMacCatalystApp: Bool { get } + + var isiOSAppOnVisionOS: Bool { get } } // This is needed because a file that only contains an @objc extension will get automatically stripped out @@ -26,4 +28,16 @@ public var processPath: String? { Bundle.main.executablePath } + + public var isiOSAppOnVisionOS: Bool { + if #available(iOS 26.1, visionOS 26.1, *) { + // Use official API when available + // https://developer.apple.com/documentation/foundation/processinfo/isiosapponvision + return self.isiOSAppOnVision + } else { + // Fallback for older versions: `UIWindowSceneGeometryPreferencesVision` is only available on visionOS + // https://developer.apple.com/documentation/uikit/uiwindowscene/geometrypreferences/vision?language=objc + return NSClassFromString("UIWindowSceneGeometryPreferencesVision") != nil + } + } } diff --git a/Sources/Swift/SentryCrash/SentryCrashWrapper.swift b/Sources/Swift/SentryCrash/SentryCrashWrapper.swift index 3f02c126d67..d09300732b7 100644 --- a/Sources/Swift/SentryCrash/SentryCrashWrapper.swift +++ b/Sources/Swift/SentryCrash/SentryCrashWrapper.swift @@ -170,6 +170,13 @@ public final class SentryCrashWrapper: NSObject { deviceData["locale"] = Locale.autoupdatingCurrent.identifier + if #available(macOS 12, *) { + deviceData["ios_app_on_macos"] = self.processInfoWrapper.isiOSAppOnMac + deviceData["mac_catalyst_app"] = self.processInfoWrapper.isMacCatalystApp + } + + deviceData["ios_app_on_visionos"] = self.processInfoWrapper.isiOSAppOnVisionOS + // Set screen dimensions if available setScreenDimensions(&deviceData) @@ -210,7 +217,7 @@ public final class SentryCrashWrapper: NSObject { if self.processInfoWrapper.isMacCatalystApp { runtimeContext["name"] = "Mac Catalyst App" - runtimeContext["raw_description"] = "raw_description" + runtimeContext["raw_description"] = "mac-catalyst-app" } } diff --git a/Tests/SentryTests/Helper/SentryDeviceTests.m b/Tests/SentryTests/Helper/SentryDeviceTests.m index e71c1528f29..cd69d2ad2f7 100644 --- a/Tests/SentryTests/Helper/SentryDeviceTests.m +++ b/Tests/SentryTests/Helper/SentryDeviceTests.m @@ -79,6 +79,18 @@ - (void)testCPUArchitecture // TODO: create a watch UI test target to test this branch as it cannot run on the watch // simulator SENTRY_ASSERT_CONTAINS(arch, @"arm"); // Real Watches +#elif TARGET_OS_VISION +# if TARGET_OS_SIMULATOR +# if TARGET_CPU_ARM64 + SENTRY_ASSERT_CONTAINS(arch, @"arm"); // Vision Pro simulator on M1 macs +# elif TARGET_CPU_X86_64 + SENTRY_ASSERT_CONTAINS(arch, @"x86"); // Vision Pro simulator on Intel macs +# else + XCTFail(@"Unexpected CPU type on test host."); +# endif // TARGET_CPU_ARM64 +# else + SENTRY_ASSERT_CONTAINS(arch, @"arm"); // Real Vision Pro devices +# endif #else XCTFail(@"Unexpected device OS"); #endif @@ -127,6 +139,8 @@ - (void)testOSName #elif TARGET_OS_WATCH // TODO: create a watch UI test target to test this branch SENTRY_ASSERT_EQUAL(osName, @"watchOS"); +#elif TARGET_OS_VISION + SENTRY_ASSERT_EQUAL(osName, @"visionOS"); #else XCTFail(@"Unexpected device OS"); #endif @@ -161,6 +175,8 @@ - (void)testDeviceModel // TODO: create a watch UI test target to test this branch as it cannot run on the watch // simulator SENTRY_ASSERT_CONTAINS(modelName, @"Watch"); +#elif TARGET_OS_VISION + SENTRY_ASSERT_CONTAINS(modelName, @"RealityDevice"); #else XCTFail(@"Unexpected target OS"); #endif @@ -205,6 +221,8 @@ - (void)testSimulatedDeviceModel // TODO: create a watch UI test target to test this branch as it cannot run on the watch // simulator SENTRY_ASSERT_CONTAINS(modelName, @"Watch"); +# elif TARGET_OS_VISION + SENTRY_ASSERT_CONTAINS(modelName, @"RealityDevice"); # else XCTFail(@"Unexpected device OS"); # endif diff --git a/Tests/SentryTests/Helper/SentryProcessInfoTests.swift b/Tests/SentryTests/Helper/SentryProcessInfoTests.swift new file mode 100644 index 00000000000..8dc580ee0ab --- /dev/null +++ b/Tests/SentryTests/Helper/SentryProcessInfoTests.swift @@ -0,0 +1,20 @@ +@_spi(Private) @testable import Sentry +import XCTest + +final class SentryProcessInfoTests: XCTestCase { + + func testIsiOSAppOnVisionOS() throws { + // -- Arrange -- + let processInfo = ProcessInfo.processInfo + + // -- Act -- + let result = processInfo.isiOSAppOnVisionOS + + // -- Assert -- + // This test only asserts that the property exists, as we can not adapt the process info in tests + // and a test running on visionOS is also not an iOS app. + // + // We asserted this manually by running iOS-Swift on visionOS, then exploring the data in `lldb` + XCTAssertFalse(result, "isiOSAppOnVisionOS should be false when not running on visionOS") + } +} diff --git a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift index 5726042d5ba..a962d73061f 100644 --- a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift @@ -78,7 +78,7 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase { let tracker = try XCTUnwrap(Dynamic(sut).tracker.asAnyObject as? SentryANRTracker) -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) XCTAssertTrue(tracker.helper is SentryANRTrackerV2, "Expected SentryANRTrackerV2, but got \(type(of: tracker))") #else XCTAssertTrue(tracker.helper is SentryANRTrackerV1, "Expected SentryANRTrackerV1 on macOS, but got \(type(of: tracker))") diff --git a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift index 81a9cc49ee8..61f74fd8314 100644 --- a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift +++ b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift @@ -3,10 +3,9 @@ import Foundation @_spi(Private) import SentryTestUtils import XCTest -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) - class TestDelayedWrapper: SentryDelayedFramesTracker {} +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) class SentryTimeToDisplayTrackerTest: XCTestCase { private class Fixture { @@ -323,7 +322,7 @@ class SentryTimeToDisplayTrackerTest: XCTestCase { let expectedInvocations = invocationsBefore + 1 XCTAssertEqual(dispatchQueueWrapper.blockOnMainInvocations.count, expectedInvocations, "reportFullyDisplayed should be dispatched on the main queue. ") } - + func testNotWaitingForFullyDisplayed_AfterTracerTimesOut() throws { fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 9)) diff --git a/Tests/SentryTests/Integrations/Session/SentrySessionTrackerTests.swift b/Tests/SentryTests/Integrations/Session/SentrySessionTrackerTests.swift index c0078e64d21..0b64d5c8ee8 100644 --- a/Tests/SentryTests/Integrations/Session/SentrySessionTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Session/SentrySessionTrackerTests.swift @@ -13,7 +13,7 @@ class SentrySessionTrackerTests: XCTestCase { let client: TestClient! let sentryCrash: TestSentryCrashWrapper - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT let _application: TestSentryUIApplication #else let _application: TestSentryNSApplication @@ -38,7 +38,7 @@ class SentrySessionTrackerTests: XCTestCase { sentryCrash = TestSentryCrashWrapper(processInfoWrapper: ProcessInfo.processInfo) - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT _application = TestSentryUIApplication() _application.unsafeApplicationState = .inactive #else @@ -537,7 +537,7 @@ class SentrySessionTrackerTests: XCTestCase { // MARK: - Helpers private func startSutInAppDelegate() { - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT // The Sentry SDK should be initialized in the `UIAppDelegate.didFinishLaunchingWithOptions` // At this point the application state is `inactive`, because the app just launched but did not // become the active app yet. @@ -565,7 +565,7 @@ class SentrySessionTrackerTests: XCTestCase { // We remove the observers, to ensure future notifications are not handled. sut.removeObservers() - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT // When the app stops, the app state is `inactive`. // This can be observed by viewing the application state in `UIAppDelegate.applicationDidEnterBackground`. fixture._application.unsafeApplicationState = .inactive @@ -580,7 +580,7 @@ class SentrySessionTrackerTests: XCTestCase { private func crashSut() { sut.removeObservers() - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT // When the app stops, the app state is `inactive`. // // This can be observed by viewing the application state in `UIAppDelegate.applicationDidEnterBackground`. @@ -600,7 +600,7 @@ class SentrySessionTrackerTests: XCTestCase { } private func goToForeground() { - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT // When the app becomes active, the app state is `active`. // This can be observed by viewing the application state in `UIAppDelegate.applicationDidBecomeActive`. fixture._application.unsafeApplicationState = .active @@ -622,7 +622,7 @@ class SentrySessionTrackerTests: XCTestCase { private func goToBackground() { // Before an app goes to background, it is still active and will resign from being active. willResignActive() - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT // It is expected that the app state is background when the didEnterBackground is called fixture._application.unsafeApplicationState = .background fixture.notificationCenter @@ -642,7 +642,7 @@ class SentrySessionTrackerTests: XCTestCase { } private func willResignActive() { - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT // When the app is about to resign being active, it is still active. // // This can be observed by viewing the application state in `UIAppDelegate.applicationWillResignActive`. @@ -665,7 +665,7 @@ class SentrySessionTrackerTests: XCTestCase { private func hybridSdkDidBecomeActive() { // When an app did become active, it is in the active state. - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT fixture._application.unsafeApplicationState = .active #else fixture._application.setIsActive(true) @@ -687,7 +687,7 @@ class SentrySessionTrackerTests: XCTestCase { } private func willTerminate() { - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT // When terminating an app, it will first move to the background and then terminate. // // This can be observed by viewing the application state in `UIAppDelegate.applicationWillTerminate`. @@ -876,7 +876,7 @@ class SentrySessionTrackerTests: XCTestCase { private func postHybridSdkDidBecomeActiveNotification() { // When the hybrid SDK posts this notification, the app should be in active state - #if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) + #if (os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT fixture._application.unsafeApplicationState = .active #else fixture._application.setIsActive(true) diff --git a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift index 3ea98fa28eb..5e7698b3040 100644 --- a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift +++ b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift @@ -126,7 +126,7 @@ class PrivateSentrySDKOnlyTests: XCTestCase { XCTAssertNil(PrivateSentrySDKOnly.envelope(with: itemData)) } - #if canImport(UIKit) + #if canImport(UIKit) && !os(visionOS) func testGetAppStartMeasurement() { let appStartMeasurement = TestData.getAppStartMeasurement(type: .warm, runtimeInitSystemTimestamp: 1) SentrySDKInternal.setAppStartMeasurement(appStartMeasurement) @@ -240,7 +240,7 @@ class PrivateSentrySDKOnlyTests: XCTestCase { XCTAssertEqual(PrivateSentrySDKOnly.options.enabled, true) } - #if !os(tvOS) && !os(watchOS) + #if !os(tvOS) && !os(watchOS) && !os(visionOS) /** * Smoke Tests profiling via PrivateSentrySDKOnly. Actual profiling unit tests are done elsewhere. */ @@ -349,6 +349,7 @@ class PrivateSentrySDKOnlyTests: XCTestCase { #endif #if canImport(UIKit) + #if SENTRY_TARGET_REPLAY_SUPPORTED func testCaptureReplayShouldCallReplayIntegration() throws { guard #available(iOS 16.0, tvOS 16.0, *) else { return } @@ -443,6 +444,11 @@ class PrivateSentrySDKOnlyTests: XCTestCase { XCTAssertTrue(redactBuilder.isRedactContainerClassTestOnly(RedactContainer.self)) } + private func getFirstIntegrationAsReplay() throws -> SentrySessionReplayIntegration { + return try XCTUnwrap(SentrySDKInternal.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration) + } + #endif // SENTRY_TARGET_REPLAY_SUPPORTED + func testAddExtraSdkPackages() throws { PrivateSentrySDKOnly.addSdkPackage("package1", version: "version1") PrivateSentrySDKOnly.addSdkPackage("package2", version: "version2") @@ -462,10 +468,6 @@ class PrivateSentrySDKOnlyTests: XCTestCase { } } - private func getFirstIntegrationAsReplay() throws -> SentrySessionReplayIntegration { - return try XCTUnwrap(SentrySDKInternal.currentHub().installedIntegrations().first as? SentrySessionReplayIntegration) - } - private let VALID_REPLAY_ID = "0eac7ab503354dd5819b03e263627a29" #endif diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 13a7e9f67a4..317190f4a88 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1951,7 +1951,7 @@ class SentryClientTests: XCTestCase { if !SentryDependencyContainer.sharedInstance().crashWrapper.isBeingTraced { expectedIntegrations = ["ANRTracking"] + expectedIntegrations } -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) expectedIntegrations.append("FramesTracking") #endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) @@ -2503,7 +2503,7 @@ private extension SentryClientTests { } private func getSpan(operation: String, tracer: SentryTracer) -> Span { -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) return SentrySpan(tracer: tracer, context: SpanContext(operation: operation), framesTracker: nil) #else return SentrySpan(tracer: tracer, context: SpanContext(operation: operation)) diff --git a/Tests/SentryTests/SentryCrash/SentryCrashWrapperTests.swift b/Tests/SentryTests/SentryCrash/SentryCrashWrapperTests.swift index a272e046918..e2fd10e178b 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashWrapperTests.swift +++ b/Tests/SentryTests/SentryCrash/SentryCrashWrapperTests.swift @@ -1,4 +1,5 @@ @_spi(Private) @testable import Sentry +@_spi(Private) import SentryTestUtils import XCTest final class SentryCrashWrapperTests: XCTestCase { @@ -129,7 +130,97 @@ final class SentryCrashWrapperTests: XCTestCase { let runtimeContext = try XCTUnwrap(scope.contextDictionary["runtime"] as? [String: Any]) XCTAssertEqual(runtimeContext["name"] as? String, "Mac Catalyst App") - XCTAssertEqual(runtimeContext["raw_description"] as? String, "raw_description") + XCTAssertEqual(runtimeContext["raw_description"] as? String, "mac-catalyst-app") #endif } + + @available(macOS 12.0, *) + func testEnrichScope_DeviceContext_iOSAppOnMac() throws { + let mockProcessInfo = MockSentryProcessInfo() + mockProcessInfo.overrides.isiOSAppOnMac = true + mockProcessInfo.overrides.isMacCatalystApp = false + + let testScope = Scope() + let crashWrapper = SentryCrashWrapper(processInfoWrapper: mockProcessInfo, systemInfo: [ + "osVersion": "23A344", + "kernelVersion": "23.0.0", + "isJailbroken": false, + "systemName": "iOS", + "cpuArchitecture": "arm64", + "machine": "iPhone14,2", + "model": "iPhone 13 Pro", + "freeMemorySize": UInt64(1_073_741_824), + "usableMemorySize": UInt64(4_294_967_296), + "memorySize": UInt64(6_442_450_944), + "appStartTime": "2023-01-01T12:00:00Z", + "deviceAppHash": "abc123", + "appID": "12345", + "buildType": "debug" + ]) + + crashWrapper.enrichScope(testScope) + + let deviceContext = try XCTUnwrap(testScope.contextDictionary["device"] as? [String: Any]) + XCTAssertEqual(deviceContext["ios_app_on_macos"] as? Bool, true) + XCTAssertEqual(deviceContext["mac_catalyst_app"] as? Bool, false) + } + + @available(macOS 12.0, *) + func testEnrichScope_DeviceContext_MacCatalyst() throws { + let mockProcessInfo = MockSentryProcessInfo() + mockProcessInfo.overrides.isiOSAppOnMac = false + mockProcessInfo.overrides.isMacCatalystApp = true + + let testScope = Scope() + let crashWrapper = SentryCrashWrapper(processInfoWrapper: mockProcessInfo, systemInfo: [ + "osVersion": "23A344", + "kernelVersion": "23.0.0", + "isJailbroken": false, + "systemName": "iOS", + "cpuArchitecture": "arm64", + "machine": "iPhone14,2", + "model": "iPhone 13 Pro", + "freeMemorySize": UInt64(1_073_741_824), + "usableMemorySize": UInt64(4_294_967_296), + "memorySize": UInt64(6_442_450_944), + "appStartTime": "2023-01-01T12:00:00Z", + "deviceAppHash": "abc123", + "appID": "12345", + "buildType": "debug" + ]) + + crashWrapper.enrichScope(testScope) + + let deviceContext = try XCTUnwrap(testScope.contextDictionary["device"] as? [String: Any]) + XCTAssertEqual(deviceContext["ios_app_on_macos"] as? Bool, false) + XCTAssertEqual(deviceContext["mac_catalyst_app"] as? Bool, true) + } + + func testEnrichScope_DeviceContext_iOSAppOnVisionOS() throws { + let mockProcessInfo = MockSentryProcessInfo() + mockProcessInfo.overrides.isiOSAppOnVisionOS = true + + let testScope = Scope() + let crashWrapper = SentryCrashWrapper(processInfoWrapper: mockProcessInfo, systemInfo: [ + "osVersion": "23A344", + "kernelVersion": "23.0.0", + "isJailbroken": false, + "systemName": "iOS", + "cpuArchitecture": "arm64", + "machine": "iPhone14,2", + "model": "iPhone 13 Pro", + "freeMemorySize": UInt64(1_073_741_824), + "usableMemorySize": UInt64(4_294_967_296), + "memorySize": UInt64(6_442_450_944), + "appStartTime": "2023-01-01T12:00:00Z", + "deviceAppHash": "abc123", + "appID": "12345", + "buildType": "debug" + ]) + + crashWrapper.enrichScope(testScope) + + let deviceContext = try XCTUnwrap(testScope.contextDictionary["device"] as? [String: Any]) + XCTAssertEqual(deviceContext["ios_app_on_visionos"] as? Bool, true) + } } diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index 08dd1a5b438..ee37030db23 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -238,7 +238,7 @@ class SentryHubTests: XCTestCase { // Assert let runtimeContext = try XCTUnwrap (hub.scope.contextDictionary["runtime"] as? [String: String]) XCTAssertEqual(runtimeContext["name"], "Mac Catalyst App") - XCTAssertEqual(runtimeContext["raw_description"], "raw_description") + XCTAssertEqual(runtimeContext["raw_description"], "mac-catalyst-app") } func testScopeNotEnriched_WhenScopeIsNil() { diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 9ca5afb041e..8449d387e4c 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -713,7 +713,7 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqual(200 * 1024 * 1024, options.maxAttachmentSize); XCTAssertEqual(NO, options.sendDefaultPii); XCTAssertTrue(options.enableAutoPerformanceTracing); -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION XCTAssertTrue(options.enableUIViewControllerTracing); XCTAssertFalse(options.attachScreenshot); XCTAssertEqual(3.0, options.idleTimeout); @@ -893,6 +893,7 @@ - (void)testEnablePreWarmedAppStartTracking [self testBooleanField:@"enablePreWarmedAppStartTracing" defaultValue:YES]; } +# if !TARGET_OS_VISION - (void)testSessionReplaySettingsInit { if (@available(iOS 16.0, tvOS 16.0, *)) { @@ -912,7 +913,7 @@ - (void)testSessionReplaySettingsDefaults XCTAssertEqual(options.sessionReplay.onErrorSampleRate, 0); } } - +# endif #endif // SENTRY_HAS_UIKIT #if SENTRY_HAS_METRIC_KIT diff --git a/Tests/SentryTests/SentrySDKTests.swift b/Tests/SentryTests/SentrySDKTests.swift index 9ec693a716e..1a6c3648b15 100644 --- a/Tests/SentryTests/SentrySDKTests.swift +++ b/Tests/SentryTests/SentrySDKTests.swift @@ -141,7 +141,7 @@ class SentrySDKTests: XCTestCase { if !SentryDependencyContainer.sharedInstance().crashWrapper.isBeingTraced { expectedIntegrations.append("SentryANRTrackingIntegration") } -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) expectedIntegrations.append("SentryFramesTrackingIntegration") #endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index c9dec3767f6..8ff4a0b6c10 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -9,8 +9,10 @@ #if SENTRY_HAS_UIKIT # import "MockUIScene.h" # import "SentryDefaultUIViewControllerPerformanceTracker.h" -# import "SentrySessionReplayIntegration+Private.h" -# import "SentrySessionReplayIntegration+Test.h" +# if SENTRY_TARGET_REPLAY_SUPPORTED +# import "SentrySessionReplayIntegration+Private.h" +# import "SentrySessionReplayIntegration+Test.h" +# endif // SENTRY_TARGET_REPLAY_SUPPORTED # import "SentryUIEventTracker.h" # import "SentryUIEventTrackerTransactionMode.h" # import "SentryUIEventTrackingIntegration.h" diff --git a/Tests/SentryTests/TestSentryNSApplication.swift b/Tests/SentryTests/TestSentryNSApplication.swift index acf4e0ba5cf..9ccca0c5cbf 100644 --- a/Tests/SentryTests/TestSentryNSApplication.swift +++ b/Tests/SentryTests/TestSentryNSApplication.swift @@ -1,6 +1,6 @@ @_spi(Private) @testable import Sentry -#if !(os(iOS) || targetEnvironment(macCatalyst) || os(tvOS)) +#if !(os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) class TestSentryNSApplication: SentryApplication { private var _underlyingIsActive = true func setIsActive(_ isActive: Bool) { diff --git a/Tests/SentryTests/Transaction/SentrySpanTests.swift b/Tests/SentryTests/Transaction/SentrySpanTests.swift index 476b597e28d..bf812293084 100644 --- a/Tests/SentryTests/Transaction/SentrySpanTests.swift +++ b/Tests/SentryTests/Transaction/SentrySpanTests.swift @@ -15,7 +15,7 @@ class SentrySpanTests: XCTestCase { let options: Options let notificationCenter = TestNSNotificationCenterWrapper() let currentDateProvider = TestCurrentDateProvider() -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT let tracer = SentryTracer(context: SpanContext(operation: "TEST"), framesTracker: nil) #else let tracer = SentryTracer(context: SpanContext(operation: "TEST")) @@ -41,7 +41,7 @@ class SentrySpanTests: XCTestCase { } func getSutWithTracer() -> SentrySpan { -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT return SentrySpan(tracer: tracer, context: SpanContext(operation: someOperation, sampled: .undecided), framesTracker: nil) #else return SentrySpan(tracer: tracer, context: SpanContext(operation: someOperation, sampled: .undecided)) @@ -538,7 +538,7 @@ class SentrySpanTests: XCTestCase { // Span has a weak reference to tracer. If we don't keep a reference // to the tracer ARC will deallocate the tracer. let sutGenerator: () -> Span = { -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT let tracer = SentryTracer(context: SpanContext(operation: "TEST"), framesTracker: nil) return SentrySpan(tracer: tracer, context: SpanContext(operation: ""), framesTracker: nil) #else @@ -628,7 +628,7 @@ class SentrySpanTests: XCTestCase { XCTAssertEqual(expectedBaggage, sut.baggageHttpHeader()) } -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT func testAddSlowFrozenFramesToData() { let (displayLinkWrapper, framesTracker) = givenFramesTracker() @@ -706,5 +706,5 @@ class SentrySpanTests: XCTestCase { return (displayLinkWrapper, framesTracker) } -#endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#endif // (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT } diff --git a/Tests/SentryTests/Transaction/SentryTracerTests.swift b/Tests/SentryTests/Transaction/SentryTracerTests.swift index bc001e564e8..2c06ea4a7d3 100644 --- a/Tests/SentryTests/Transaction/SentryTracerTests.swift +++ b/Tests/SentryTests/Transaction/SentryTracerTests.swift @@ -615,7 +615,7 @@ class SentryTracerTests: XCTestCase { } func testIdleTimeout_TracerDeallocated() throws { -#if !os(tvOS) && !os(watchOS) +#if !os(tvOS) && !os(watchOS) && !os(visionOS) if sentry_threadSanitizerIsPresent() { throw XCTSkip("doesn't currently work with TSAN enabled. the tracer instance remains retained by something in the TSAN dylib, and we cannot debug the memory graph with TSAN attached to see what is retaining it. it's likely out of our control.") } diff --git a/Tests/SentryTests/UIImageHelperTests.swift b/Tests/SentryTests/UIImageHelperTests.swift index 1b92c573f3d..f8292ac7ac3 100644 --- a/Tests/SentryTests/UIImageHelperTests.swift +++ b/Tests/SentryTests/UIImageHelperTests.swift @@ -1,4 +1,5 @@ #if canImport(UIKit) +#if os(iOS) || os(tvOS) import Foundation @testable import Sentry import XCTest @@ -61,4 +62,5 @@ class UIImageHelperTests: XCTestCase { } } -#endif +#endif // os(iOS) || os(tvOS) +#endif // canImport(UIKit) From ad1b8e0ac774618a3fd94302d8fa3639df6fa384 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 1 Dec 2025 15:56:29 +0100 Subject: [PATCH 2/6] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6de2fa864a0..f95532ce457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Feature - Add options to customize UserFeedback error messages (#6790) +- Add isiOSAppOnVisionOS, isiOSAppOnMac, isMacCatalystApp to device context #6939 ### Fixes From ad08e2681d82ab25ed8ddc2d649e5be579008f68 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 2 Dec 2025 11:46:16 +0100 Subject: [PATCH 3/6] WIP --- CHANGELOG.md | 7 ++++++- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- Sources/Sentry/SentryBuildAppStartSpans.m | 2 +- Sources/Sentry/SentrySpan.m | 6 +++--- Sources/Sentry/SentryTracer.m | 20 +++++++++---------- Sources/Sentry/include/SentrySpan.h | 4 ++-- Sources/Swift/Helper/SentryProcessInfo.swift | 16 ++++++++++----- .../SentryANRTrackingIntegrationTests.swift | 2 +- .../SentryTimeToDisplayTrackerTest.swift | 2 +- .../PrivateSentrySDKOnlyTests.swift | 2 +- Tests/SentryTests/Protocol/TestData.swift | 6 +++--- Tests/SentryTests/SentryClientTests.swift | 4 ++-- Tests/SentryTests/SentryOptionsTest.m | 8 ++++---- Tests/SentryTests/SentrySDKTests.swift | 2 +- .../SentryTests/TestSentryNSApplication.swift | 2 +- .../SentryBuildAppStartSpansTests.swift | 2 +- .../Transaction/SentrySpanTests.swift | 10 +++++----- 17 files changed, 54 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8423f86044f..c0c046d7895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add isiOSAppOnVisionOS, isiOSAppOnMac, isMacCatalystApp to device context #6939 + ## 9.0.0 This changelog lists every breaking change. For a high-level overview and upgrade guidance, see the [migration guide](https://docs.sentry.io/platforms/apple/migration/). @@ -62,7 +68,6 @@ This changelog lists every breaking change. For a high-level overview and upgrad - Add `sentry.replay_id` attribute to logs ([#6515](https://github.com/getsentry/sentry-cocoa/pull/6515)) - Structured Logs: Add log APIs to `Hub` and `Client` (#6737) - Add options to customize UserFeedback error messages (#6790) -- Add isiOSAppOnVisionOS, isiOSAppOnMac, isMacCatalystApp to device context #6939 ### Fixes diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 17d87f9778b..b685e071d6a 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -26,7 +26,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { SampleAppDebugMenu.shared.display() metricKit.receiveReports() - + return true } diff --git a/Sources/Sentry/SentryBuildAppStartSpans.m b/Sources/Sentry/SentryBuildAppStartSpans.m index e3e0b5e60f8..38b6fb649de 100644 --- a/Sources/Sentry/SentryBuildAppStartSpans.m +++ b/Sources/Sentry/SentryBuildAppStartSpans.m @@ -7,7 +7,7 @@ #import "SentryTracer.h" #import -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION SentrySpan * sentryBuildAppStartSpan( diff --git a/Sources/Sentry/SentrySpan.m b/Sources/Sentry/SentrySpan.m index 1e5e782a8ca..55cc1e468b7 100644 --- a/Sources/Sentry/SentrySpan.m +++ b/Sources/Sentry/SentrySpan.m @@ -46,7 +46,7 @@ @implementation SentrySpan { } - (instancetype)initWithContext:(SentrySpanContext *)context -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION framesTracker:(nullable SentryFramesTracker *)framesTracker; #endif // SENTRY_HAS_UIKIT { @@ -65,7 +65,7 @@ - (instancetype)initWithContext:(SentrySpanContext *)context getThreadName:currentThread]; } -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION _framesTracker = framesTracker; if (_framesTracker.isRunning) { SentryScreenFrames *currentFrames = _framesTracker.currentFrames; @@ -137,7 +137,7 @@ - (void)stopObservingContinuousProfiling - (instancetype)initWithTracer:(SentryTracer *)tracer context:(SentrySpanContext *)context -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION framesTracker:(nullable SentryFramesTracker *)framesTracker { if (self = [self initWithContext:context framesTracker:framesTracker]) { diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index 5f41a8b5d4c..39e027c80ec 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -30,7 +30,7 @@ # import "SentryProfiledTracerConcurrency.h" #endif // SENTRY_TARGET_PROFILING_SUPPORTED -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION # import "SentryAppStartMeasurement.h" # import "SentryBuildAppStartSpans.h" #endif // SENTRY_HAS_UIKIT @@ -39,7 +39,7 @@ static const void *spanTimestampObserver = &spanTimestampObserver; -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION /** * The maximum amount of seconds the app start measurement end time and the start time of the * transaction are allowed to be apart. @@ -114,7 +114,7 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti configuration:(SentryTracerConfiguration *)configuration; { if (!(self = [super initWithContext:transactionContext -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION framesTracker:SentryDependencyContainer.sharedInstance.framesTracker #endif // SENTRY_HAS_UIKIT ])) { @@ -390,7 +390,7 @@ - (BOOL)isAutoGeneratedTransaction SentrySpan *child = [[SentrySpan alloc] initWithTracer:self context:context -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION framesTracker:SentryDependencyContainer.sharedInstance.framesTracker #endif // SENTRY_HAS_UIKIT ]; @@ -615,13 +615,13 @@ - (BOOL)finishTracer:(SentrySpanStatus)unfinishedSpansFinishStatus shouldCleanUp } [super finishWithStatus:_finishStatus]; } -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION appStartMeasurement = [self getAppStartMeasurement]; if (appStartMeasurement != nil) { [self updateStartTime:appStartMeasurement.appStartTimestamp]; } -#endif // SENTRY_HAS_UIKIT +#endif // SENTRY_HAS_UIKIT && !TARGET_OS_VISION if (shouldCleanUp) { [self.delegate tracerDidFinish:self]; @@ -721,7 +721,7 @@ - (SentryTransaction *)toTransaction { NSUInteger capacity; -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION [self addFrameStatistics]; NSArray> *appStartSpans = sentryBuildAppStartSpans(self, appStartMeasurement); @@ -736,7 +736,7 @@ - (SentryTransaction *)toTransaction [spans addObjectsFromArray:_children]; } -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION [spans addObjectsFromArray:appStartSpans]; #endif // SENTRY_HAS_UIKIT @@ -763,7 +763,7 @@ - (SentryTransaction *)toTransaction [debugImageProvider getDebugImagesFromCacheForFrames:framesOfAllSpans]; } -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION [self addAppStartMeasurements:transaction]; if ([viewNames count] > 0) { @@ -774,7 +774,7 @@ - (SentryTransaction *)toTransaction return transaction; } -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION - (nullable SentryAppStartMeasurement *)getAppStartMeasurement SENTRY_DISABLE_THREAD_SANITIZER( "double-checked lock produce false alarms") diff --git a/Sources/Sentry/include/SentrySpan.h b/Sources/Sentry/include/SentrySpan.h index b15dbe69428..bcc9fba7459 100644 --- a/Sources/Sentry/include/SentrySpan.h +++ b/Sources/Sentry/include/SentrySpan.h @@ -96,7 +96,7 @@ SENTRY_NO_INIT */ - (instancetype)initWithTracer:(SentryTracer *)transaction context:(SentrySpanContext *)context -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION framesTracker:(nullable SentryFramesTracker *)framesTracker; #endif // SENTRY_HAS_UIKIT ; @@ -106,7 +106,7 @@ SENTRY_NO_INIT * @param context This span context information. */ - (instancetype)initWithContext:(SentrySpanContext *)context -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION framesTracker:(nullable SentryFramesTracker *)framesTracker; #endif // SENTRY_HAS_UIKIT ; diff --git a/Sources/Swift/Helper/SentryProcessInfo.swift b/Sources/Swift/Helper/SentryProcessInfo.swift index db48e1c882b..95e8057044f 100644 --- a/Sources/Swift/Helper/SentryProcessInfo.swift +++ b/Sources/Swift/Helper/SentryProcessInfo.swift @@ -33,11 +33,17 @@ if #available(iOS 26.1, visionOS 26.1, *) { // Use official API when available // https://developer.apple.com/documentation/foundation/processinfo/isiosapponvision - return self.isiOSAppOnVision - } else { - // Fallback for older versions: `UIWindowSceneGeometryPreferencesVision` is only available on visionOS - // https://developer.apple.com/documentation/uikit/uiwindowscene/geometrypreferences/vision?language=objc - return NSClassFromString("UIWindowSceneGeometryPreferencesVision") != nil + // + // For unknown reasons when running an iOS app "Designed for iPad" on visionOS 1.1, the simulator system + // version is simulator 17.4, but it still enters this block. + // + // Due to that it crashes with an uncaught exception 'NSInvalidArgumentException', reason: '-[NSProcessInfo isiOSAppOnVision]: unrecognized selector sent to instance 0x600001549230' + if self.responds(to: NSSelectorFromString("isiOSAppOnVision")) { + return self.isiOSAppOnVision + } } + // Fallback for older versions: `UIWindowSceneGeometryPreferencesVision` is only available on visionOS + // https://developer.apple.com/documentation/uikit/uiwindowscene/geometrypreferences/vision?language=objc + return NSClassFromString("UIWindowSceneGeometryPreferencesVision") != nil } } diff --git a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift index a962d73061f..5726042d5ba 100644 --- a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift @@ -78,7 +78,7 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase { let tracker = try XCTUnwrap(Dynamic(sut).tracker.asAnyObject as? SentryANRTracker) -#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) XCTAssertTrue(tracker.helper is SentryANRTrackerV2, "Expected SentryANRTrackerV2, but got \(type(of: tracker))") #else XCTAssertTrue(tracker.helper is SentryANRTrackerV1, "Expected SentryANRTrackerV1 on macOS, but got \(type(of: tracker))") diff --git a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift index 61f74fd8314..50a5fe2c78a 100644 --- a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift +++ b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift @@ -322,7 +322,7 @@ class SentryTimeToDisplayTrackerTest: XCTestCase { let expectedInvocations = invocationsBefore + 1 XCTAssertEqual(dispatchQueueWrapper.blockOnMainInvocations.count, expectedInvocations, "reportFullyDisplayed should be dispatched on the main queue. ") } - + func testNotWaitingForFullyDisplayed_AfterTracerTimesOut() throws { fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 9)) diff --git a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift index 5e7698b3040..208583dbaa5 100644 --- a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift +++ b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift @@ -126,7 +126,7 @@ class PrivateSentrySDKOnlyTests: XCTestCase { XCTAssertNil(PrivateSentrySDKOnly.envelope(with: itemData)) } - #if canImport(UIKit) && !os(visionOS) + #if canImport(UIKit) func testGetAppStartMeasurement() { let appStartMeasurement = TestData.getAppStartMeasurement(type: .warm, runtimeInitSystemTimestamp: 1) SentrySDKInternal.setAppStartMeasurement(appStartMeasurement) diff --git a/Tests/SentryTests/Protocol/TestData.swift b/Tests/SentryTests/Protocol/TestData.swift index 538c3d198de..6c9f8d6b3a7 100644 --- a/Tests/SentryTests/Protocol/TestData.swift +++ b/Tests/SentryTests/Protocol/TestData.swift @@ -2,7 +2,7 @@ import Sentry @_spi(Private) import Sentry @_spi(Private) import SentryTestUtils -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) import UIKit #endif @@ -340,8 +340,8 @@ class TestData { return request } - #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) - + #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || os(visionOS) + static func getAppStartMeasurement( type: SentryAppStartType, appStartTimestamp: Date = TestData.timestamp, diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 317190f4a88..13a7e9f67a4 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1951,7 +1951,7 @@ class SentryClientTests: XCTestCase { if !SentryDependencyContainer.sharedInstance().crashWrapper.isBeingTraced { expectedIntegrations = ["ANRTracking"] + expectedIntegrations } -#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) expectedIntegrations.append("FramesTracking") #endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) @@ -2503,7 +2503,7 @@ private extension SentryClientTests { } private func getSpan(operation: String, tracer: SentryTracer) -> Span { -#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) return SentrySpan(tracer: tracer, context: SpanContext(operation: operation), framesTracker: nil) #else return SentrySpan(tracer: tracer, context: SpanContext(operation: operation)) diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 8449d387e4c..bb837bc1f98 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -359,7 +359,7 @@ - (void)testBeforeSendSpan SentryOptions *options = [self getValidOptions:@{ @"beforeSendSpan" : callback }]; options.beforeSendSpan( [[SentrySpan alloc] initWithContext:[[SentrySpanContext alloc] initWithOperation:@""] -#if SENTRY_HAS_UIKIT +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION framesTracker:NULL #endif ]); @@ -713,7 +713,7 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqual(200 * 1024 * 1024, options.maxAttachmentSize); XCTAssertEqual(NO, options.sendDefaultPii); XCTAssertTrue(options.enableAutoPerformanceTracing); -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT && SENTRY_TARGET_REPLAY_SUPPORTED XCTAssertTrue(options.enableUIViewControllerTracing); XCTAssertFalse(options.attachScreenshot); XCTAssertEqual(3.0, options.idleTimeout); @@ -893,7 +893,7 @@ - (void)testEnablePreWarmedAppStartTracking [self testBooleanField:@"enablePreWarmedAppStartTracing" defaultValue:YES]; } -# if !TARGET_OS_VISION +# if SENTRY_TARGET_REPLAY_SUPPORTED - (void)testSessionReplaySettingsInit { if (@available(iOS 16.0, tvOS 16.0, *)) { @@ -913,7 +913,7 @@ - (void)testSessionReplaySettingsDefaults XCTAssertEqual(options.sessionReplay.onErrorSampleRate, 0); } } -# endif +# endif // SENTRY_TARGET_REPLAY_SUPPORTED #endif // SENTRY_HAS_UIKIT #if SENTRY_HAS_METRIC_KIT diff --git a/Tests/SentryTests/SentrySDKTests.swift b/Tests/SentryTests/SentrySDKTests.swift index 1a6c3648b15..9ec693a716e 100644 --- a/Tests/SentryTests/SentrySDKTests.swift +++ b/Tests/SentryTests/SentrySDKTests.swift @@ -141,7 +141,7 @@ class SentrySDKTests: XCTestCase { if !SentryDependencyContainer.sharedInstance().crashWrapper.isBeingTraced { expectedIntegrations.append("SentryANRTrackingIntegration") } -#if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) expectedIntegrations.append("SentryFramesTrackingIntegration") #endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) diff --git a/Tests/SentryTests/TestSentryNSApplication.swift b/Tests/SentryTests/TestSentryNSApplication.swift index 9ccca0c5cbf..888bb80f5d4 100644 --- a/Tests/SentryTests/TestSentryNSApplication.swift +++ b/Tests/SentryTests/TestSentryNSApplication.swift @@ -1,6 +1,6 @@ @_spi(Private) @testable import Sentry -#if !(os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || (swift(>=5.9) && os(visionOS))) +#if !(os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || os(visionOS)) class TestSentryNSApplication: SentryApplication { private var _underlyingIsActive = true func setIsActive(_ isActive: Bool) { diff --git a/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift b/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift index b78bd129b3f..31bd88407c3 100644 --- a/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift +++ b/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift @@ -1,7 +1,7 @@ @testable import Sentry import XCTest -#if canImport(UIKit) +#if canImport(UIKit) && !os(visionOS) class SentryBuildAppStartSpansTests: XCTestCase { func testSentryBuildAppStartSpans_appStartMeasurementIsNil_shouldNotReturnAnySpans() { diff --git a/Tests/SentryTests/Transaction/SentrySpanTests.swift b/Tests/SentryTests/Transaction/SentrySpanTests.swift index bf812293084..476b597e28d 100644 --- a/Tests/SentryTests/Transaction/SentrySpanTests.swift +++ b/Tests/SentryTests/Transaction/SentrySpanTests.swift @@ -15,7 +15,7 @@ class SentrySpanTests: XCTestCase { let options: Options let notificationCenter = TestNSNotificationCenterWrapper() let currentDateProvider = TestCurrentDateProvider() -#if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) let tracer = SentryTracer(context: SpanContext(operation: "TEST"), framesTracker: nil) #else let tracer = SentryTracer(context: SpanContext(operation: "TEST")) @@ -41,7 +41,7 @@ class SentrySpanTests: XCTestCase { } func getSutWithTracer() -> SentrySpan { -#if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) return SentrySpan(tracer: tracer, context: SpanContext(operation: someOperation, sampled: .undecided), framesTracker: nil) #else return SentrySpan(tracer: tracer, context: SpanContext(operation: someOperation, sampled: .undecided)) @@ -538,7 +538,7 @@ class SentrySpanTests: XCTestCase { // Span has a weak reference to tracer. If we don't keep a reference // to the tracer ARC will deallocate the tracer. let sutGenerator: () -> Span = { -#if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) let tracer = SentryTracer(context: SpanContext(operation: "TEST"), framesTracker: nil) return SentrySpan(tracer: tracer, context: SpanContext(operation: ""), framesTracker: nil) #else @@ -628,7 +628,7 @@ class SentrySpanTests: XCTestCase { XCTAssertEqual(expectedBaggage, sut.baggageHttpHeader()) } -#if (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) func testAddSlowFrozenFramesToData() { let (displayLinkWrapper, framesTracker) = givenFramesTracker() @@ -706,5 +706,5 @@ class SentrySpanTests: XCTestCase { return (displayLinkWrapper, framesTracker) } -#endif // (os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) || (swift(>=5.9) && os(visionOS))) && !SENTRY_NO_UIKIT +#endif // os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) } From b6c509fa94d694529a5bcdf7678080cb4549afa2 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 23 Dec 2025 14:54:38 +0100 Subject: [PATCH 4/6] cleanup --- Sources/Sentry/SentryBuildAppStartSpans.m | 2 +- Sources/Sentry/SentrySpan.m | 6 +++--- Sources/Sentry/SentryTracer.m | 20 +++++++++---------- Sources/Sentry/include/SentrySpan.h | 4 ++-- .../SentryCrash/SentryCrashWrapper.swift | 3 +-- .../SentryTimeToDisplayTrackerTest.swift | 1 - Tests/SentryTests/SentryOptionsTest.m | 4 ++-- .../SentryBuildAppStartSpansTests.swift | 2 +- 8 files changed, 20 insertions(+), 22 deletions(-) diff --git a/Sources/Sentry/SentryBuildAppStartSpans.m b/Sources/Sentry/SentryBuildAppStartSpans.m index 38b6fb649de..e3e0b5e60f8 100644 --- a/Sources/Sentry/SentryBuildAppStartSpans.m +++ b/Sources/Sentry/SentryBuildAppStartSpans.m @@ -7,7 +7,7 @@ #import "SentryTracer.h" #import -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT SentrySpan * sentryBuildAppStartSpan( diff --git a/Sources/Sentry/SentrySpan.m b/Sources/Sentry/SentrySpan.m index 55cc1e468b7..1e5e782a8ca 100644 --- a/Sources/Sentry/SentrySpan.m +++ b/Sources/Sentry/SentrySpan.m @@ -46,7 +46,7 @@ @implementation SentrySpan { } - (instancetype)initWithContext:(SentrySpanContext *)context -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT framesTracker:(nullable SentryFramesTracker *)framesTracker; #endif // SENTRY_HAS_UIKIT { @@ -65,7 +65,7 @@ - (instancetype)initWithContext:(SentrySpanContext *)context getThreadName:currentThread]; } -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT _framesTracker = framesTracker; if (_framesTracker.isRunning) { SentryScreenFrames *currentFrames = _framesTracker.currentFrames; @@ -137,7 +137,7 @@ - (void)stopObservingContinuousProfiling - (instancetype)initWithTracer:(SentryTracer *)tracer context:(SentrySpanContext *)context -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT framesTracker:(nullable SentryFramesTracker *)framesTracker { if (self = [self initWithContext:context framesTracker:framesTracker]) { diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index 39e027c80ec..5f41a8b5d4c 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -30,7 +30,7 @@ # import "SentryProfiledTracerConcurrency.h" #endif // SENTRY_TARGET_PROFILING_SUPPORTED -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT # import "SentryAppStartMeasurement.h" # import "SentryBuildAppStartSpans.h" #endif // SENTRY_HAS_UIKIT @@ -39,7 +39,7 @@ static const void *spanTimestampObserver = &spanTimestampObserver; -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT /** * The maximum amount of seconds the app start measurement end time and the start time of the * transaction are allowed to be apart. @@ -114,7 +114,7 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti configuration:(SentryTracerConfiguration *)configuration; { if (!(self = [super initWithContext:transactionContext -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT framesTracker:SentryDependencyContainer.sharedInstance.framesTracker #endif // SENTRY_HAS_UIKIT ])) { @@ -390,7 +390,7 @@ - (BOOL)isAutoGeneratedTransaction SentrySpan *child = [[SentrySpan alloc] initWithTracer:self context:context -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT framesTracker:SentryDependencyContainer.sharedInstance.framesTracker #endif // SENTRY_HAS_UIKIT ]; @@ -615,13 +615,13 @@ - (BOOL)finishTracer:(SentrySpanStatus)unfinishedSpansFinishStatus shouldCleanUp } [super finishWithStatus:_finishStatus]; } -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT appStartMeasurement = [self getAppStartMeasurement]; if (appStartMeasurement != nil) { [self updateStartTime:appStartMeasurement.appStartTimestamp]; } -#endif // SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#endif // SENTRY_HAS_UIKIT if (shouldCleanUp) { [self.delegate tracerDidFinish:self]; @@ -721,7 +721,7 @@ - (SentryTransaction *)toTransaction { NSUInteger capacity; -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT [self addFrameStatistics]; NSArray> *appStartSpans = sentryBuildAppStartSpans(self, appStartMeasurement); @@ -736,7 +736,7 @@ - (SentryTransaction *)toTransaction [spans addObjectsFromArray:_children]; } -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT [spans addObjectsFromArray:appStartSpans]; #endif // SENTRY_HAS_UIKIT @@ -763,7 +763,7 @@ - (SentryTransaction *)toTransaction [debugImageProvider getDebugImagesFromCacheForFrames:framesOfAllSpans]; } -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT [self addAppStartMeasurements:transaction]; if ([viewNames count] > 0) { @@ -774,7 +774,7 @@ - (SentryTransaction *)toTransaction return transaction; } -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT - (nullable SentryAppStartMeasurement *)getAppStartMeasurement SENTRY_DISABLE_THREAD_SANITIZER( "double-checked lock produce false alarms") diff --git a/Sources/Sentry/include/SentrySpan.h b/Sources/Sentry/include/SentrySpan.h index bcc9fba7459..b15dbe69428 100644 --- a/Sources/Sentry/include/SentrySpan.h +++ b/Sources/Sentry/include/SentrySpan.h @@ -96,7 +96,7 @@ SENTRY_NO_INIT */ - (instancetype)initWithTracer:(SentryTracer *)transaction context:(SentrySpanContext *)context -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT framesTracker:(nullable SentryFramesTracker *)framesTracker; #endif // SENTRY_HAS_UIKIT ; @@ -106,7 +106,7 @@ SENTRY_NO_INIT * @param context This span context information. */ - (instancetype)initWithContext:(SentrySpanContext *)context -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT framesTracker:(nullable SentryFramesTracker *)framesTracker; #endif // SENTRY_HAS_UIKIT ; diff --git a/Sources/Swift/SentryCrash/SentryCrashWrapper.swift b/Sources/Swift/SentryCrash/SentryCrashWrapper.swift index d09300732b7..167ba340e42 100644 --- a/Sources/Swift/SentryCrash/SentryCrashWrapper.swift +++ b/Sources/Swift/SentryCrash/SentryCrashWrapper.swift @@ -173,8 +173,7 @@ public final class SentryCrashWrapper: NSObject { if #available(macOS 12, *) { deviceData["ios_app_on_macos"] = self.processInfoWrapper.isiOSAppOnMac deviceData["mac_catalyst_app"] = self.processInfoWrapper.isMacCatalystApp - } - + } deviceData["ios_app_on_visionos"] = self.processInfoWrapper.isiOSAppOnVisionOS // Set screen dimensions if available diff --git a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift index d7ecab0fb89..058e84a0694 100644 --- a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift +++ b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift @@ -6,7 +6,6 @@ import XCTest #if os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst) class TestDelayedWrapper: SentryDelayedFramesTracker {} -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) class SentryTimeToDisplayTrackerTest: XCTestCase { private class Fixture { diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 0e22b750a65..8c86b37dd1c 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -359,7 +359,7 @@ - (void)testBeforeSendSpan SentryOptions *options = [self getValidOptions:@{ @"beforeSendSpan" : callback }]; options.beforeSendSpan( [[SentrySpan alloc] initWithContext:[[SentrySpanContext alloc] initWithOperation:@""] -#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +#if SENTRY_HAS_UIKIT framesTracker:NULL #endif ]); @@ -713,7 +713,7 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqual(200 * 1024 * 1024, options.maxAttachmentSize); XCTAssertEqual(NO, options.sendDefaultPii); XCTAssertTrue(options.enableAutoPerformanceTracing); -#if SENTRY_HAS_UIKIT && SENTRY_TARGET_REPLAY_SUPPORTED +#if SENTRY_HAS_UIKIT XCTAssertTrue(options.enableUIViewControllerTracing); XCTAssertFalse(options.attachScreenshot); XCTAssertEqual(3.0, options.idleTimeout); diff --git a/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift b/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift index 31bd88407c3..b78bd129b3f 100644 --- a/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift +++ b/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift @@ -1,7 +1,7 @@ @testable import Sentry import XCTest -#if canImport(UIKit) && !os(visionOS) +#if canImport(UIKit) class SentryBuildAppStartSpansTests: XCTestCase { func testSentryBuildAppStartSpans_appStartMeasurementIsNil_shouldNotReturnAnySpans() { From 6960e9ac53350d2195488d18daf0845da4ce410f Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 23 Dec 2025 14:59:42 +0100 Subject: [PATCH 5/6] fix: Dynamically access isiOSAppOnVision property to prevent crashes on older SDKs --- Sources/Swift/Helper/SentryProcessInfo.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Swift/Helper/SentryProcessInfo.swift b/Sources/Swift/Helper/SentryProcessInfo.swift index 95e8057044f..a04aad87440 100644 --- a/Sources/Swift/Helper/SentryProcessInfo.swift +++ b/Sources/Swift/Helper/SentryProcessInfo.swift @@ -39,7 +39,11 @@ // // Due to that it crashes with an uncaught exception 'NSInvalidArgumentException', reason: '-[NSProcessInfo isiOSAppOnVision]: unrecognized selector sent to instance 0x600001549230' if self.responds(to: NSSelectorFromString("isiOSAppOnVision")) { - return self.isiOSAppOnVision + // Use value(forKey:) to dynamically access the property at runtime + // This avoids compile-time errors when the API isn't available in the SDK headers of Xcode 16 or older. + if let value = self.value(forKey: "isiOSAppOnVision") as? Bool { + return value + } } } // Fallback for older versions: `UIWindowSceneGeometryPreferencesVision` is only available on visionOS From 31f637fd1637bdecd2950796c0bdefc1027487ca Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Wed, 7 Jan 2026 17:38:33 +0100 Subject: [PATCH 6/6] apply feedback from review --- Sources/Swift/Helper/SentryProcessInfo.swift | 11 ++++- .../SentryCrash/SentryCrashWrapper.swift | 17 ++++++-- .../SentryCrash/SentryCrashWrapperTests.swift | 41 ++++++++++++++++++- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/Sources/Swift/Helper/SentryProcessInfo.swift b/Sources/Swift/Helper/SentryProcessInfo.swift index a04aad87440..7061803a07b 100644 --- a/Sources/Swift/Helper/SentryProcessInfo.swift +++ b/Sources/Swift/Helper/SentryProcessInfo.swift @@ -41,13 +41,22 @@ if self.responds(to: NSSelectorFromString("isiOSAppOnVision")) { // Use value(forKey:) to dynamically access the property at runtime // This avoids compile-time errors when the API isn't available in the SDK headers of Xcode 16 or older. + // + // It's important to keep this approach as long as we support compiling with Xcode 16, + // because users might compile the SDK locally using Xcode 16 via SPM or other distribution methods. if let value = self.value(forKey: "isiOSAppOnVision") as? Bool { return value } } } - // Fallback for older versions: `UIWindowSceneGeometryPreferencesVision` is only available on visionOS + // Fallback for older visionOS versions (< 26.1) when the official API isn't available. + // The `#if os(iOS)` check is necessary because `UIWindowSceneGeometryPreferencesVision` exists on all + // visionOS builds, so without this compile-time check, native visionOS apps would incorrectly return `true`. + #if os(iOS) // https://developer.apple.com/documentation/uikit/uiwindowscene/geometrypreferences/vision?language=objc return NSClassFromString("UIWindowSceneGeometryPreferencesVision") != nil + #else + return false + #endif } } diff --git a/Sources/Swift/SentryCrash/SentryCrashWrapper.swift b/Sources/Swift/SentryCrash/SentryCrashWrapper.swift index 09bd9afba23..0bb7a2a2ca9 100644 --- a/Sources/Swift/SentryCrash/SentryCrashWrapper.swift +++ b/Sources/Swift/SentryCrash/SentryCrashWrapper.swift @@ -170,11 +170,20 @@ public final class SentryCrashWrapper: NSObject { deviceData["locale"] = Locale.autoupdatingCurrent.identifier + // Only include these boolean flags when they are `true` to reduce payload size. + // These flags are `false` for the vast majority of events (regular iOS apps, native macOS apps, etc.), + // so omitting them when `false` significantly reduces the payload size without losing meaningful information. if #available(macOS 12, *) { - deviceData["ios_app_on_macos"] = self.processInfoWrapper.isiOSAppOnMac - deviceData["mac_catalyst_app"] = self.processInfoWrapper.isMacCatalystApp - } - deviceData["ios_app_on_visionos"] = self.processInfoWrapper.isiOSAppOnVisionOS + if self.processInfoWrapper.isiOSAppOnMac { + deviceData["ios_app_on_macos"] = true + } + if self.processInfoWrapper.isMacCatalystApp { + deviceData["mac_catalyst_app"] = true + } + } + if self.processInfoWrapper.isiOSAppOnVisionOS { + deviceData["ios_app_on_visionos"] = true + } // Set screen dimensions if available setScreenDimensions(&deviceData) diff --git a/Tests/SentryTests/SentryCrash/SentryCrashWrapperTests.swift b/Tests/SentryTests/SentryCrash/SentryCrashWrapperTests.swift index 3cfa98ad509..5a023a482fd 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashWrapperTests.swift +++ b/Tests/SentryTests/SentryCrash/SentryCrashWrapperTests.swift @@ -162,7 +162,8 @@ final class SentryCrashWrapperTests: XCTestCase { let deviceContext = try XCTUnwrap(testScope.contextDictionary["device"] as? [String: Any]) XCTAssertEqual(deviceContext["ios_app_on_macos"] as? Bool, true) - XCTAssertEqual(deviceContext["mac_catalyst_app"] as? Bool, false) + // When false, the flag is not included in the dictionary to reduce payload size + XCTAssertNil(deviceContext["mac_catalyst_app"]) } @available(macOS 12.0, *) @@ -192,7 +193,8 @@ final class SentryCrashWrapperTests: XCTestCase { crashWrapper.enrichScope(testScope) let deviceContext = try XCTUnwrap(testScope.contextDictionary["device"] as? [String: Any]) - XCTAssertEqual(deviceContext["ios_app_on_macos"] as? Bool, false) + // When false, the flag is not included in the dictionary to reduce payload size + XCTAssertNil(deviceContext["ios_app_on_macos"]) XCTAssertEqual(deviceContext["mac_catalyst_app"] as? Bool, true) } @@ -223,4 +225,39 @@ final class SentryCrashWrapperTests: XCTestCase { let deviceContext = try XCTUnwrap(testScope.contextDictionary["device"] as? [String: Any]) XCTAssertEqual(deviceContext["ios_app_on_visionos"] as? Bool, true) } + + @available(macOS 12.0, *) + func testEnrichScope_DeviceContext_FlagsNotIncludedWhenFalse() throws { + // Verify that boolean flags are not included when they are false to reduce payload size + let mockProcessInfo = MockSentryProcessInfo() + mockProcessInfo.overrides.isiOSAppOnMac = false + mockProcessInfo.overrides.isMacCatalystApp = false + mockProcessInfo.overrides.isiOSAppOnVisionOS = false + + let testScope = Scope() + let crashWrapper = SentryCrashWrapper(processInfoWrapper: mockProcessInfo, systemInfo: [ + "osVersion": "23A344", + "kernelVersion": "23.0.0", + "isJailbroken": false, + "systemName": "iOS", + "cpuArchitecture": "arm64", + "machine": "iPhone14,2", + "model": "iPhone 13 Pro", + "freeMemorySize": UInt64(1_073_741_824), + "usableMemorySize": UInt64(4_294_967_296), + "memorySize": UInt64(6_442_450_944), + "appStartTime": "2023-01-01T12:00:00Z", + "deviceAppHash": "abc123", + "appID": "12345", + "buildType": "debug" + ]) + + crashWrapper.enrichScope(testScope) + + let deviceContext = try XCTUnwrap(testScope.contextDictionary["device"] as? [String: Any]) + // All flags should be absent when false + XCTAssertNil(deviceContext["ios_app_on_macos"]) + XCTAssertNil(deviceContext["mac_catalyst_app"]) + XCTAssertNil(deviceContext["ios_app_on_visionos"]) + } }