Repository navigation
feat(telemetry): anonymous daily installation heartbeat - #67
Merged
Merged
Conversation
Exploration, proposal, spec, design and tasks for an anonymous, opt-out daily heartbeat from self-hosted installs.
Each install sends aggregate counts, runtime and SDK versions once a day to a Flagward collector. On by default in production and development, off in CI, and FLAGWARD_TELEMETRY=false turns it off entirely. - Random UUID4 installation identity, never derived from host data - One send per 24h across workers via a conditional UPDATE on last_sent_at - Started from the ASGI/WSGI entrypoints, never from management commands - SDK types/versions mapped to an allowlist; evaluations sent as a bucket - manage.py telemetry --show / --send, and a local collector stub - compose.dev.yml forwards CI so pipelines stay silent
The collector is live, so the destination is now fixed instead of configurable: FLAGWARD_TELEMETRY_URL is gone and operators can only turn telemetry off, not redirect it. The local stub goes with it; manage.py telemetry --show remains the way to inspect the payload. Docs now point to the public stats page.
basb7
added a commit
to basb7/flagward-telemetry
that referenced
this pull request
Sep 29, 2026
Flagward now always sends to telemetry.flagward.com, so FLAGWARD_TELEMETRY_URL no longer exists (basb7/flagward#67). Local testing posts the real payloads in tests/fixtures instead.
3 tasks done
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.
What does this change?
Flagward is self-hosted, so today there is no way to know how many installations exist, which versions they run, or whether multivariate flags and the SDK adapters are actually used. This adds an anonymous, aggregate-only, opt-out daily heartbeat from each installation.
What is sent: version, runtime (Python/Django, Docker or not, database, Redis, email configured), counts (orgs, projects, environments, active users, flags by type, rules, rollouts, variants, active overrides), active SDK types/versions per environment, and a bucketed 24h evaluation volume. Plus
mode(production/development) andserver(asgi/wsgi/runserver), so prod and dev installs are counted separately.What is never sent: emails, names, flag keys, variant names, condition values, API keys, hostnames.
sdk_type/versionare client-supplied free text, so they are mapped to an allowlist (other/unknown). Every field is listed indocs/telemetry.md.When it is on
FLAGWARD_TELEMETRY→ on, in production and development alike, except whenCIis truthy.FLAGWARD_TELEMETRY=false→ no thread, no DB row, no network request.compose.dev.ymlforwardsCIinto the backend container; without it the e2e job would report itself as a dev install on every run.How it works
telemetry.InstallationIdentity: a singleton row (DB-enforced) with a random UUID4.config/asgi.py/config/wsgi.py, never fromAppConfig.ready(), somigrate, tests and other commands never spawn it.UPDATE ... WHERE last_sent_at < now-24hlets exactly one worker per window send. Claim-then-send: a failed send is not retried until the next window.urllib, 3s timeout, every failure swallowed and logged at DEBUG only.https://telemetry.flagward.com/v1/heartbeat(telemetry.settings.TELEMETRY_URL). Operators can turn it off, not redirect it — there is exactly one place the data can go, and it is public.manage.py telemetry --showprints the exact payload without sending;--sendsends now.config/version.py(__version__, bumped per release): operators build from source, so a Docker build arg would be empty. Adds a minimalLOGGINGfor thetelemetrylogger, since Django's default config would swallow the startup notice.The collector is live
basb7/flagward-telemetry is deployed at telemetry.flagward.com: strict schema v1, one row per installation per day, Nginx strips the client IP before the app and keeps no access logs, and the public page publishes only per-installation aggregates with a minimum group size of 5.
How was it verified?
secret-launch,Acme,ana@acme.com, the API key) and asserting none leak, slot dedup, failure isolation,--shownever creating the identity or reaching the network, and a clear error on an unmigrated DB.compose.dev.yml→ payload withmode=development,server=runserver; identity stable across restartsCI=true→--sendreports disabled, no startup notice, nothing receivedcompose.yml(4 workers) → 4 startup notices, exactly one payload,mode=production,server=asgi; restart within 24h → nothingFLAGWARD_TELEMETRY=false→ no notice, nothing received/api/v1/health/still 200204, and a second one the same day kept a single row.Checklist
ruff check .passespytestpassesnpm run lintandnpm run buildpass, if the frontend changed — frontend unchanged