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
93 lines
3.2 KiB
Python
93 lines
3.2 KiB
Python
"""Telemetry beacon: payload shape, anonymity and bucketing.
|
|
|
|
These tests never touch the network or a database: ``_build_payload`` is given
|
|
a ``None`` session (the user-count lookup degrades to 0) and a fixed install id,
|
|
so we can assert the PostHog-shaped envelope and that no secrets leak.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from bot.services.telemetry_worker import HEARTBEAT_EVENT, TelemetryWorker, _bucket_users
|
|
from config.settings import Settings
|
|
|
|
|
|
@pytest.fixture
|
|
def settings(monkeypatch) -> Settings:
|
|
# Provide the minimal required env so Settings() validates without a .env,
|
|
# keeping the test self-contained in CI.
|
|
monkeypatch.setenv("BOT_TOKEN", "1234567890:AA_secret_bot_token_value")
|
|
monkeypatch.setenv("POSTGRES_USER", "u")
|
|
monkeypatch.setenv("POSTGRES_PASSWORD", "Sup3rSecretDbPassw0rd")
|
|
monkeypatch.setenv("POSTGRES_DB", "d")
|
|
monkeypatch.setenv("ADMIN_IDS", "1")
|
|
return Settings()
|
|
|
|
|
|
def test_bucket_users_boundaries():
|
|
assert _bucket_users(0) == "0"
|
|
assert _bucket_users(1) == "1-10"
|
|
assert _bucket_users(10) == "1-10"
|
|
assert _bucket_users(11) == "11-50"
|
|
assert _bucket_users(50) == "11-50"
|
|
assert _bucket_users(200) == "51-200"
|
|
assert _bucket_users(1000) == "201-1000"
|
|
assert _bucket_users(5000) == "1001-5000"
|
|
assert _bucket_users(5001) == "5000+"
|
|
assert _bucket_users(99999) == "5000+"
|
|
|
|
|
|
def test_build_payload_shape(settings):
|
|
worker = TelemetryWorker(settings, None)
|
|
payload = asyncio.run(worker._build_payload(None, "install-123"))
|
|
|
|
assert payload["event"] == HEARTBEAT_EVENT
|
|
assert payload["distinct_id"] == "install-123"
|
|
assert payload["api_key"] == settings.TELEMETRY_API_KEY.strip()
|
|
|
|
props = payload["properties"]
|
|
for key in (
|
|
"app_version",
|
|
"app_version_tag",
|
|
"os",
|
|
"arch",
|
|
"python_version",
|
|
"locale",
|
|
"users_bucket",
|
|
"payment_providers",
|
|
"webapp_enabled",
|
|
"panel_configured",
|
|
):
|
|
assert key in props, f"missing property: {key}"
|
|
|
|
assert isinstance(props["payment_providers"], list)
|
|
# No DB session -> user count degrades to the smallest bucket.
|
|
assert props["users_bucket"] == "0"
|
|
# Person properties mirror the event properties so PostHog breakdowns work.
|
|
assert props["$set"]["app_version"] == props["app_version"]
|
|
assert props["$lib"] == "remnawave-minishop"
|
|
|
|
|
|
def test_payload_contains_no_secrets_or_pii(settings):
|
|
worker = TelemetryWorker(settings, None)
|
|
payload = asyncio.run(worker._build_payload(None, "install-123"))
|
|
blob = json.dumps(payload)
|
|
|
|
# The bot token and DB password must never appear in the beacon.
|
|
assert settings.BOT_TOKEN not in blob
|
|
assert settings.POSTGRES_PASSWORD not in blob
|
|
|
|
|
|
def test_delivery_disabled_without_endpoint_or_key(settings, monkeypatch):
|
|
worker = TelemetryWorker(settings, None)
|
|
assert worker._delivery_configured() is True
|
|
|
|
monkeypatch.setattr(settings, "TELEMETRY_API_KEY", "", raising=False)
|
|
assert worker._delivery_configured() is False
|
|
|
|
monkeypatch.setattr(settings, "TELEMETRY_API_KEY", "phc_x", raising=False)
|
|
monkeypatch.setattr(settings, "TELEMETRY_ENDPOINT", "", raising=False)
|
|
assert worker._delivery_configured() is False
|