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
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 underStatsConfiguration.hashedUserId(_:)andhashedUserId(_:salt:)— the exact user ididentify()sends, so an app’s server can name the account in an erase request
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
setConsentmoves 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 usersetConsent(.default)now grants identity; a value recorded with it on 0.2.0 stays[.usage, .diagnostics]- One live client per app id: a second
StatsClientfor the same app id forwards to the first instead of double-sending.currentConsent,isEnabledand the newhasStableInstallIdentityareget async - Fixed: no send while not collecting, an unreadable queue file is no longer overwritten,
seqno longer runs across per-session ids, andsession_enddurations use the monotonic clock
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 withadmin.mjs set-retentionorsetProjectRetention() - Migration 0008:
events,batchesandbatch_contextcascade 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
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_dedupedtally 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
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.headsidecar next toqueue.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 oftrack()that is safe in a button action or a view body
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