Skip to content

feat: [SDK-5106] report host app build toolchain on iOS logs - #1728

Merged
abdulraqeeb33 merged 6 commits into
mainfrom
ar/sdk-ios-host-toolchain-attrs
Aug 27, 2026
Merged

feat: [SDK-5106] report host app build toolchain on iOS logs#1728
abdulraqeeb33 merged 6 commits into
mainfrom
ar/sdk-ios-host-toolchain-attrs

Conversation

@abdulraqeeb33

@abdulraqeeb33 abdulraqeeb33 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Linear: SDK-5106

OSLoggerPlatformProvider shipped swiftVersion, kotlinVersion, and additionalVersionAttributes as placeholders (nil / Catalyst-only), so iOS logs carried nothing about the toolchain that built the host app. LogFields uses putIfValueNotBlank, 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's Info.plist via Bundle.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 vend OneSignalCore.xcframework, Package.swift uses binary targets), that value freezes at our build time and is identical for every customer — it would duplicate ossdk.sdk_base_version and tell you nothing about the app. Reading Bundle.main describes the customer's build and varies per app.

Attributes added

Emitted as ossdk.<key> resource attributes (attached once per resource, not per record):

Attribute Source Example
xcode_version DTXcode, decoded 26.2
xcode_build DTXcodeBuild 17C52
minimum_os_version MinimumOSVersion 12.0
minimum_macos_version LSMinimumSystemVersion (Catalyst / macOS) 13.1

apple_platform (mac_catalyst) is pre-existing and stays. DTXcode is packed ("2620" = 26.2) so it is decoded rather than shipped raw. Blank and missing keys are dropped rather than emitted empty.

LSMinimumSystemVersion is a macOS version, so it is not folded into minimum_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_*, and build_sdk_* — they duplicated existing device.* / os.* fields or each other.

Why minimum_os_version matters

These 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.version is what a customer's users are running; minimum_os_version is 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 swiftVersion stays nil

An earlier revision of this PR approximated swiftVersion from the Xcode version via a lookup table. That has been dropped, and the field is left nil. 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_version meaningfully different from the Xcode version would be the language mode — the per-target SWIFT_VERSION build 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 host Info.plist at 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_version gates 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 with BUILD_LIBRARY_FOR_DISTRIBUTION = YES and therefore ship a module-stable .swiftinterface, which newer compilers consume fine. The practical gate is "customer's Xcode >= ours", and xcode_version answers that directly.

Worth noting why Android can do this and we cannot: KotlinVersion.CURRENT works because kotlin-stdlib is 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 tracks os.version.

kotlinVersion likewise stays nil: the protocol scopes it to a Kotlin host app, which iOS is not. The shared module's own provenance is already on ossdk.kmp_version.

Test plan

  • OneSignalOSCoreTests green against main with KMP at the pinned v0.3.0
  • New OSLoggerHostBuildAttributesTests covering DTXcode digit unpacking, key mapping including minimum_os_version / minimum_macos_version, and omission of blank/missing keys
  • swiftlint clean on the changed source file
  • KMP XCFramework rebuilt at the pinned submodule commit
  • Confirm the new ossdk.* attributes land in GCP Logs Explorer on a real device build

Verify on device with:

resource.type="k8s_container"
labels.log_source="sdk_log"
jsonPayload."ossdk.app_id"="<app id>"

Related: SDK-5106, SDK-4999 (KMP contract), SDK-5081 (bug bash covering remote logging + feature flags).

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>
@abdulraqeeb33
abdulraqeeb33 requested a review from a team August 26, 2026 16:29
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>
@fadi-george

fadi-george commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Lets see what nan says but maybe we can drop DTXcodeBuild?
build_sdk_name seems to just be build_platform_name + build_platform_version, could drop build_sdk_name or just build_platform_name + build_platform_version.
build_platform_build seems to most often same as build_sdk_build so maybe just keep build_sdk_build.

@fadi-george

Copy link
Copy Markdown
Collaborator

Could we fall back to LSMinimumSystemVersion when MinimumOSVersion is absent? Catalyst bundles use that key, so minimum_os_version is currently omitted for Catalyst hosts.

for key in ["MinimumOSVersion", "LSMinimumSystemVersion"] {
    guard let value = infoDictionary?[key] as? String, !value.isEmpty else {
        continue
    }
    attributes["minimum_os_version"] = value
    break
}

@fadi-george

fadi-george commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Bundle.main refers to the extension bundle when this runs inside an NSE, not the containing app. Should these attributes describe the current executable instead? If so, could we update the “host app” wording? Otherwise, we need to locate the enclosing .app bundle before reading its Info.plist.

@nan-li

nan-li commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Bundle.main refers to the extension bundle when this runs inside an NSE, not the containing app. Should these attributes describe the current executable instead? If so, could we update the “host app” wording? Otherwise, we need to locate the enclosing .app bundle before reading its Info.plist.

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

@nan-li

nan-li commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Lets see what nan says but maybe we can drop DTXcodeBuild? build_sdk_name seems to just be build_platform_name + build_platform_version, could drop build_sdk_name or just build_platform_name + build_platform_version. build_platform_build seems to most often same as build_sdk_build so maybe just keep build_sdk_build.

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, ossdk.build_platform_name: "iphonesimulator" when we already have device.model.identifier: "Simulator iPhone"

Attribute Verdict Why
xcode_version keep The human-filterable toolchain field.
xcode_build keep Exact toolchain; separates beta from release; determines all SDK values below.
minimum_os_version keep The one fact nothing else carries, and the PR's stated point. Needs the LSMinimumSystemVersion fallback for Catalyst.
apple_platform keep Pre-existing Catalyst marker.

Here's a snippet of the payload

device.manufacturer: "Apple"
device.model.identifier: "Simulator iPhone"
os.build_id: "25F80"
os.name: "iOS"
os.version: "26.5"
ossdk.build_platform_build: "23F81a"
ossdk.build_platform_name: "iphonesimulator"
ossdk.build_platform_version: "26.5"
ossdk.build_sdk_build: "23F81a"
ossdk.build_sdk_name: "iphonesimulator26.5"
ossdk.install_id: "xxxxxxxxxxxxxxx"
ossdk.kmp_version: "v0.3.0"
ossdk.minimum_os_version: "16.0"
ossdk.sdk_base: "ios"
ossdk.sdk_base_version: "050506"
ossdk.xcode_build: "17F113"
ossdk.xcode_version: "26.6"

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>
@abdulraqeeb33

Copy link
Copy Markdown
Contributor Author

Pushed 45a0eac8 — took the full trim you both landed on.

Kept (3 new + 1 pre-existing):

Attribute Example
xcode_version 26.6
xcode_build 17F113
minimum_os_version 16.0
apple_platform mac_catalyst (pre-existing)

Dropped: build_compiler, build_platform_name, build_platform_version, build_platform_build, build_sdk_name, build_sdk_build.

The payload sample made the redundancy hard to argue with — build_platform_build and build_sdk_build were both 23F81a, and build_sdk_name was literally build_platform_name + build_platform_version concatenated. Agreed on build_platform_name vs device.model.identifier too.

Catalyst fallback (@fadi-george): added with your ordering, MinimumOSVersion then LSMinimumSystemVersion. Two tests cover it — the Catalyst-only case, and that MinimumOSVersion wins when both keys are present.

NSE / Bundle.main (@fadi-george, @nan-li): taking Nan's call that this is fine functionally, so no code change. I did fix the wording — the doc comment now says "the running executable" rather than "the host app", since that is accurate in either context.

Also renamed macCatalystAttribute -> applePlatformAttribute, and cut the doc comments down.

swiftVersion stays nil for now; @abdulraqeeb33 is removing the property in a separate PR since it is a KMP protocol requirement rather than something iOS can drop unilaterally.

Tests: 107 passing, 0 failures, against the pinned KMP submodule. Net -47/+61 across the provider and its tests.

@fadi-george

Copy link
Copy Markdown
Collaborator

Maybe a create a linear ticket for this and add to pr title.

@abdulraqeeb33

Copy link
Copy Markdown
Contributor Author

Created SDK-5106 and put it In Review. Title updated to match.

@abdulraqeeb33 abdulraqeeb33 changed the title feat: report host app build toolchain on iOS logs feat: [SDK-5106] report host app build toolchain on iOS logs Aug 27, 2026
@fadi-george

fadi-george commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

LSMinimumSystemVersion is a macOS version, while minimum_os_version otherwise contains an iOS version. Could Catalyst use a separate minimum_macos_version attribute to avoid mixing version namespaces?

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>
@abdulraqeeb33

Copy link
Copy Markdown
Contributor Author

Agreed — LSMinimumSystemVersion is a macOS version, so stuffing it into minimum_os_version would make "who is below iOS 15" queries include Catalyst apps pinned to macOS 13.

Pushed 70952b7d: Catalyst (and any host with that plist key) now emits ossdk.minimum_macos_version. minimum_os_version is only MinimumOSVersion. Both can be present and are written independently.

OSLoggerHostBuildAttributesTests covers the Catalyst-only case and the both-keys case. SDK-5106 updated to match.

AR Abdul Azeez and others added 2 commits August 27, 2026 12:36
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>
@abdulraqeeb33
abdulraqeeb33 merged commit a2005bb into main Aug 27, 2026
4 checks passed
@abdulraqeeb33
abdulraqeeb33 deleted the ar/sdk-ios-host-toolchain-attrs branch August 27, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants