From bad6cc04f62c522f7f0e184568340888189f9857 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Wed, 26 Aug 2026 11:29:14 -0500 Subject: [PATCH 1/6] feat: report host app build toolchain on iOS logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The logger shipped `swiftVersion` and `additionalVersionAttributes` as placeholders, so iOS logs carried no signal about the toolchain that built the host app. Read Xcode's `DT*` keys from the host's Info.plist instead of a compile-time check: the SDK ships as a prebuilt XCFramework, so `#if swift(...)` here would describe OneSignal's build machine and be identical for every customer. Emits xcode_version, xcode_build, build_compiler, build_platform_*, and build_sdk_* as ossdk.* resource attributes. Apple exposes no runtime API for the Swift language version, so swiftVersion is approximated from the Xcode version via a floor lookup; the exact xcode_version rides alongside so a stale row stays recoverable downstream. kotlinVersion stays nil — it describes a Kotlin host app, which iOS is not. Co-authored-by: Cursor --- .../Logging/OSLoggerPlatformProvider.swift | 107 ++++++++++++++++-- .../OSLoggerAdaptersTests.swift | 56 +++++++++ 2 files changed, 155 insertions(+), 8 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift index 72f10bc26..80c5c9555 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift @@ -51,6 +51,9 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { static let deviceManufacturer = "Apple" static let unknown = "unknown" static let osBuildName = "kern.osversion" + static let xcodeVersionInfoKey = "DTXcode" + static let xcodeVersionAttribute = "xcode_version" + static let macCatalystAttribute = "apple_platform" static let disabledLogLevel = "NONE" static let crashDirectoryComponents = ["onesignal", "logger", "crashes"] @@ -107,15 +110,20 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { let osBuildId = OSLoggerPlatformProvider.systemValue(named: Constants.osBuildName) let sdkWrapper = OneSignalWrapper.sdkType let sdkWrapperVersion = OneSignalWrapper.sdkVersion + /// Nil on iOS: this describes the *host app's* Kotlin stack, and an iOS app is + /// not a Kotlin host. The shared module's own provenance rides on + /// `ossdk.kmp_version` instead. let kotlinVersion: String? = nil - let swiftVersion: String? = nil - let additionalVersionAttributes: [String: String] = { - #if targetEnvironment(macCatalyst) - return ["apple_platform": "mac_catalyst"] - #else - return [:] - #endif - }() + + /// Approximated from the host app's Xcode version. Apple ships one Swift + /// toolchain per Xcode release and exposes no runtime API for the language + /// version, so this is the closest signal available. `xcode_version` is emitted + /// alongside and is exact, so a stale row in the lookup table stays recoverable. + let swiftVersion: String? = OSLoggerPlatformProvider.hostXcodeVersion() + .flatMap(OSLoggerPlatformProvider.approximateSwiftVersion(forXcodeVersion:)) + + let additionalVersionAttributes: [String: String] = + OSLoggerPlatformProvider.hostBuildAttributes() var enabledFeatureFlags: [String] { featureFlagsProvider() } @@ -189,6 +197,89 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { let apiBaseUrl = OS_API_SERVER_URL.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + /// Xcode stamps its own version, the compiler, and the SDK it built against + /// into the *host app's* `Info.plist`. Reading `Bundle.main` is deliberate: + /// this SDK ships as a prebuilt XCFramework, so a compile-time check here would + /// describe OneSignal's build machine and be identical for every customer. + /// + /// These are build-time facts. The OS actually running is reported separately + /// as `os.name` / `os.version` / `os.build_id`. + private static let buildMetadataKeys: [(infoKey: String, attribute: String)] = [ + ("DTXcodeBuild", "xcode_build"), + ("DTCompiler", "build_compiler"), + ("DTPlatformName", "build_platform_name"), + ("DTPlatformVersion", "build_platform_version"), + ("DTPlatformBuild", "build_platform_build"), + ("DTSDKName", "build_sdk_name"), + ("DTSDKBuild", "build_sdk_build") + ] + + /// Floor lookup, so an Xcode newer than every row resolves to the most recent + /// known Swift release rather than dropping the attribute. Add a row whenever + /// an Xcode release ships a new Swift version. + private static let swiftVersionByXcode: [(xcode: (major: Int, minor: Int), swift: String)] = [ + ((14, 0), "5.7"), + ((14, 3), "5.8"), + ((15, 0), "5.9"), + ((15, 3), "5.10"), + ((16, 0), "6.0"), + ((16, 3), "6.1"), + ((26, 0), "6.2") + ] + + static func hostBuildAttributes( + infoDictionary: [String: Any]? = Bundle.main.infoDictionary + ) -> [String: String] { + var attributes: [String: String] = [:] + #if targetEnvironment(macCatalyst) + attributes[Constants.macCatalystAttribute] = "mac_catalyst" + #endif + if let xcodeVersion = hostXcodeVersion(infoDictionary: infoDictionary) { + attributes[Constants.xcodeVersionAttribute] = xcodeVersion + } + for (infoKey, attribute) in buildMetadataKeys { + guard let value = infoDictionary?[infoKey] as? String, !value.isEmpty else { + continue + } + attributes[attribute] = value + } + return attributes + } + + static func hostXcodeVersion( + infoDictionary: [String: Any]? = Bundle.main.infoDictionary + ) -> String? { + guard let raw = infoDictionary?[Constants.xcodeVersionInfoKey] as? String else { + return nil + } + return decodeXcodeVersion(raw) + } + + /// `DTXcode` packs major/minor/patch into digits: `"2620"` is 26.2, `"0900"` is 9.0. + static func decodeXcodeVersion(_ raw: String) -> String? { + guard let packed = Int(raw.trimmingCharacters(in: .whitespaces)), packed > 0 else { + return nil + } + let major = packed / 100 + let minor = (packed / 10) % 10 + let patch = packed % 10 + return patch == 0 ? "\(major).\(minor)" : "\(major).\(minor).\(patch)" + } + + static func approximateSwiftVersion(forXcodeVersion version: String) -> String? { + let components = version.split(separator: ".").compactMap { Int($0) } + guard let major = components.first else { + return nil + } + let minor = components.count > 1 ? components[1] : 0 + var resolved: String? + for entry in swiftVersionByXcode + where (entry.xcode.major, entry.xcode.minor) <= (major, minor) { + resolved = entry.swift + } + return resolved + } + private static func systemValue(named name: String) -> String { var size = 0 guard sysctlbyname(name, nil, &size, nil, 0) == 0, size > 0 else { diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift index 0b2f2c433..e99e564c7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift @@ -428,6 +428,62 @@ final class OSLoggerAdaptersTests: XCTestCase { } } +/// Host build metadata read from the app's `Info.plist`, kept separate from the +/// adapter tests above so neither class outgrows the lint limit. +final class OSLoggerHostBuildAttributesTests: XCTestCase { + func testDecodeXcodeVersionUnpacksDTXcodeDigits() { + XCTAssertEqual(OSLoggerPlatformProvider.decodeXcodeVersion("2620"), "26.2") + XCTAssertEqual(OSLoggerPlatformProvider.decodeXcodeVersion("1600"), "16.0") + XCTAssertEqual(OSLoggerPlatformProvider.decodeXcodeVersion("0900"), "9.0") + XCTAssertEqual(OSLoggerPlatformProvider.decodeXcodeVersion("1632"), "16.3.2") + XCTAssertNil(OSLoggerPlatformProvider.decodeXcodeVersion("")) + XCTAssertNil(OSLoggerPlatformProvider.decodeXcodeVersion("not-a-number")) + } + + func testApproximateSwiftVersionFloorsToNearestKnownXcode() { + let approximate = OSLoggerPlatformProvider.approximateSwiftVersion(forXcodeVersion:) + + XCTAssertEqual(approximate("16.0"), "6.0") + XCTAssertEqual(approximate("16.3"), "6.1") + // Unlisted release between known rows floors to the row below it. + XCTAssertEqual(approximate("16.4"), "6.1") + // Newer than every row keeps the last known value rather than dropping. + XCTAssertEqual(approximate("99.9"), "6.2") + // Older than every row has nothing sensible to report. + XCTAssertNil(approximate("13.4")) + XCTAssertNil(approximate("")) + } + + func testHostBuildAttributesMapsInfoPlistKeys() { + let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [ + "DTXcode": "2620", + "DTXcodeBuild": "17C52", + "DTCompiler": "com.apple.compilers.llvm.clang.1_0", + "DTPlatformName": "iphonesimulator", + "DTPlatformVersion": "26.2", + "DTSDKName": "iphonesimulator26.2", + "DTSDKBuild": "" + ]) + + XCTAssertEqual(attributes["xcode_version"], "26.2") + XCTAssertEqual(attributes["xcode_build"], "17C52") + XCTAssertEqual(attributes["build_compiler"], "com.apple.compilers.llvm.clang.1_0") + XCTAssertEqual(attributes["build_platform_name"], "iphonesimulator") + XCTAssertEqual(attributes["build_platform_version"], "26.2") + XCTAssertEqual(attributes["build_sdk_name"], "iphonesimulator26.2") + // Blank and absent values are omitted rather than emitted empty. + XCTAssertNil(attributes["build_sdk_build"]) + XCTAssertNil(attributes["build_platform_build"]) + } + + func testHostBuildAttributesOmitsXcodeVersionWhenKeyMissing() { + let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [:]) + + XCTAssertNil(attributes["xcode_version"]) + XCTAssertNil(attributes["xcode_build"]) + } +} + private final class LoggerAdapterListener: NSObject, OSLogListener { var levels: [ONE_S_LOG_LEVEL] = [] From 1e7d3faa8bfdc5f95cd73bb958ae6010e810aa3f Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Wed, 26 Aug 2026 12:10:25 -0500 Subject: [PATCH 2/6] refactor: drop approximated swift version, report deployment target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit swiftVersion was approximated from the host's Xcode version, which is redundant: Xcode determines the Swift compiler 1:1, so the derived value only re-encodes xcode_version less precisely. The one thing a distinct value could carry is the per-target language mode (SWIFT_VERSION, 5 vs 6), and that is not in the host Info.plist at all — so the approximation reported the wrong thing for exactly the case where it would have been useful. Unlike Kotlin metadata, a module-stable .swiftinterface (we build every target with BUILD_LIBRARY_FOR_DISTRIBUTION) is consumed fine by newer compilers, so the support question is "is their Xcode at least ours", which xcode_version answers. Add minimum_os_version from MinimumOSVersion instead. That is the field that gates raising our own deployment target: os.version says what a customer's users run, while the declared minimum says what their build is pinned to, and only the latter breaks when we bump. Co-authored-by: Cursor --- .../Logging/OSLoggerPlatformProvider.swift | 59 ++++++------------- .../OSLoggerAdaptersTests.swift | 17 +----- 2 files changed, 22 insertions(+), 54 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift index 80c5c9555..0d36c3edd 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift @@ -115,12 +115,13 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { /// `ossdk.kmp_version` instead. let kotlinVersion: String? = nil - /// Approximated from the host app's Xcode version. Apple ships one Swift - /// toolchain per Xcode release and exposes no runtime API for the language - /// version, so this is the closest signal available. `xcode_version` is emitted - /// alongside and is exact, so a stale row in the lookup table stays recoverable. - let swiftVersion: String? = OSLoggerPlatformProvider.hostXcodeVersion() - .flatMap(OSLoggerPlatformProvider.approximateSwiftVersion(forXcodeVersion:)) + /// Nil on iOS. Apple exposes no runtime API for the Swift language version and + /// there is no `DTSwiftVersion` key, so the only derivable value would come from + /// the Xcode version — which maps to a Swift compiler 1:1 and is already emitted + /// exactly as `xcode_version`. The one thing a distinct value could carry is the + /// per-target language mode (`SWIFT_VERSION`, 5 vs 6), and that is not in the + /// host's `Info.plist` at all. + let swiftVersion: String? = nil let additionalVersionAttributes: [String: String] = OSLoggerPlatformProvider.hostBuildAttributes() @@ -197,13 +198,17 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { let apiBaseUrl = OS_API_SERVER_URL.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - /// Xcode stamps its own version, the compiler, and the SDK it built against - /// into the *host app's* `Info.plist`. Reading `Bundle.main` is deliberate: - /// this SDK ships as a prebuilt XCFramework, so a compile-time check here would - /// describe OneSignal's build machine and be identical for every customer. + /// Xcode stamps its own version, the compiler, and the SDK it built against into + /// the *host app's* `Info.plist`, alongside the deployment target the app + /// declares. Reading `Bundle.main` is deliberate: this SDK ships as a prebuilt + /// XCFramework, so a compile-time check here would describe OneSignal's build + /// machine and be identical for every customer. /// - /// These are build-time facts. The OS actually running is reported separately - /// as `os.name` / `os.version` / `os.build_id`. + /// These are build-time facts and answer what the host *commits to* supporting. + /// The OS actually running is reported separately as `os.name` / `os.version` / + /// `os.build_id`, and the two diverge: an app can serve only iOS 18 users while + /// still declaring a much older `minimum_os_version`, which is what constrains + /// raising our own deployment target. private static let buildMetadataKeys: [(infoKey: String, attribute: String)] = [ ("DTXcodeBuild", "xcode_build"), ("DTCompiler", "build_compiler"), @@ -211,20 +216,8 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { ("DTPlatformVersion", "build_platform_version"), ("DTPlatformBuild", "build_platform_build"), ("DTSDKName", "build_sdk_name"), - ("DTSDKBuild", "build_sdk_build") - ] - - /// Floor lookup, so an Xcode newer than every row resolves to the most recent - /// known Swift release rather than dropping the attribute. Add a row whenever - /// an Xcode release ships a new Swift version. - private static let swiftVersionByXcode: [(xcode: (major: Int, minor: Int), swift: String)] = [ - ((14, 0), "5.7"), - ((14, 3), "5.8"), - ((15, 0), "5.9"), - ((15, 3), "5.10"), - ((16, 0), "6.0"), - ((16, 3), "6.1"), - ((26, 0), "6.2") + ("DTSDKBuild", "build_sdk_build"), + ("MinimumOSVersion", "minimum_os_version") ] static func hostBuildAttributes( @@ -266,20 +259,6 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { return patch == 0 ? "\(major).\(minor)" : "\(major).\(minor).\(patch)" } - static func approximateSwiftVersion(forXcodeVersion version: String) -> String? { - let components = version.split(separator: ".").compactMap { Int($0) } - guard let major = components.first else { - return nil - } - let minor = components.count > 1 ? components[1] : 0 - var resolved: String? - for entry in swiftVersionByXcode - where (entry.xcode.major, entry.xcode.minor) <= (major, minor) { - resolved = entry.swift - } - return resolved - } - private static func systemValue(named name: String) -> String { var size = 0 guard sysctlbyname(name, nil, &size, nil, 0) == 0, size > 0 else { diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift index e99e564c7..00eb084e9 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift @@ -440,20 +440,6 @@ final class OSLoggerHostBuildAttributesTests: XCTestCase { XCTAssertNil(OSLoggerPlatformProvider.decodeXcodeVersion("not-a-number")) } - func testApproximateSwiftVersionFloorsToNearestKnownXcode() { - let approximate = OSLoggerPlatformProvider.approximateSwiftVersion(forXcodeVersion:) - - XCTAssertEqual(approximate("16.0"), "6.0") - XCTAssertEqual(approximate("16.3"), "6.1") - // Unlisted release between known rows floors to the row below it. - XCTAssertEqual(approximate("16.4"), "6.1") - // Newer than every row keeps the last known value rather than dropping. - XCTAssertEqual(approximate("99.9"), "6.2") - // Older than every row has nothing sensible to report. - XCTAssertNil(approximate("13.4")) - XCTAssertNil(approximate("")) - } - func testHostBuildAttributesMapsInfoPlistKeys() { let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [ "DTXcode": "2620", @@ -462,6 +448,7 @@ final class OSLoggerHostBuildAttributesTests: XCTestCase { "DTPlatformName": "iphonesimulator", "DTPlatformVersion": "26.2", "DTSDKName": "iphonesimulator26.2", + "MinimumOSVersion": "12.0", "DTSDKBuild": "" ]) @@ -471,6 +458,8 @@ final class OSLoggerHostBuildAttributesTests: XCTestCase { XCTAssertEqual(attributes["build_platform_name"], "iphonesimulator") XCTAssertEqual(attributes["build_platform_version"], "26.2") XCTAssertEqual(attributes["build_sdk_name"], "iphonesimulator26.2") + // The host's declared deployment target, not the OS it happens to run on. + XCTAssertEqual(attributes["minimum_os_version"], "12.0") // Blank and absent values are omitted rather than emitted empty. XCTAssertNil(attributes["build_sdk_build"]) XCTAssertNil(attributes["build_platform_build"]) From 45a0eac819b8129965500a3e8867b1a514dfcc7b Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Wed, 26 Aug 2026 16:54:09 -0500 Subject: [PATCH 3/6] refactor: trim build attributes to the three that carry information Review found most of the emitted set was noise. DTCompiler has been the same constant on effectively every app since 2011; build_sdk_name is just build_platform_name and build_platform_version concatenated; build_platform_build matches build_sdk_build in practice; and build_platform_name duplicates what device.model.identifier already says. Drops all six, leaving xcode_version, xcode_build, and minimum_os_version alongside the pre-existing apple_platform. Fall back to LSMinimumSystemVersion when MinimumOSVersion is absent, so Catalyst hosts report a deployment target instead of omitting the field. Rename macCatalystAttribute to applePlatformAttribute so the constant matches the attribute it holds, and cut the doc comments back to the reasoning that is not evident from the code. Co-authored-by: Cursor --- .../Logging/OSLoggerPlatformProvider.swift | 66 +++++++++---------- .../OSLoggerAdaptersTests.swift | 42 ++++++++---- 2 files changed, 61 insertions(+), 47 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift index 0d36c3edd..ee85be188 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift @@ -53,7 +53,13 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { static let osBuildName = "kern.osversion" static let xcodeVersionInfoKey = "DTXcode" static let xcodeVersionAttribute = "xcode_version" - static let macCatalystAttribute = "apple_platform" + static let xcodeBuildInfoKey = "DTXcodeBuild" + static let xcodeBuildAttribute = "xcode_build" + /// Catalyst bundles declare their floor as `LSMinimumSystemVersion`; iOS uses + /// `MinimumOSVersion`. First match wins. + static let minimumOSVersionInfoKeys = ["MinimumOSVersion", "LSMinimumSystemVersion"] + static let minimumOSVersionAttribute = "minimum_os_version" + static let applePlatformAttribute = "apple_platform" static let disabledLogLevel = "NONE" static let crashDirectoryComponents = ["onesignal", "logger", "crashes"] @@ -115,12 +121,8 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { /// `ossdk.kmp_version` instead. let kotlinVersion: String? = nil - /// Nil on iOS. Apple exposes no runtime API for the Swift language version and - /// there is no `DTSwiftVersion` key, so the only derivable value would come from - /// the Xcode version — which maps to a Swift compiler 1:1 and is already emitted - /// exactly as `xcode_version`. The one thing a distinct value could carry is the - /// per-target language mode (`SWIFT_VERSION`, 5 vs 6), and that is not in the - /// host's `Info.plist` at all. + /// Nil on iOS: there is no runtime API for the Swift language version, and anything + /// derivable would just re-encode `xcode_version` less precisely. let swiftVersion: String? = nil let additionalVersionAttributes: [String: String] = @@ -198,47 +200,43 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { let apiBaseUrl = OS_API_SERVER_URL.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - /// Xcode stamps its own version, the compiler, and the SDK it built against into - /// the *host app's* `Info.plist`, alongside the deployment target the app - /// declares. Reading `Bundle.main` is deliberate: this SDK ships as a prebuilt - /// XCFramework, so a compile-time check here would describe OneSignal's build - /// machine and be identical for every customer. + /// Toolchain and deployment target of the running executable, read from its + /// `Info.plist`. Read at runtime rather than with `#if swift(...)` because we ship a + /// prebuilt XCFramework — a compile-time check would describe OneSignal's build + /// machine, identically for every customer. /// - /// These are build-time facts and answer what the host *commits to* supporting. - /// The OS actually running is reported separately as `os.name` / `os.version` / - /// `os.build_id`, and the two diverge: an app can serve only iOS 18 users while - /// still declaring a much older `minimum_os_version`, which is what constrains - /// raising our own deployment target. - private static let buildMetadataKeys: [(infoKey: String, attribute: String)] = [ - ("DTXcodeBuild", "xcode_build"), - ("DTCompiler", "build_compiler"), - ("DTPlatformName", "build_platform_name"), - ("DTPlatformVersion", "build_platform_version"), - ("DTPlatformBuild", "build_platform_build"), - ("DTSDKName", "build_sdk_name"), - ("DTSDKBuild", "build_sdk_build"), - ("MinimumOSVersion", "minimum_os_version") - ] - + /// `minimum_os_version` is what the host *commits to* supporting, which is the fact + /// that gates raising our own deployment target. The OS actually running is separate + /// (`os.name` / `os.version` / `os.build_id`) and routinely diverges from it. static func hostBuildAttributes( infoDictionary: [String: Any]? = Bundle.main.infoDictionary ) -> [String: String] { var attributes: [String: String] = [:] #if targetEnvironment(macCatalyst) - attributes[Constants.macCatalystAttribute] = "mac_catalyst" + attributes[Constants.applePlatformAttribute] = "mac_catalyst" #endif if let xcodeVersion = hostXcodeVersion(infoDictionary: infoDictionary) { attributes[Constants.xcodeVersionAttribute] = xcodeVersion } - for (infoKey, attribute) in buildMetadataKeys { - guard let value = infoDictionary?[infoKey] as? String, !value.isEmpty else { - continue - } - attributes[attribute] = value + if let xcodeBuild = infoValue(Constants.xcodeBuildInfoKey, in: infoDictionary) { + attributes[Constants.xcodeBuildAttribute] = xcodeBuild + } + if let minimumOSVersion = Constants.minimumOSVersionInfoKeys + .lazy + .compactMap({ infoValue($0, in: infoDictionary) }) + .first { + attributes[Constants.minimumOSVersionAttribute] = minimumOSVersion } return attributes } + private static func infoValue(_ key: String, in infoDictionary: [String: Any]?) -> String? { + guard let value = infoDictionary?[key] as? String, !value.isEmpty else { + return nil + } + return value + } + static func hostXcodeVersion( infoDictionary: [String: Any]? = Bundle.main.infoDictionary ) -> String? { diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift index 00eb084e9..3515cd0dc 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift @@ -444,32 +444,48 @@ final class OSLoggerHostBuildAttributesTests: XCTestCase { let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [ "DTXcode": "2620", "DTXcodeBuild": "17C52", + "MinimumOSVersion": "12.0", + // Present in every built Info.plist but deliberately not emitted: each is + // either constant across all apps or derivable from the fields above. "DTCompiler": "com.apple.compilers.llvm.clang.1_0", "DTPlatformName": "iphonesimulator", "DTPlatformVersion": "26.2", + "DTPlatformBuild": "23C53", "DTSDKName": "iphonesimulator26.2", - "MinimumOSVersion": "12.0", - "DTSDKBuild": "" + "DTSDKBuild": "23C53" ]) XCTAssertEqual(attributes["xcode_version"], "26.2") XCTAssertEqual(attributes["xcode_build"], "17C52") - XCTAssertEqual(attributes["build_compiler"], "com.apple.compilers.llvm.clang.1_0") - XCTAssertEqual(attributes["build_platform_name"], "iphonesimulator") - XCTAssertEqual(attributes["build_platform_version"], "26.2") - XCTAssertEqual(attributes["build_sdk_name"], "iphonesimulator26.2") // The host's declared deployment target, not the OS it happens to run on. XCTAssertEqual(attributes["minimum_os_version"], "12.0") - // Blank and absent values are omitted rather than emitted empty. - XCTAssertNil(attributes["build_sdk_build"]) - XCTAssertNil(attributes["build_platform_build"]) + XCTAssertEqual(attributes.count, 3) + } + + func testHostBuildAttributesFallsBackToCatalystMinimumSystemVersion() { + let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [ + "LSMinimumSystemVersion": "13.1" + ]) + + XCTAssertEqual(attributes["minimum_os_version"], "13.1") + } + + func testHostBuildAttributesPrefersMinimumOSVersionOverCatalystKey() { + let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [ + "MinimumOSVersion": "12.0", + "LSMinimumSystemVersion": "13.1" + ]) + + XCTAssertEqual(attributes["minimum_os_version"], "12.0") } - func testHostBuildAttributesOmitsXcodeVersionWhenKeyMissing() { - let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [:]) + func testHostBuildAttributesOmitsBlankAndMissingValues() { + let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [ + "DTXcodeBuild": "", + "MinimumOSVersion": "" + ]) - XCTAssertNil(attributes["xcode_version"]) - XCTAssertNil(attributes["xcode_build"]) + XCTAssertTrue(attributes.isEmpty) } } From 70952b7d543b71694b4799ed601f9cfb82684e95 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Thu, 27 Aug 2026 12:28:57 -0500 Subject: [PATCH 4/6] fix: emit Catalyst floor as minimum_macos_version LSMinimumSystemVersion is a macOS version. Folding it into minimum_os_version mixes it with iOS deployment targets, so a query for hosts below iOS 15 would include Catalyst apps pinned to macOS 13. Emit it as its own attribute; both keys can coexist. Co-authored-by: Cursor --- .../Logging/OSLoggerPlatformProvider.swift | 27 ++++++++++++------- .../OSLoggerAdaptersTests.swift | 11 +++++--- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift index ee85be188..a205ba880 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift @@ -55,10 +55,12 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { static let xcodeVersionAttribute = "xcode_version" static let xcodeBuildInfoKey = "DTXcodeBuild" static let xcodeBuildAttribute = "xcode_build" - /// Catalyst bundles declare their floor as `LSMinimumSystemVersion`; iOS uses - /// `MinimumOSVersion`. First match wins. - static let minimumOSVersionInfoKeys = ["MinimumOSVersion", "LSMinimumSystemVersion"] + /// iOS / tvOS / watchOS deployment target. Distinct from the macOS floor: + /// `LSMinimumSystemVersion` is a macOS version and must not share this key. + static let minimumOSVersionInfoKey = "MinimumOSVersion" static let minimumOSVersionAttribute = "minimum_os_version" + static let minimumMacOSVersionInfoKey = "LSMinimumSystemVersion" + static let minimumMacOSVersionAttribute = "minimum_macos_version" static let applePlatformAttribute = "apple_platform" static let disabledLogLevel = "NONE" static let crashDirectoryComponents = ["onesignal", "logger", "crashes"] @@ -205,9 +207,11 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { /// prebuilt XCFramework — a compile-time check would describe OneSignal's build /// machine, identically for every customer. /// - /// `minimum_os_version` is what the host *commits to* supporting, which is the fact - /// that gates raising our own deployment target. The OS actually running is separate - /// (`os.name` / `os.version` / `os.build_id`) and routinely diverges from it. + /// `minimum_os_version` is the iOS deployment target the host *commits to*, which + /// is the fact that gates raising our own target. Catalyst's macOS floor is a + /// different namespace (`LSMinimumSystemVersion` → `minimum_macos_version`) and + /// is not mixed in. The OS actually running is separate (`os.name` / `os.version` + /// / `os.build_id`) and routinely diverges from both. static func hostBuildAttributes( infoDictionary: [String: Any]? = Bundle.main.infoDictionary ) -> [String: String] { @@ -221,12 +225,15 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { if let xcodeBuild = infoValue(Constants.xcodeBuildInfoKey, in: infoDictionary) { attributes[Constants.xcodeBuildAttribute] = xcodeBuild } - if let minimumOSVersion = Constants.minimumOSVersionInfoKeys - .lazy - .compactMap({ infoValue($0, in: infoDictionary) }) - .first { + if let minimumOSVersion = infoValue(Constants.minimumOSVersionInfoKey, in: infoDictionary) { attributes[Constants.minimumOSVersionAttribute] = minimumOSVersion } + if let minimumMacOSVersion = infoValue( + Constants.minimumMacOSVersionInfoKey, + in: infoDictionary + ) { + attributes[Constants.minimumMacOSVersionAttribute] = minimumMacOSVersion + } return attributes } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift index 3515cd0dc..dc3f9ad27 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift @@ -462,27 +462,30 @@ final class OSLoggerHostBuildAttributesTests: XCTestCase { XCTAssertEqual(attributes.count, 3) } - func testHostBuildAttributesFallsBackToCatalystMinimumSystemVersion() { + func testHostBuildAttributesEmitsCatalystFloorAsMacOSVersion() { let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [ "LSMinimumSystemVersion": "13.1" ]) - XCTAssertEqual(attributes["minimum_os_version"], "13.1") + XCTAssertEqual(attributes["minimum_macos_version"], "13.1") + XCTAssertNil(attributes["minimum_os_version"]) } - func testHostBuildAttributesPrefersMinimumOSVersionOverCatalystKey() { + func testHostBuildAttributesEmitsIOSAndMacOSFloorsIndependently() { let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [ "MinimumOSVersion": "12.0", "LSMinimumSystemVersion": "13.1" ]) XCTAssertEqual(attributes["minimum_os_version"], "12.0") + XCTAssertEqual(attributes["minimum_macos_version"], "13.1") } func testHostBuildAttributesOmitsBlankAndMissingValues() { let attributes = OSLoggerPlatformProvider.hostBuildAttributes(infoDictionary: [ "DTXcodeBuild": "", - "MinimumOSVersion": "" + "MinimumOSVersion": "", + "LSMinimumSystemVersion": "" ]) XCTAssertTrue(attributes.isEmpty) From c10883f4fb574d8774826fcec46c70c543c33278 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Thu, 27 Aug 2026 12:36:13 -0500 Subject: [PATCH 5/6] refactor: inline one-use host-build attribute names The Info.plist key / wire-name pairs were leftovers from an eight-entry mapping table. After the trim each name is used once, so the Constants aliases added nothing. Co-authored-by: Cursor --- .../Logging/OSLoggerPlatformProvider.swift | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift index a205ba880..cd5732c28 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift @@ -51,17 +51,6 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { static let deviceManufacturer = "Apple" static let unknown = "unknown" static let osBuildName = "kern.osversion" - static let xcodeVersionInfoKey = "DTXcode" - static let xcodeVersionAttribute = "xcode_version" - static let xcodeBuildInfoKey = "DTXcodeBuild" - static let xcodeBuildAttribute = "xcode_build" - /// iOS / tvOS / watchOS deployment target. Distinct from the macOS floor: - /// `LSMinimumSystemVersion` is a macOS version and must not share this key. - static let minimumOSVersionInfoKey = "MinimumOSVersion" - static let minimumOSVersionAttribute = "minimum_os_version" - static let minimumMacOSVersionInfoKey = "LSMinimumSystemVersion" - static let minimumMacOSVersionAttribute = "minimum_macos_version" - static let applePlatformAttribute = "apple_platform" static let disabledLogLevel = "NONE" static let crashDirectoryComponents = ["onesignal", "logger", "crashes"] @@ -217,22 +206,23 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { ) -> [String: String] { var attributes: [String: String] = [:] #if targetEnvironment(macCatalyst) - attributes[Constants.applePlatformAttribute] = "mac_catalyst" + attributes["apple_platform"] = "mac_catalyst" #endif if let xcodeVersion = hostXcodeVersion(infoDictionary: infoDictionary) { - attributes[Constants.xcodeVersionAttribute] = xcodeVersion + attributes["xcode_version"] = xcodeVersion } - if let xcodeBuild = infoValue(Constants.xcodeBuildInfoKey, in: infoDictionary) { - attributes[Constants.xcodeBuildAttribute] = xcodeBuild + if let xcodeBuild = infoValue("DTXcodeBuild", in: infoDictionary) { + attributes["xcode_build"] = xcodeBuild } - if let minimumOSVersion = infoValue(Constants.minimumOSVersionInfoKey, in: infoDictionary) { - attributes[Constants.minimumOSVersionAttribute] = minimumOSVersion + if let minimumOSVersion = infoValue("MinimumOSVersion", in: infoDictionary) { + attributes["minimum_os_version"] = minimumOSVersion } + // macOS version — do not fold into minimum_os_version if let minimumMacOSVersion = infoValue( - Constants.minimumMacOSVersionInfoKey, + "LSMinimumSystemVersion", in: infoDictionary ) { - attributes[Constants.minimumMacOSVersionAttribute] = minimumMacOSVersion + attributes["minimum_macos_version"] = minimumMacOSVersion } return attributes } @@ -247,7 +237,7 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { static func hostXcodeVersion( infoDictionary: [String: Any]? = Bundle.main.infoDictionary ) -> String? { - guard let raw = infoDictionary?[Constants.xcodeVersionInfoKey] as? String else { + guard let raw = infoDictionary?["DTXcode"] as? String else { return nil } return decodeXcodeVersion(raw) From 74d56c531ac7250b2db9d1925c3a2218533c81b6 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Thu, 27 Aug 2026 12:43:13 -0500 Subject: [PATCH 6/6] refactor: keep host-build key constants that tests also pin Those plist keys and attribute names are used in production and in OSLoggerHostBuildAttributesTests. Leave the one-site apple_platform literal inlined. Co-authored-by: Cursor --- .../Logging/OSLoggerPlatformProvider.swift | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift index cd5732c28..e697aaff8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift @@ -51,6 +51,14 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { static let deviceManufacturer = "Apple" static let unknown = "unknown" static let osBuildName = "kern.osversion" + static let xcodeVersionInfoKey = "DTXcode" + static let xcodeVersionAttribute = "xcode_version" + static let xcodeBuildInfoKey = "DTXcodeBuild" + static let xcodeBuildAttribute = "xcode_build" + static let minimumOSVersionInfoKey = "MinimumOSVersion" + static let minimumOSVersionAttribute = "minimum_os_version" + static let minimumMacOSVersionInfoKey = "LSMinimumSystemVersion" + static let minimumMacOSVersionAttribute = "minimum_macos_version" static let disabledLogLevel = "NONE" static let crashDirectoryComponents = ["onesignal", "logger", "crashes"] @@ -209,20 +217,20 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { attributes["apple_platform"] = "mac_catalyst" #endif if let xcodeVersion = hostXcodeVersion(infoDictionary: infoDictionary) { - attributes["xcode_version"] = xcodeVersion + attributes[Constants.xcodeVersionAttribute] = xcodeVersion } - if let xcodeBuild = infoValue("DTXcodeBuild", in: infoDictionary) { - attributes["xcode_build"] = xcodeBuild + if let xcodeBuild = infoValue(Constants.xcodeBuildInfoKey, in: infoDictionary) { + attributes[Constants.xcodeBuildAttribute] = xcodeBuild } - if let minimumOSVersion = infoValue("MinimumOSVersion", in: infoDictionary) { - attributes["minimum_os_version"] = minimumOSVersion + if let minimumOSVersion = infoValue(Constants.minimumOSVersionInfoKey, in: infoDictionary) { + attributes[Constants.minimumOSVersionAttribute] = minimumOSVersion } // macOS version — do not fold into minimum_os_version if let minimumMacOSVersion = infoValue( - "LSMinimumSystemVersion", + Constants.minimumMacOSVersionInfoKey, in: infoDictionary ) { - attributes["minimum_macos_version"] = minimumMacOSVersion + attributes[Constants.minimumMacOSVersionAttribute] = minimumMacOSVersion } return attributes } @@ -237,7 +245,7 @@ final class OSLoggerPlatformProvider: ILoggerPlatformProvider { static func hostXcodeVersion( infoDictionary: [String: Any]? = Bundle.main.infoDictionary ) -> String? { - guard let raw = infoDictionary?["DTXcode"] as? String else { + guard let raw = infoDictionary?[Constants.xcodeVersionInfoKey] as? String else { return nil } return decodeXcodeVersion(raw)