Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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 @@ -3614,6 +3616,7 @@
7BD7299B24654CD500EA3610 /* Helper */ = {
isa = PBXGroup;
children = (
D4E1CE642EDDCBD300D2EAC1 /* SentryProcessInfoTests.swift */,
D4A0C22A2E9E3CE100791353 /* InfoPlist */,
F4A930242E661856006DA6EF /* SentryMobileProvisionParserTests.swift */,
D4F7BD7C2E4373BB004A2D77 /* SentryLevelMapperTests.swift */,
Expand Down Expand Up @@ -6334,6 +6337,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
}
}
33 changes: 33 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,35 @@
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.
//
// 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
}
Comment thread
philprime marked this conversation as resolved.
}
}
// 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
Comment thread
philprime marked this conversation as resolved.
#else
return false
#endif
Comment thread
philprime marked this conversation as resolved.
}
}
15 changes: 15 additions & 0 deletions Sources/Swift/SentryCrash/SentryCrashWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ 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.
Comment thread
philprime marked this conversation as resolved.
if #available(macOS 12, *) {
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)

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")
}
}
128 changes: 128 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,131 @@ 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)
// When false, the flag is not included in the dictionary to reduce payload size
XCTAssertNil(deviceContext["mac_catalyst_app"])
}

@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])
// 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)
}

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)
}

@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"])
}
}
Loading