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