AnalyticsSystem 2.0.0 — dependency-free core, Swift 6 concurrency, real test coverage - #8
Merged
Merged
Conversation
…e (2.0.0)
The package had not been touched since November 2020 and no longer built:
Firebase and Facebook were pinned to `.branch("master")`, the deployment
target was iOS 10, and the adapters called SDK APIs removed years ago.
BREAKING CHANGE: 2.0.0 redesigns the public API. 1.0.0 is untouched and
remains installable; see MIGRATION.md for a symbol-by-symbol map.
Fixes, each with a regression test:
* Force cast in the public API. `FactoryAnalyticsTracker` recovered an
event's concrete type with `event as! Event`, commented "safe operation".
Events now carry their type in a closure captured at the generic call
site, so nothing on the event path casts at all.
* Two `fatalError` initializers reachable from public API, which made
`addTracker(_:with:)` a guaranteed crash for Mixpanel and Bugsnag. The
library no longer constructs trackers, so the requirement is gone.
* Mixpanel `logOut` never called `reset()`, so the distinct ID survived
log out and the next user was silently correlated with the previous one.
* The registry keyed on `String(describing:)` — unstable, and unable to
tell two instances of one provider apart.
* No synchronisation on the tracker dictionary or the enabled flag.
* `UIApplication.LaunchOptionsKey` leaked into the cross-platform tracker
protocol, guarded inconsistently against its own import.
* `@UserDefault` stored a null instead of removing a key, so clearing the
anonymous ID did not clear it; it also called deprecated `synchronize()`.
* Bugsnag could never receive an event; Firebase never reported crashes.
* CI ran `swift build` on macOS while every provider was `os(iOS)`-gated,
so no adapter body was ever compiled — green for years, proving nothing.
Structural changes:
* Package traits gate each provider. A default install now resolves ZERO
third-party packages; previously every consumer pulled Firebase's entire
transitive tree just to use the console tracker.
* Swift 6 language mode throughout, with no `@unchecked Sendable` outside
one documented mutex box. `[String: Any]` becomes `AnalyticsValue`.
* Mapping and filtering move from class inheritance to composable values
bound at registration.
* Dispatch moves off `OperationQueue.main` onto one serial queue, which
also gives ordering between lifecycle calls and events.
* Platforms: iOS 15, macOS 12, tvOS 15, watchOS 8, visionOS 1.
* 57 tests across 11 suites (v1 had one test with its assertion commented
out), plus an integration package that compiles all four adapters
against their real SDKs for iOS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The package was last touched in November 2020 and no longer builds. Firebase and Facebook were pinned to
.branch("master"), the deployment target was iOS 10 (below what current Xcode accepts), and the adapters call SDK APIs that were removed years ago — Facebook movedAppEvents/Settingsoff the type level in SDK v12.CI had been green that entire time while proving nothing: it ran
swift buildon macOS, but every provider is#if os(iOS)-gated, so not one line of adapter code was ever compiled.The headline change
A default install now resolves zero third-party dependencies. Previously every consumer pulled Firebase's whole transitive tree — gRPC, BoringSSL, abseil, leveldb, nanopb, Promises, GTMSessionFetcher — just to use the console tracker. Providers are now opt-in package traits:
Verified:
swift package resolvewith default traits produces noPackage.resolvedat all.Crashers fixed
event as! Event// safe operation; it was the framework's only type-recovery mechanism and trapped on any factory/event mismatch. Events now capture their concrete type in a closure at the generic call site — nothing on the event path casts.fatalErrorinitsMixpanelTracker/BugsnagTrackerdeclaredrequired init(eventsFactory:) { fatalError() }to satisfy an inherited requirement, makingaddTracker(_:with:)a guaranteed crash. The library no longer constructs trackers, so the requirement is gone.Correctness fixed
logOutnever calledreset()— the distinct ID survived log out, so the next user on a device was silently correlated with the previous one. Worth knowing this will show as a discontinuity in Mixpanel data at upgrade.isAvailablehardcodedfalse).appDidCrashLastLaunch()returnedfalseunconditionally, though Crashlytics answers it).@UserDefaultstored a null instead of removing the key.UIApplication.LaunchOptionsKeyleaked into the cross-platform protocol, its typealias guarded oncanImport(UIKit)while the import was guarded onos(iOS).Design
Two root causes drove nearly every defect: type erasure happened too early, and behaviour was carried by class inheritance plus mutable stored state — the exact thing that cannot be made
Sendable. So v2 erases late and composes instead of inheriting. Mapping and filtering bind at registration rather than being baked into aFactoryAnalyticsTracker<F>subclass.trackstays synchronous and callable from any isolation domain — making the facade an actor would have put a suspension point in every UI call site. Lifecycle calls and events share one serial queue, which is what guaranteeslogInreaches providers before atrackissued right after it.Swift 6 language mode throughout, with no
@unchecked Sendableanywhere except one documentedNSLockbox (Mutexneeds iOS 18, above the iOS 15 floor).Verification
IntegrationTests/ProviderBuildpackage built for iOS Simulator — this is the gate that closes the CI blind spot.swift build -Xswiftc -strict-concurrency=complete→ 0 diagnostics.swiftlint --strict→ 0 violations.pod lib lintpasses.Compatibility
Breaking, hence 2.0.0. 1.0.0 is untouched and stays installable; consumers below iOS 15 or on Xcode < 16.3 simply stay there. MIGRATION.md maps every symbol.
The CocoaPods spec exposes all four providers again — v1 had quietly deleted the Firebase and Mixpanel subspecs, so CocoaPods and SPM shipped different sets. Traits are emulated there via
SWIFT_ACTIVE_COMPILATION_CONDITIONS.🤖 Generated with Claude Code