Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions app/routes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import app as app_module
from app import db, limiter
from app.config.analytics_defaults import get_analytics_config
from app.models import (
DonationInteraction,
Invoice,
Expand Down Expand Up @@ -1047,6 +1048,7 @@ def delete_user(user_id):
def telemetry_dashboard():
"""Telemetry and analytics dashboard"""
installation_config = get_installation_config()
analytics_config = get_analytics_config()

# Get telemetry status
telemetry_data = {
Expand All @@ -1059,16 +1061,19 @@ def telemetry_dashboard():
}

# Get OTEL OTLP status
grafana_endpoint = analytics_config.get("otel_exporter_otlp_endpoint") or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "")
grafana_token = analytics_config.get("otel_exporter_otlp_token") or os.getenv("OTEL_EXPORTER_OTLP_TOKEN", "")
grafana_data = {
"enabled": bool(os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")) and bool(os.getenv("OTEL_EXPORTER_OTLP_TOKEN")),
"endpoint": os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
"token_set": bool(os.getenv("OTEL_EXPORTER_OTLP_TOKEN")),
"enabled": bool(grafana_endpoint) and bool(grafana_token),
"endpoint": grafana_endpoint,
"token_set": bool(grafana_token),
}

# Get Sentry status
sentry_dsn = analytics_config.get("sentry_dsn") or os.getenv("SENTRY_DSN", "")
sentry_data = {
"enabled": bool(os.getenv("SENTRY_DSN")),
"dsn_set": bool(os.getenv("SENTRY_DSN")),
"enabled": bool(sentry_dsn),
"dsn_set": bool(sentry_dsn),
"traces_rate": os.getenv("SENTRY_TRACES_RATE", "0.0"),
}

Expand Down
5 changes: 5 additions & 0 deletions app/telemetry/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
BASE_SCHEMA_KEYS = frozenset(
{
"install_id",
"telemetry_fingerprint",
"app_version",
"platform",
"os_version",
Expand All @@ -45,6 +46,7 @@ def is_detailed_analytics_enabled() -> bool:
def _build_base_telemetry_payload(event_kind: str) -> Dict[str, Any]:
from app.config.analytics_defaults import get_analytics_config
from app.utils.installation import get_installation_config
from app.utils.telemetry import get_telemetry_fingerprint

config = get_analytics_config()
inst = get_installation_config()
Expand All @@ -53,6 +55,7 @@ def _build_base_telemetry_payload(event_kind: str) -> Dict[str, Any]:
first_seen = inst.get_base_first_seen_sent_at() or now
payload = {
"install_id": inst.get_install_id(),
"telemetry_fingerprint": get_telemetry_fingerprint(),
"app_version": config.get("app_version", "unknown"),
"platform": platform.system(),
"os_version": platform.release(),
Expand Down Expand Up @@ -271,10 +274,12 @@ def send_analytics_event(user_id: Any, event_name: str, properties: Optional[Dic
return
from app.config.analytics_defaults import get_analytics_config
from app.utils.installation import get_installation_config
from app.utils.telemetry import get_telemetry_fingerprint

config = get_analytics_config()
enhanced = dict(properties or {})
enhanced["install_id"] = get_installation_config().get_install_id()
enhanced["telemetry_fingerprint"] = get_telemetry_fingerprint()
enhanced["environment"] = os.getenv("FLASK_ENV", "production")
enhanced["app_version"] = config.get("app_version")
enhanced["deployment_method"] = "docker" if os.path.exists("/.dockerenv") else "native"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setup(
name='timetracker',
version='5.1.0',
version='5.1.1',
packages=find_packages(),
include_package_data=True,
install_requires=[
Expand Down
27 changes: 23 additions & 4 deletions tests/test_telemetry_consent_and_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ def test_send_analytics_event_capture_when_opt_in(self, mock_send):
}
with patch("app.utils.installation.get_installation_config") as mock_inst:
mock_inst.return_value.get_install_id.return_value = "install-uuid-123"
send_analytics_event(1, "test.event", {"k": "v"})
with patch("app.utils.telemetry.get_telemetry_fingerprint", return_value="fp-abc-123"):
send_analytics_event(1, "test.event", {"k": "v"})
mock_send.assert_called_once()
call_kw = mock_send.call_args[1]
assert call_kw["identity"] == "1"
assert call_kw["event_name"] == "test.event"
assert call_kw["properties"].get("install_id") == "install-uuid-123"
assert call_kw["properties"].get("telemetry_fingerprint") == "fp-abc-123"


class TestBaseTelemetry:
Expand All @@ -56,9 +58,10 @@ def test_send_base_first_seen_idempotent(self):

with patch("app.utils.installation.get_installation_config", return_value=mock_inst):
with patch("app.telemetry.service.send_base_telemetry") as mock_send:
mock_send.return_value = True
r1 = send_base_first_seen()
r2 = send_base_first_seen()
with patch("app.utils.telemetry.get_telemetry_fingerprint", return_value="fp-abc-123"):
mock_send.return_value = True
r1 = send_base_first_seen()
r2 = send_base_first_seen()
assert mock_send.call_count == 1, "first_seen should be sent only once"
assert r1 is True
assert r2 is False
Expand Down Expand Up @@ -98,6 +101,22 @@ def test_send_base_heartbeat_calls_telemetry_with_schema(self):
assert "app_version" in call_payload
assert "platform" in call_payload

def test_base_payload_includes_telemetry_fingerprint(self):
"""Base telemetry payload includes both install_id and telemetry fingerprint."""
from app.telemetry.service import _build_base_telemetry_payload

mock_inst = MagicMock()
mock_inst.get_base_first_seen_sent_at.return_value = None
mock_inst.get_install_id.return_value = "install-uuid-123"

with patch("app.utils.installation.get_installation_config", return_value=mock_inst):
with patch("app.config.analytics_defaults.get_analytics_config", return_value={"app_version": "1.0.0"}):
with patch("app.utils.telemetry.get_telemetry_fingerprint", return_value="fp-abc-123"):
payload = _build_base_telemetry_payload("heartbeat")

assert payload["install_id"] == "install-uuid-123"
assert payload["telemetry_fingerprint"] == "fp-abc-123"


class TestInstallIdInPayloads:
"""install_id is stable and present where required."""
Expand Down
Loading