Skip to content

Hide bundled dependency symbols to prevent OpenTelemetry type clashes#290

Open
ypopovych wants to merge 4 commits into
mainfrom
fix/isolate-bundled-dependencies
Open

Hide bundled dependency symbols to prevent OpenTelemetry type clashes#290
ypopovych wants to merge 4 commits into
mainfrom
fix/isolate-bundled-dependencies

Conversation

@ypopovych

Copy link
Copy Markdown
Contributor

Problem

The framework statically links OpenTelemetry (api/sdk), KSCrash, Kronos, SigmaSwiftStatistics and swift-code-coverage and exported all of their symbols. When a project-under-test links its own copy of the same dependency (e.g. a project that itself depends on opentelemetry-swift at a different version — like dd-sdk-ios), dyld coalesces the duplicate weak type-metadata symbols to one process-wide definition. Mismatched layouts then make one image read objects through the other's metadata, crashing in swift_getObjectType (observed via OpenTelemetryContextProvider.setActiveSpan while the SDK wrapped a test).

Change

  • Add UNEXPORTED_SYMBOLS_FILE (DatadogSDKTesting-unexported-symbols.txt) on the framework target so every bundled-dependency symbol is non-exported; only the public DatadogSDKTesting API stays exported. Verified on a real archive: exported symbols drop from ~11,240 to 762 (0 OpenTelemetry*/KSCrash/Kronos/… exported), public Swift + ObjC API intact.
  • Remove DDInstrumentationControl.openTelemetryTracer and its README section: with OpenTelemetry now private, the documented as? Tracer interop can no longer work.

Known limitation (why CI is the real test)

Symbol hiding closes the dyld weak-symbol coalescing vector. It does not remove Swift classes from the ObjC runtime's __objc_classlist (pure-Swift classes on Darwin still register as _TtC… ObjC classes regardless of export visibility), so objc: Class … implemented in both … can still occur. A synthetic two-copy repro didn't crash on either the fixed or unfixed build, so this PR is pushed to validate against the real dd-sdk-ios suite in CI. If the ObjC-class residual proves to matter, the fully-robust fix is renaming the OpenTelemetry module (DatadogOpenTelemetryApi).

🤖 Generated with Claude Code

The framework statically links OpenTelemetry (api/sdk), KSCrash, Kronos,
SigmaSwiftStatistics and swift-code-coverage and exported all of their
symbols. When a project-under-test links its own copy of the same
dependency (e.g. opentelemetry-swift at a different version), dyld
coalesces the duplicate weak type-metadata symbols to a single
process-wide definition. Mismatched layouts then make one image read
objects through the other's metadata, crashing in swift_getObjectType
(observed via OpenTelemetryContextProvider.setActiveSpan).

Mark every bundled-dependency symbol non-exported via
UNEXPORTED_SYMBOLS_FILE so each image binds its own copy and dyld can't
coalesce them. Only the public DatadogSDKTesting API stays exported
(762 symbols vs ~11,240 before).

Also drop DDInstrumentationControl.openTelemetryTracer and its README
section: with OpenTelemetry now fully private, the documented
`as? Tracer` interop can no longer work, so the hook is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ypopovych ypopovych requested review from a team as code owners July 10, 2026 11:08

@datadog-prod-us1-3 datadog-prod-us1-3 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Datadog Autotest: PASS

More details

The *12CodeCoverage* pattern in the unexported symbols file also matches the DatadogSDKTesting-internal CodeCoverage class (same 12-char identifier), but this is harmless: the class is internal, and unexported symbols remain callable within the same binary. Symbol pattern length-prefixes are all correct, all bundled dependency modules are covered, openTelemetryTracer has zero remaining references across the entire repo, and both Debug and Release configurations receive the UNEXPORTED_SYMBOLS_FILE setting. The change is a clean linker-level isolation fix with one intentional public API break.

Was this helpful? React 👍 or 👎

📊 Validated against 7 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit 07592fe · What is Autotest? · Any feedback? Reach out in #autotest

ypopovych and others added 3 commits July 10, 2026 12:31
`UNEXPORTED_SYMBOLS_FILE` was set on both Debug and Release configs of the
framework target. The unit tests (`DatadogSDKTestingTests`) `@testable
import DatadogSDKTesting` and reference internal symbols whose mangled
names embed dependency types — e.g. `DDTracer.createSpanFromCrash(...) ->
OpenTelemetrySdk.SpanSdk` and `EarlyFlakeDetection.init(... EventsExporter
.TracerSettings...)`. The substring hide-patterns matched those, so the
Debug framework stopped exporting them and the test bundle failed to link
(undefined symbols).

Hiding only matters for the shipped artifact, which is archived in
Release. Move `UNEXPORTED_SYMBOLS_FILE` to the Release config only: tests
build in Debug (all symbols exported, link OK) and the distributed
xcframework is archived in Release (dependency symbols hidden).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The internal integration test targets should validate the framework as it
ships — with bundled-dependency symbols stripped. The outer Runner
(`IntegrationTests`) keeps `@testable`/Debug access, and it builds the
inner consumer targets it drives, so it now passes `-configuration
Release` to those nested `xcodebuild` invocations. Release is where
`UNEXPORTED_SYMBOLS_FILE` applies, so the inner targets link the stripped
framework.

Rework the inner smoke tests into pure public-API consumers: drop the
`OpenTelemetry.instance.loggerProvider` bridge (and the Runner assertions
that the OTel LogRecord reached the backend). That path only works when a
consumer shares the SDK's OpenTelemetry instance, which is exactly what
stripping makes private — so it isn't available to a real consumer of the
shipped framework. stdout capture, per-test spans and code coverage
(which don't need shared OTel) still run and are still asserted.

Note: a genuine second in-process OpenTelemetry copy (the coalescing
repro) can't be produced here — SwiftPM dedupes the shared package product
into one dynamic framework, which also breaks the framework's own static
OTel linkage. That scenario is covered by dd-sdk-ios in CI, whose OTel is
a separate binary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Building the inner integration targets in Release (to get the stripped
framework) also dragged in -O/WMO/LTO: slow builds and optimized,
hard-to-debug test failures — none of which is relevant to what these
tests validate (symbol isolation, not optimization).

Add an `Integration` build configuration, copied from Debug, that keeps
`-Onone`/`GCC_OPTIMIZATION_LEVEL=0` (fast, debuggable) but applies the
framework's `UNEXPORTED_SYMBOLS_FILE` and sets `ENABLE_TESTABILITY=NO`.
The result is a framework whose exported symbol surface matches the
shipped Release build exactly — 0 bundled-dependency symbols
(OpenTelemetry/KSCrash/Kronos/Sigma/EventsExporter), 745 public — without
the Release optimizer overhead.

The Runner now builds the inner consumer targets with `-configuration
Integration` instead of `Release`. Config matrix for the framework:
- Debug:       no strip, testability on   (unit tests, @testable)
- Integration: strip,    testability off  (integration tests, Debug-speed)
- Release:     strip                       (shipped xcframework)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant