Files
remnawave-minishop/tests/test_telemetry_worker.py
T
3252a8 a0ea2261f4 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
2026-06-01 14:14:02 +03:00

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