diff --git a/CHANGELOG.md b/CHANGELOG.md index 6318d6a1d2..77a405b381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add isiOSAppOnVisionOS, isiOSAppOnMac, isMacCatalystApp to device context #6939 + ### Fixes - The transport now correctly discard envelopes on 4xx and 5xx responses and records client reports `send_error` (#6618) This also fixes edge cases in which the SDK kept retrying sending a faulty envelope until the offline cache overflowed. diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 1edbd2c84d..aecc858654 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -820,6 +820,7 @@ D4D7AA782EEAE30F00E28DFB /* BatcherScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D7AA772EEAE30B00E28DFB /* BatcherScopeTests.swift */; }; D4DDC0F42EE8572F00F321F6 /* BatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DDC0F32EE8572F00F321F6 /* BatcherTests.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 */; }; @@ -2197,6 +2198,7 @@ D4D7AA772EEAE30B00E28DFB /* BatcherScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatcherScopeTests.swift; sourceTree = ""; }; D4DDC0F32EE8572F00F321F6 /* BatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatcherTests.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 = ""; }; @@ -3603,6 +3605,7 @@ 7BD7299B24654CD500EA3610 /* Helper */ = { isa = PBXGroup; children = ( + D4E1CE642EDDCBD300D2EAC1 /* SentryProcessInfoTests.swift */, D4A0C22A2E9E3CE100791353 /* InfoPlist */, F4A930242E661856006DA6EF /* SentryMobileProvisionParserTests.swift */, D4F7BD7C2E4373BB004A2D77 /* SentryLevelMapperTests.swift */, @@ -6344,6 +6347,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/Sources/TestSentryNSProcessInfoWrapper.swift b/SentryTestUtils/Sources/TestSentryNSProcessInfoWrapper.swift index 30d189c779..c4e411cf49 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 180b0b8f4f..a04aad8744 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,26 @@ 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 + // + // 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")) { + // 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 + // 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 8c07195307..167ba340e4 100644 --- a/Sources/Swift/SentryCrash/SentryCrashWrapper.swift +++ b/Sources/Swift/SentryCrash/SentryCrashWrapper.swift @@ -170,6 +170,12 @@ 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) diff --git a/Tests/SentryTests/Helper/SentryProcessInfoTests.swift b/Tests/SentryTests/Helper/SentryProcessInfoTests.swift new file mode 100644 index 0000000000..8dc580ee0a --- /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/SentryCrash/SentryCrashWrapperTests.swift b/Tests/SentryTests/SentryCrash/SentryCrashWrapperTests.swift index d1f34aaf6e..e2fd10e178 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 { @@ -132,4 +133,94 @@ final class SentryCrashWrapperTests: XCTestCase { 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) + } }