Skip to content

feat: report host app build toolchain on iOS logs - #1728

Open
abdulraqeeb33 wants to merge 3 commits into
mainfrom
ar/sdk-ios-host-toolchain-attrs
Open

feat: report host app build toolchain on iOS logs#1728
abdulraqeeb33 wants to merge 3 commits into
mainfrom
ar/sdk-ios-host-toolchain-attrs

Conversation

@abdulraqeeb33

@abdulraqeeb33 abdulraqeeb33 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

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 host app'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
build_compiler DTCompiler com.apple.compilers.llvm.clang.1_0
build_platform_name DTPlatformName iphonesimulator
build_platform_version DTPlatformVersion 26.2
build_platform_build DTPlatformBuild 23C53
build_sdk_name DTSDKName iphonesimulator26.2
build_sdk_build DTSDKBuild 23C53
minimum_os_version MinimumOSVersion 12.0

Prefixed build_sdk_* rather than sdk_* because "SDK" already means the OneSignal SDK in this namespace — ossdk.sdk_name would read as ours, not Apple's. DTXcode is packed ("2620" = 26.2) so it is decoded rather than shipped raw. Blank and missing keys are dropped rather than emitted empty.

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, 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-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>
/// 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will remove this swiftVersion in another pr

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

Comment on lines +201 to +211
/// 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 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit do we need 10 lines to describe this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimmed. Now four lines, keeping only the part that is not evident from the code (why this is read at runtime rather than with a compile-time check).

static let osBuildName = "kern.osversion"
static let xcodeVersionInfoKey = "DTXcode"
static let xcodeVersionAttribute = "xcode_version"
static let macCatalystAttribute = "apple_platform"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the name of this variable is a bit confusing. Can we be worded or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed macCatalystAttribute -> applePlatformAttribute so the constant matches the attribute name it holds.

("DTPlatformBuild", "build_platform_build"),
("DTSDKName", "build_sdk_name"),
("DTSDKBuild", "build_sdk_build"),
("MinimumOSVersion", "minimum_os_version")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, agree with Fadi on including LSMinimumSystemVersion for catalyst

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the fallback, using Fadi's ordering — MinimumOSVersion first, then LSMinimumSystemVersion. Covered by two tests: the Catalyst-only fallback, and that MinimumOSVersion wins when both are present.

/// raising our own deployment target.
private static let buildMetadataKeys: [(infoKey: String, attribute: String)] = [
("DTXcodeBuild", "xcode_build"),
("DTCompiler", "build_compiler"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recommend to drop this field as DTCompiler has been the constant com.apple.compilers.llvm.clang.1_0 on effectively every app built since about 2011

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped build_compiler, and took the rest of the trim too — build_platform_name, build_platform_version, build_platform_build, build_sdk_name, build_sdk_build. Left with xcode_version, xcode_build, minimum_os_version, plus the pre-existing apple_platform.

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.

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