Telemetry (Product)¶
On-demand reference for product telemetry. The short rule in CLAUDE.md is: telemetry is opt-in and off by default; every event property must be explicitly allowlisted; never bypass the scrubber.
Enabling¶
- Off by default. Enable with
SYNTHORG_TELEMETRY_ENABLED=trueor thetelemetry.enabledsetting (or via the dashboard / DB). - Delivery backend is Logfire. The write-only project token is embedded in the release wheel at build time (
src/synthorg/telemetry/reporters/_embedded_token.pyis rewritten by.github/workflows/release.ymlbeforeuv build); operators never configure or paste it. Local source installs ship the sentinel and run telemetry-disabled by build.
Lifecycle log signals¶
The collector emits exactly one lifecycle log line per process boot, depending on which path applies:
| State | Severity | Event | Notes |
|---|---|---|---|
| Disabled (default) | INFO | telemetry.disabled |
Heartbeat task not started; zero per-cycle warnings. |
| Enabled, embedded token present | INFO | telemetry.reporter.initialized |
Heartbeat starts; deployment.startup follows. |
| Enabled, sentinel token (build artifact missing token) | ERROR | telemetry.token.missing |
Operator-actionable: rebuild with LOGFIRE_PROJECT_TOKEN CI secret. Heartbeat NOT started -- no per-cycle spam. |
Enabled, logfire import or configure failure |
WARNING | telemetry.report.failed (factory) |
error_type carries the actual exception class (no longer hardcoded as ImportError). Heartbeat NOT started. |
Privacy by allowlist¶
Every event property must be explicitly listed in src/synthorg/telemetry/privacy.py::_ALLOWED_PROPERTIES keyed by event type. Unknown keys raise PrivacyViolationError and are dropped before delivery.
Forbidden key patterns (rejected even if allowlisted):
key,token,secret,password,credential,bearer,authcontent,message,prompt,description
String values are capped at synthorg.telemetry.config.MAX_STRING_LENGTH (64 chars).
Collector lifecycle¶
Two-phase to keep filesystem I/O off the event loop:
- Construction (
__init__): reads config and resolves the environment tag. Performs zero filesystem I/O. Safe to call synchronously from the app builder. - Startup (
async start()): loads or creates the anonymous deployment ID viaasyncio.to_thread(5 s hard deadline). Sends the initialdeployment.startupevent with Docker enrichment. Schedules the periodic heartbeat task. Idempotent under concurrent callers via the lifecycle lock; the second caller sees_deployment_idalready populated and skips the load. - Shutdown (
async shutdown()): cancels the heartbeat, sendsdeployment.session_summaryanddeployment.shutdown, then shuts down the reporter. Skips event emission if_deployment_idwas never loaded (and logstelemetry.shutdown.without_startso operators can detect a silent init failure).
The deployment ID lives at {data_dir}/telemetry_id. Multiple replicas mounting the same /data race on O_CREAT|O_EXCL; losers re-read the winner's UUID with retry on partial-write windows so all replicas converge to one ID. On read/write failure the collector falls back to an in-memory UUID and tags the warning log with using_generated_id=True so dashboards can detect splinter deployments.
Environment resolution chain¶
First match wins, implemented in synthorg.telemetry.collector._resolve_environment:
SYNTHORG_TELEMETRY_ENV(operator override; always wins).- CI auto-detection:
CI,GITLAB_CI,BUILDKITE,JENKINS_URL, anyRUNPOD_*→"ci". SYNTHORG_TELEMETRY_ENV_BAKED(DockerfileARG DEPLOYMENT_ENVbaked at build; CI setsprod/pre-release/dev).TelemetryConfig.environment(default"dev").
Docker daemon enrichment¶
At startup via synthorg.telemetry.host_info.fetch_docker_info (uses aiodocker). Requires /var/run/docker.sock bind-mounted (sandbox overlay / synthorg init --sandbox true).
Allowlisted fields:
docker_server_version, docker_operating_system, docker_os_type, docker_os_version, docker_architecture, docker_kernel_version, docker_storage_driver, docker_default_runtime, docker_isolation, docker_ncpu, docker_mem_total, docker_gpu_runtime_nvidia_available.
When the socket isn't mounted or the daemon is unreachable, the event carries docker_info_available=False + a categorical docker_info_unavailable_reason.
Adding a new event or property¶
Keep in sync:
host_info._extract()(if sourcing from host info).- The
DockerHostInfoTypedDict (if a Docker field). - The scrubber's
_ALLOWED_PROPERTIESentry for the event type.
New allowlisted keys must not match a forbidden pattern. Never bypass the scrubber.