Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -2197,6 +2198,7 @@
D4D7AA772EEAE30B00E28DFB /* BatcherScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatcherScopeTests.swift; sourceTree = "<group>"; };
D4DDC0F32EE8572F00F321F6 /* BatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatcherTests.swift; sourceTree = "<group>"; };
D4DEE6582E439B2E00FCA5A9 /* SentryProfileTimeseriesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProfileTimeseriesTests.m; sourceTree = "<group>"; };
D4E1CE642EDDCBD300D2EAC1 /* SentryProcessInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryProcessInfoTests.swift; sourceTree = "<group>"; };
D4E942042E9D1CF300DB7521 /* TestSessionReplayEnvironmentChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSessionReplayEnvironmentChecker.swift; sourceTree = "<group>"; };
D4E9420B2E9D1D7600DB7521 /* TestSessionReplayEnvironmentCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSessionReplayEnvironmentCheckerTests.swift; sourceTree = "<group>"; };
D4ECA3FF2E3CBEDE00C757EA /* SentryDummyPrivateEmptyClass.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDummyPrivateEmptyClass.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3603,6 +3605,7 @@
7BD7299B24654CD500EA3610 /* Helper */ = {
isa = PBXGroup;
children = (
D4E1CE642EDDCBD300D2EAC1 /* SentryProcessInfoTests.swift */,
D4A0C22A2E9E3CE100791353 /* InfoPlist */,
F4A930242E661856006DA6EF /* SentryMobileProvisionParserTests.swift */,
D4F7BD7C2E4373BB004A2D77 /* SentryLevelMapperTests.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -45,4 +46,8 @@
public var isMacCatalystApp: Bool {
return overrides.isMacCatalystApp ?? ProcessInfo.processInfo.isMacCatalystApp
}

public var isiOSAppOnVisionOS: Bool {
return overrides.isiOSAppOnVisionOS ?? ProcessInfo.processInfo.isiOSAppOnVisionOS
}
}
24 changes: 24 additions & 0 deletions Sources/Swift/Helper/SentryProcessInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback incorrectly returns true for native visionOS apps

The fallback detection for isiOSAppOnVisionOS uses NSClassFromString("UIWindowSceneGeometryPreferencesVision") to check if running on visionOS. This class exists on all visionOS builds, so native visionOS apps on older visionOS versions (< 26.1, before the official API exists) will incorrectly return true when they should return false. Since the SDK officially supports visionOS as a native platform (per Package.swift), this could result in incorrect telemetry. A compile-time check like #if os(visionOS) could be used to always return false for native visionOS builds.

Fix in CursorΒ Fix in Web

}
}
6 changes: 6 additions & 0 deletions Sources/Swift/SentryCrash/SentryCrashWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
20 changes: 20 additions & 0 deletions Tests/SentryTests/Helper/SentryProcessInfoTests.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
91 changes: 91 additions & 0 deletions Tests/SentryTests/SentryCrash/SentryCrashWrapperTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@_spi(Private) @testable import Sentry
@_spi(Private) import SentryTestUtils
import XCTest

final class SentryCrashWrapperTests: XCTestCase {
Expand Down Expand Up @@ -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)
}
}
Loading