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
127 lines
3.8 KiB
Python
127 lines
3.8 KiB
Python
"""Single source of truth for the application version string.
|
|
|
|
Resolution order mirrors the Dockerfile build chain so the dev checkout and
|
|
the runtime container agree on the value:
|
|
|
|
REMNAWAVE_MINISHOP_VERSION env > .build-version file > live ``git describe``
|
|
> ``dev+unknown``
|
|
|
|
The same value powers the admin sidebar (web process) and the anonymous
|
|
telemetry beacon (worker process), so "active installs" and version
|
|
breakdowns line up across both.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
# ``/app`` in the container (parent of ``/app/backend``); repo root in dev.
|
|
# Matches where the Dockerfile drops .build-version / .build-tag / .build-commit.
|
|
APP_ROOT = Path(__file__).resolve().parents[3]
|
|
|
|
_APP_VERSION_CACHE: Optional[str] = None
|
|
|
|
|
|
def _run_git_command(*args: str) -> str:
|
|
try:
|
|
result = subprocess.run(
|
|
["git", *args],
|
|
cwd=APP_ROOT,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=1.5,
|
|
)
|
|
except (OSError, subprocess.SubprocessError):
|
|
return ""
|
|
return result.stdout.strip()
|
|
|
|
|
|
def _normalize_version_branch(raw_branch: str) -> str:
|
|
branch = str(raw_branch or "").strip()
|
|
for prefix in ("refs/heads/", "refs/remotes/origin/", "origin/"):
|
|
if branch.startswith(prefix):
|
|
branch = branch[len(prefix) :]
|
|
break
|
|
if branch in ("", "HEAD"):
|
|
return ""
|
|
return re.sub(r"[^A-Za-z0-9._-]+", "-", branch).strip("-")[:48]
|
|
|
|
|
|
def _resolve_version_branch() -> str:
|
|
for env_name in (
|
|
"REMNAWAVE_MINISHOP_BRANCH",
|
|
"GIT_BRANCH",
|
|
"BRANCH_NAME",
|
|
"GITHUB_REF_NAME",
|
|
"CI_COMMIT_REF_NAME",
|
|
):
|
|
branch = _normalize_version_branch(os.getenv(env_name, ""))
|
|
if branch:
|
|
return branch
|
|
return _normalize_version_branch(
|
|
_run_git_command("branch", "--show-current")
|
|
or _run_git_command("symbolic-ref", "--quiet", "--short", "HEAD")
|
|
)
|
|
|
|
|
|
def _format_app_version(tag: str, sha: str, branch: str) -> str:
|
|
branch_suffix = "" if not branch or branch == "main" else f"-{branch}"
|
|
if tag and sha:
|
|
return f"{tag}{branch_suffix}+g{sha}"
|
|
if sha:
|
|
return f"dev{branch_suffix}+g{sha}"
|
|
if tag:
|
|
return f"{tag}{branch_suffix}"
|
|
return f"dev{branch_suffix}+unknown"
|
|
|
|
|
|
def _read_build_file(name: str) -> str:
|
|
try:
|
|
return (APP_ROOT / name).read_text(encoding="utf-8").strip()
|
|
except OSError:
|
|
return ""
|
|
|
|
|
|
def resolve_app_version() -> str:
|
|
"""Full version string (cached), e.g. ``v3.4.6+gabc1234``."""
|
|
global _APP_VERSION_CACHE
|
|
if _APP_VERSION_CACHE:
|
|
return _APP_VERSION_CACHE
|
|
|
|
env_version = os.getenv("REMNAWAVE_MINISHOP_VERSION", "").strip()
|
|
if env_version:
|
|
_APP_VERSION_CACHE = env_version
|
|
return env_version
|
|
|
|
build_version = _read_build_file(".build-version")
|
|
if build_version:
|
|
_APP_VERSION_CACHE = build_version
|
|
return build_version
|
|
|
|
tag = _run_git_command("describe", "--tags", "--abbrev=0")
|
|
sha = _run_git_command("rev-parse", "--short", "HEAD")
|
|
branch = _resolve_version_branch()
|
|
version = _format_app_version(tag, sha, branch)
|
|
_APP_VERSION_CACHE = version
|
|
return version
|
|
|
|
|
|
def resolve_app_version_tag() -> str:
|
|
"""Clean release tag for low-cardinality breakdowns, e.g. ``v3.4.6``.
|
|
|
|
Prefers the build-time ``.build-tag`` artifact, then a live ``git
|
|
describe``; falls back to the full version string when no tag is known.
|
|
"""
|
|
build_tag = _read_build_file(".build-tag")
|
|
if build_tag and build_tag != "unknown":
|
|
return build_tag
|
|
tag = _run_git_command("describe", "--tags", "--abbrev=0")
|
|
if tag:
|
|
return tag
|
|
return resolve_app_version()
|