feat: [SDK-5106] report host app build toolchain on iOS logs - #1728
Conversation
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 <cursoragent@cursor.com>
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 <cursoragent@cursor.com>
|
Lets see what nan says but maybe we can drop DTXcodeBuild? |
|
Could we fall back to for key in ["MinimumOSVersion", "LSMinimumSystemVersion"] {
guard let value = infoDictionary?[key] as? String, !value.isEmpty else {
continue
}
attributes["minimum_os_version"] = value
break
} |
|
|
This is ok, nothing in the extension or outcomes reference remote logging. Even if we did it would be okay because the same xcode builds everything in general |
I agree on trimming this down and recommend we just add 3 new fields as everything else is just noise based on looking at the full picture of labels we get below. For example,
Here's a snippet of the payload |
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 <cursoragent@cursor.com>
|
Pushed Kept (3 new + 1 pre-existing):
Dropped: The payload sample made the redundancy hard to argue with — Catalyst fallback (@fadi-george): added with your ordering, NSE / Also renamed
Tests: 107 passing, 0 failures, against the pinned KMP submodule. Net -47/+61 across the provider and its tests. |
|
Maybe a create a linear ticket for this and add to pr title. |
|
Created SDK-5106 and put it In Review. Title updated to match. |
|
|
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 <cursoragent@cursor.com>
|
Agreed — Pushed
|
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 <cursoragent@cursor.com>
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 <cursoragent@cursor.com>
Summary
Linear: SDK-5106
OSLoggerPlatformProvidershippedswiftVersion,kotlinVersion, andadditionalVersionAttributesas placeholders (nil/ Catalyst-only), so iOS logs carried nothing about the toolchain that built the host app.LogFieldsusesputIfValueNotBlank, so those attributes were silently absent from every record rather than failing loudly.This reads Xcode's
DT*keys and the declared deployment target from the running executable'sInfo.plistviaBundle.main.Why not a compile-time check.
#if swift(>=6.0)inside SDK source is evaluated when we build the SDK. Since we distribute a prebuilt XCFramework (podspecs vendOneSignalCore.xcframework,Package.swiftuses binary targets), that value freezes at our build time and is identical for every customer — it would duplicateossdk.sdk_base_versionand tell you nothing about the app. ReadingBundle.maindescribes the customer's build and varies per app.Attributes added
Emitted as
ossdk.<key>resource attributes (attached once per resource, not per record):xcode_versionDTXcode, decoded26.2xcode_buildDTXcodeBuild17C52minimum_os_versionMinimumOSVersion12.0minimum_macos_versionLSMinimumSystemVersion(Catalyst / macOS)13.1apple_platform(mac_catalyst) is pre-existing and stays.DTXcodeis packed ("2620"= 26.2) so it is decoded rather than shipped raw. Blank and missing keys are dropped rather than emitted empty.LSMinimumSystemVersionis a macOS version, so it is not folded intominimum_os_version— a query for hosts below iOS 15 would otherwise include Catalyst apps pinned to macOS 13. Both keys can be present and are emitted independently.Review trimmed
build_compiler,build_platform_*, andbuild_sdk_*— they duplicated existingdevice.*/os.*fields or each other.Why
minimum_os_versionmattersThese attributes exist to answer support questions, not for general telemetry, and this is the one that answers "can we raise our deployment target?"
It is deliberately distinct from
os.version, which is already emitted.os.versionis what a customer's users are running;minimum_os_versionis what their build is pinned to. A customer can serve 100% iOS 18 traffic while still declaring min iOS 13 — and raising our deployment target breaks their build regardless of where their users are. #1724 just raised our minimum to iOS 12; this is the field that would show who that affects before the next bump.Why
swiftVersionstays nilAn earlier revision of this PR approximated
swiftVersionfrom the Xcode version via a lookup table. That has been dropped, and the field is leftnil. Three reasons:1. It is redundant with
xcode_version. Xcode determines the Swift compiler 1:1 — there is no Xcode 16 shipping a Swift 5.9 compiler. Anything derived from the Xcode version is a lossy re-encoding of a field we already emit exactly, and can only ever be less accurate than the value sitting next to it.2. The one thing a distinct value could carry, this approach cannot capture. A
swift_versionmeaningfully different from the Xcode version would be the language mode — the per-targetSWIFT_VERSIONbuild setting, 5 vs 6. That would answer a real question ("if we adopt Swift 6 strict concurrency in our public API, do customers still building in Swift 5 mode break?"). But language mode is not in the hostInfo.plistat all, and an approximation derived from the compiler reports the wrong thing for exactly that case. A field that looks like it answers the question and does not is worse than an absent field.3. The iOS compatibility cliff is not shaped like Kotlin's. On Android,
kotlin_versiongates a real hazard: Kotlin metadata is forward-incompatible, so a library compiled with Kotlin 2.x cannot be consumed by a 1.9 compiler, making the customer's Kotlin version a hard blocker on bumping ours. iOS has no equivalent, because we build every target withBUILD_LIBRARY_FOR_DISTRIBUTION = YESand therefore ship a module-stable.swiftinterface, which newer compilers consume fine. The practical gate is "customer's Xcode >= ours", andxcode_versionanswers that directly.Worth noting why Android can do this and we cannot:
KotlinVersion.CURRENTworks becausekotlin-stdlibis an ordinary dependency bundled into the APK, so the version travels with the app as readable data. Swift's runtime has been ABI-stable and OS-provided since Swift 5, so there is no app-bundled artifact carrying a language version — anything readable at runtime describes the OS's Swift runtime, which already tracksos.version.kotlinVersionlikewise staysnil: the protocol scopes it to a Kotlin host app, which iOS is not. The shared module's own provenance is already onossdk.kmp_version.Test plan
OneSignalOSCoreTestsgreen againstmainwith KMP at the pinnedv0.3.0OSLoggerHostBuildAttributesTestscoveringDTXcodedigit unpacking, key mapping includingminimum_os_version/minimum_macos_version, and omission of blank/missing keysswiftlintclean on the changed source fileossdk.*attributes land in GCP Logs Explorer on a real device buildVerify on device with:
Related: SDK-5106, SDK-4999 (KMP contract), SDK-5081 (bug bash covering remote logging + feature flags).