Changelog

Two things ship here

The Swift package follows semantic versioning, and the wire schema is versioned separately. The hosted dashboard ships in phases.

Swift package

0.3.1

Sign-out and account erasure for apps that call identify(userID:). Additive; no wire-format change. Pin with .upToNextMinor(from: "0.3.1").

  • forgetUser() — the sign-out call: stops attaching the hashed user id, in memory and on disk, and keeps the install, the session and the queue. Events already recorded keep the id they were recorded under
  • StatsConfiguration.hashedUserId(_:) and hashedUserId(_:salt:) — the exact user id identify() sends, so an app’s server can name the account in an erase request
0.3.0

Per-install by default. StatsConsent.default now includes .identity, so each install keeps a stable, salted, random id and installs, first-seen installs and retention count installs rather than sessions. No userId is sent unless the app calls identify(userID:). No wire-format change; pin with .upToNextMinor(from: "0.3.0") and read the upgrade notes first.

  • An app that never called setConsent moves to per-install ids at its next launch: every existing device shows up once as a new install in the upgrade week. The SDK manifest now declares Device ID (not linked, not tracking) — update the App Store label
  • identify(userID:) now takes effect under the default: the hashed user id is sent and persisted, which is what the dashboard’s Users view counts. Declare User ID and mark collected types linked, or pass [.usage, .diagnostics] to stay per-session with no user
  • setConsent(.default) now grants identity; a value recorded with it on 0.2.0 stays [.usage, .diagnostics]
  • One live client per app id: a second StatsClient for the same app id forwards to the first instead of double-sending. currentConsent, isEnabled and the new hasStableInstallIdentity are get async
  • Fixed: no send while not collecting, an unreadable queue file is no longer overwritten, seq no longer runs across per-session ids, and session_end durations use the monotonic clock
backend-cloudflare-0.4.0

Cloudflare backend only — rollup and retention integrity. The batch format and the /v1 shapes are untouched; some read numbers change by design. Two migrations: 0007 is required by the new Worker, 0008 rebuilds the raw tables and should be rehearsed on a large database.

  • Fixed: re-rolling a day for one project no longer wipes other projects’ rollups — rollups are per project
  • Migration 0007: projects.raw_complete_from — raising retention no longer makes already-swept days read as zero. Raise it only with admin.mjs set-retention or setProjectRetention()
  • Migration 0008: events, batches and batch_context cascade with their project, and existing orphans are dropped
  • The breakdown prop cap ranks props by how often they are present, and its null row is counted per day
  • The nightly job stays inside the Workers subrequest and wall-time limits, resuming the next night, and one project’s failure no longer skips the others
backend-cloudflare-0.3.0

Cloudflare backend only — key liveness, first-seen installs and per-project retention. No wire change: the SDK, the batch format and the /v1 bodies are untouched, so an existing emitter and reader keep working. The number skips 0.2.0 because the backend package was never bumped for that tag.

  • Migration 0004: keys.last_used_at, coalesced to at most one write per key per minute, so a key can be checked for traffic before it is revoked
  • Migration 0005: installs — first sighting per install, exempt from the raw purge, with a backfill marker so backfilled days are not read as exact
  • Migration 0006: projects.retention_days — raw retention per project, default 90, bounds 90–400
  • Fixed: the events_deduped tally no longer counts the installs write, which had made returning users read as replayed events
  • The read endpoints now perform the coalesced key touch, deferred with waitUntil — still safe and idempotent, no longer literally write-free
0.2.0

A hardening pass with no breaking API changes: every 0.1.0 call site compiles unchanged. One normative wording change in wire schema v1 — a backend SHOULD now treat (projectId, installId, seq) as a per-event idempotency key — and one required Worker migration.

  • Low Data Mode: the default transport sets allowsConstrainedNetworkAccess = false, so batches stay queued until the mode lifts
  • Request timeouts: 20s per request and 60s per resource, replacing the URLSession defaults of 60s and 7 days
  • An additive queue.head sidecar next to queue.jsonl; 0.1.0 queue files load unchanged
  • Per-event idempotency in the Worker (migration 0003_event_idempotency.sql): replays are absorbed, the request still answers 202
  • StatsClient.record(_:props:) — a nonisolated, non-async sibling of track() that is safe in a button action or a view body
0.1.0

First tagged release: wire schema v1, the Stats core emitter, the StatsCloudflare adapter, and the Cloudflare Worker + D1 backend.

  • Actor-based client: track, identify, consent, flush, reset, lifecycle
  • JSON-lines durable queue; track() returns once on disk
  • Dispatcher: batching by bytes then count, jittered backoff, 24-hour delivery ceiling
  • Consent as three independent groups, persisted
  • Bundled privacy manifest with tracking set to false, asserted by a test
Unreleased — nothing tagged since the entries above. Until 1.0, minor versions may make breaking API changes; the wire schema v1 will not break.

Hosted dashboard

P14aMagic-link auth, app shell, project list, membershipSHIPPED
P14bSelf-serve setup wizard, key minting and revocation, first-event checkSHIPPED
P14cThe dashboard: KPI tiles, daily chart, top events and properties, environment breakdown, freshness, settingsSHIPPED
D1–D6Event catalog and event detail, Releases, Sessions, Retention, Settings, and the interactive chartSHIPPED
FlowsFlow explorer and funnels from a flow path (experimental)SHIPPED
P14dThis marketing siteSHIPPED
BillingPaid tiersIN PROGRESS