Skip to content

2.1.0 — fix two silent event-loss bugs, add observability, docs and repo health - #9

Merged
AndrewKochulab merged 1 commit into
masterfrom
feature/v2.1-improvements
Aug 18, 2026
Merged

2.1.0 — fix two silent event-loss bugs, add observability, docs and repo health#9
AndrewKochulab merged 1 commit into
masterfrom
feature/v2.1-improvements

Conversation

@AndrewKochulab

Copy link
Copy Markdown
Owner

Why

I probed 2.0.0 for weaknesses before proposing anything, and found two real bugs — both of which lose data silently. Everything else here follows from the same theme: analytics failures are invisible by default, so the library should say when it discards something.

Additive throughout; 2.0.0 code keeps compiling.

The two bugs

Confirmed by probe before fixing, each now with a named regression test:

Before After
track() before any tracker is registered event silently lost — exactly the app-launch case held, replayed after start()
track() before start() record() delivered to a provider whose SDK was never initialised — Firebase logging before FirebaseApp.configure(), Mixpanel discarding internally delivery never precedes start()
// before: this event went nowhere, with no diagnostic
system.track(AppLaunched())
try await system.register(tracker)
await system.start()

Governed by AnalyticsStartupBuffer — 100 events by default, replayed in order, bounded with oldest-dropped-first. .disabled restores the old behaviour.

Note this is a behaviour change in a minor version. I think it's justified: in both cases the previous behaviour was losing data, and the new behaviour delivers it. Tests that assert on delivery now need start(), which is what an app does anyway.

Making failure visible

AnalyticsDiagnostic plus a handler on Configuration reports every event the system buffers, drops, rejects or rewrites. Previously all of it was invisible — an event that never sent looked identical to one never tracked.

AnalyticsSystem(configuration: .init(diagnostics: { logger.warning("analytics: \($0)") }))

AnalyticsRecordValidator is the same idea for vendor limits. Firebase silently discards events with names over 40 characters, more than 25 parameters, or reserved firebase_/google_/ga_ prefixes — nothing is reported, the data just never appears. .firebase runs those rules locally, sanitizing what it safely can and rejecting the rest with a reason.

Also

  • Global propertiessetGlobalProperties(_:) merged into every record, event attributes winning on conflict.
  • flushProviders() — asks each vendor SDK to send what it batched, for backgrounding. Wired to Mixpanel and Facebook; flushPendingEvents() defaults to a no-op so no provider is forced to stub it.
  • DocC catalog + two articles, published to GitHub Pages.
  • .spi.yml, CONTRIBUTING, SECURITY, issue/PR templates, CODEOWNERS, Dependabot.

One deliberate non-choice

I added swift-docc-plugin, then removed it: it shows up in a default swift package resolve, which would break the zero-dependency guarantee that is this package's headline feature. Docs are built with xcodebuild docbuild instead, which costs nothing. CI asserts the guarantee on every run.

Verification

  • 96 tests / 18 suites (was 57 / 11). Coverage 71.9% → 87.8% of lines, 92.3% of regions.
  • All four adapters compiled against real SDKs; AppEvents.shared.flush() and .firebase verified on the iOS integration build, since Facebook's code is stubbed out on macOS.
  • Five platforms, -strict-concurrency=complete clean, swiftlint --strict clean, podspec lints at 2.1.0, DocC builds.
  • Default swift package resolve still pulls zero third-party packages.

🤖 Generated with Claude Code

Additive throughout — 2.0.0 code keeps compiling. The two fixes are behaviour
changes, and both replace silent data loss with delivery.

Fixed (each confirmed by probe before fixing, each with a regression test):

* Events tracked before any tracker was registered were silently lost. That is
  exactly the app-launch case: anything reported before `register` returned went
  nowhere, with no diagnostic.
* Events could reach a provider before its SDK was initialised. `track` before
  `start()` called `record()` on a tracker that had never been started — Firebase
  logging before `FirebaseApp.configure()`, Mixpanel discarding the event
  internally.

Both are governed by AnalyticsStartupBuffer, which holds up to 100 events by
default and replays them in order once `start()` completes. `.disabled` restores
the previous behaviour.

Added:

* Global properties, merged into every record; event attributes win on conflict.
* AnalyticsDiagnostic and a handler on Configuration, reporting every event the
  system buffers, drops, rejects or rewrites. All of these were previously
  invisible — an event that never sent looked identical to one never tracked.
* AnalyticsRecordValidator per registration, with `.firebase` encoding Firebase's
  real limits. Firebase discards violations server-side and reports nothing, so
  these were undebuggable.
* flushPendingEvents() on AnalyticsTracker (default no-op), surfaced as
  flushProviders(), wired to Mixpanel and Facebook, for backgrounding.
* DocC catalog with two articles, published to GitHub Pages; .spi.yml;
  CONTRIBUTING, SECURITY, issue/PR templates, CODEOWNERS, Dependabot; code
  coverage and a DocC job in CI.

Coverage rose from 71.9% to 87.8% of lines; 96 tests in 18 suites, up from 57
in 11.

Note: DocC is built with `xcodebuild docbuild` rather than swift-docc-plugin,
because the plugin appears in a default `swift package resolve` and would break
the zero-dependency guarantee. CI asserts that guarantee on every run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AndrewKochulab
AndrewKochulab merged commit a0859e5 into master Aug 18, 2026
10 checks passed
@AndrewKochulab
AndrewKochulab deleted the feature/v2.1-improvements branch August 18, 2026 20:51
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