Skip to content

Instantly share code, notes, and snippets.

@danielctull
Created April 24, 2026 09:53
Show Gist options
  • Select an option

  • Save danielctull/eb23ff94fee69ec60cdac99746ec6937 to your computer and use it in GitHub Desktop.

Select an option

Save danielctull/eb23ff94fee69ec60cdac99746ec6937 to your computer and use it in GitHub Desktop.
Why make Tracker Sendable — high-level rationale

Why make Tracker Sendable?

TL;DR

Telemetry is cross-cutting. A Tracker gets passed into view models, services, background workers, network layers — anything that might want to record an event. In Swift 6, anything shared across concurrency boundaries (tasks, actors, @Sendable closures) must be Sendable. Today Tracker isn't, so every consumer either fights the compiler with workarounds (@unchecked Sendable wrappers, nonisolated(unsafe), captured-var hacks) or simply can't adopt strict concurrency.

Making Tracker Sendable removes that friction once, at the source, so every downstream library and feature gets safe concurrent telemetry for free.

The problem in practice

When a feature team turns on Swift 6 / strict concurrency, the first wall they hit is usually telemetry:

Task {
  tracker.track(MyEvent())  // ❌ capture of non-Sendable 'Tracker' in @Sendable closure
}

Workarounds we've seen across the codebase:

  • @unchecked Sendable wrappers around Tracker.
  • nonisolated(unsafe) let tracker.
  • Forcing the call site onto @MainActor even when there's no UI reason to.
  • Avoiding Task / async entirely and reaching for Combine just to dodge the warning.

Each is a local patch that hides a real concurrency question and adds noise.

What changes

Tracker (and its supporting types — Detail, Event, Baggage, the recording closure) become Sendable. The internals use Mutex / Lock to make the mutable state (baggage, child trackers) safe to touch from any isolation domain. The public API is unchanged in shape — same track(_:), same composition operators — it just stops requiring escape hatches.

Why it's worth doing now

  1. Unblocks Swift 6 adoption everywhere downstream. Telemetry sits at the bottom of the dependency graph; until it's Sendable, no library above it can cleanly turn on strict concurrency. Fixing it once unblocks dozens of consumers.
  2. Removes a class of subtle bugs. Captured-var patterns in test code (var count = 0; tracker { _ in count += 1 }) and in production (background closures mutating shared state) are data races the compiler currently can't see. Sendable forces them into safe shapes (Lock, recorder types).
  3. Better testing story. Sendable-friendly EventRecorder / DetailRecorder replace ad-hoc captured-var counters in tests, giving cleaner, race-free assertions.
  4. Aligns with Apple's direction. Swift 6 strict concurrency is the default-on future. Foundation, SwiftUI, and the platform SDKs are all moving this way; libraries that don't follow become friction points.
  5. Cheap to consume. For most callers it's a no-op recompile. For callers using the hacky patterns above, it's a small mechanical refactor to a safer equivalent — and we do that work in this rollout, not later under pressure.

Cost

  • One coordinated 2.0 release of mobile-swift-telemetry and its integration packages (Dynatrace, Adobe, New Relic, etc.).
  • A handful of small downstream PRs to swap captured-var test patterns for EventRecorder / DetailRecorder. Already done in BankingKit, ConfigurationKit, OptimizelyFlagging, Sparks; pattern is well established.
  • No behavioural change at runtime — it's a type-system / safety improvement.

Bottom line

We can either fix telemetry once at the source, or every team fixes it locally with @unchecked Sendable and lives with the data races. The first option is cheaper, safer, and a one-time cost.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment