feat: anonymous opt-out install telemetry beacon

Add a once-a-day anonymous heartbeat (PostHog) so maintainers can see
active installs and version/OS breakdowns. Self-hosted friendly: opt out
via TELEMETRY_ENABLED in .env or the Admin -> System toggle (applied
without a restart), or by clearing the endpoint/key.

- Share version resolution in bot/utils/app_version.py so the admin
  sidebar and the beacon report the same build version
- TelemetryWorker sends an opaque install id plus coarse facts only
  (version, OS/arch, python, locale, enabled providers, user-count
  range); never tokens, domains or user data
- Register the worker in main_worker.py behind a Redis single-flight lock
- Expose TELEMETRY_* settings and an Admin -> System manifest toggle
- Document the payload and opt-out in docs/configuration/telemetry.md
- Cover bucketing, payload shape and anonymity with tests
This commit is contained in:
3252a8
2026-06-01 14:14:02 +03:00
parent 5bb1400917
commit a0ea2261f4
13 changed files with 546 additions and 79 deletions
+31
View File
@@ -1100,6 +1100,37 @@ class Settings(BaseSettings):
)
LOG_SUPPORT: bool = Field(default=True, description="Send support ticket notifications")
# Anonymous install telemetry (self-hosted friendly, opt-out).
TELEMETRY_ENABLED: bool = Field(
default=True,
description=(
"Send an anonymous daily install heartbeat (version, OS, locale, "
"user-count range). No personal data. Opt out here, via the web "
"admin, or by clearing TELEMETRY_ENDPOINT/TELEMETRY_API_KEY."
),
)
TELEMETRY_ENDPOINT: str = Field(
default="https://eu.i.posthog.com",
description="PostHog ingestion host. Empty disables telemetry.",
)
TELEMETRY_API_KEY: str = Field(
default="phc_sRiAbbrjhyYPfsgBwSZyLvujDXBLaDpmWKt6paGmCCMm",
description=(
"PostHog project API key (phc_...). Safe to ship in the image: it is "
"a write-only ingest key. Empty disables telemetry."
),
)
TELEMETRY_INTERVAL_HOURS: int = Field(default=24)
@property
def telemetry_configured(self) -> bool:
"""True when telemetry is enabled and has a delivery target."""
return bool(
self.TELEMETRY_ENABLED
and str(self.TELEMETRY_ENDPOINT or "").strip()
and str(self.TELEMETRY_API_KEY or "").strip()
)
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", extra="ignore", populate_by_name=True
)