From 8938761f14c47ffc985a47f90f10ce5d03895546 Mon Sep 17 00:00:00 2001 From: lukeocodes Date: Fri, 8 May 2026 21:02:09 +0100 Subject: [PATCH] feat(telemetry): enable session tracking and flush envelopes on exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small additions to deepctl-telemetry so the dx-cli Sentry project gets release-health signal alongside errors: 1. `auto_session_tracking=True` in sentry_sdk.init. Already the SDK default, but spelling it out makes the contract explicit and defends against a future SDK release flipping the default off. 2. New `_flush_on_exit` registered via `atexit.register` after a successful init. Calls `sentry_sdk.flush(timeout=2.0)` so the session envelope (and any error envelope captured at the very tail of execution) actually leaves the process before Python tears down. Without this, short-lived `dg` invocations can lose their session record entirely. Wrapped in a bare except so a Sentry hang or network glitch can never block CLI exit. 2s is the maximum the user waits. After this lands, the Releases view in Sentry (https://deepgram.sentry.io/releases/?project=4510993603362816) starts showing per-version session counts and crash-free percentages — the 'is anyone actually using v0.2.X' dashboard. Errors-only contract is preserved: traces_sample_rate stays 0, profiles_sample_rate stays 0, no per-command instrumentation. Tests: 5 new cases in TestSessionFlush covering auto_session_tracking being set to True in init kwargs, atexit registration of _flush_on_exit, no atexit registration when telemetry is disabled, 2.0s flush timeout, and exception suppression in the flush path. --- .../src/deepctl_telemetry/client.py | 14 +++++ .../tests/unit/test_telemetry.py | 63 ++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/deepctl-telemetry/src/deepctl_telemetry/client.py b/packages/deepctl-telemetry/src/deepctl_telemetry/client.py index 5602854..516c173 100644 --- a/packages/deepctl-telemetry/src/deepctl_telemetry/client.py +++ b/packages/deepctl-telemetry/src/deepctl_telemetry/client.py @@ -2,6 +2,7 @@ from __future__ import annotations +import atexit import os import platform import sys @@ -62,6 +63,7 @@ def init_telemetry(config: Config) -> bool: release=f"deepctl@{cli_version}", environment="production", send_default_pii=False, + auto_session_tracking=True, traces_sample_rate=0.0, profiles_sample_rate=0.0, max_breadcrumbs=20, @@ -76,10 +78,22 @@ def init_telemetry(config: Config) -> bool: ) sentry_sdk.set_tag("cli.version", cli_version) + atexit.register(_flush_on_exit) + _initialized = True return True +def _flush_on_exit() -> None: + """Flush queued envelopes before process exit (best-effort, 2s budget).""" + try: + import sentry_sdk + + sentry_sdk.flush(timeout=2.0) + except Exception: + pass + + def _read_cli_version() -> str: import importlib.metadata diff --git a/packages/deepctl-telemetry/tests/unit/test_telemetry.py b/packages/deepctl-telemetry/tests/unit/test_telemetry.py index 15941a9..0f50e26 100644 --- a/packages/deepctl-telemetry/tests/unit/test_telemetry.py +++ b/packages/deepctl-telemetry/tests/unit/test_telemetry.py @@ -2,9 +2,11 @@ from __future__ import annotations -from unittest.mock import Mock +from unittest.mock import Mock, patch -from deepctl_telemetry import is_enabled, render_notice +import pytest + +from deepctl_telemetry import init_telemetry, is_enabled, render_notice from deepctl_telemetry.client import DISABLE_ENV_VAR @@ -35,3 +37,60 @@ def test_on_message(self) -> None: def test_off_message(self) -> None: notice = render_notice(_config(False)) assert "Telemetry is off" in notice + + +class TestSessionFlush: + """Verify init_telemetry enables session tracking and registers atexit flush.""" + + @pytest.fixture(autouse=True) + def reset_initialized(self) -> None: + from deepctl_telemetry import client + + client._initialized = False + yield + client._initialized = False + + def test_init_enables_auto_session_tracking(self) -> None: + with patch("deepctl_telemetry.client.atexit.register"), patch( + "sentry_sdk.init" + ) as mock_init, patch("sentry_sdk.set_tag"): + init_telemetry(_config(True)) + + assert mock_init.called + kwargs = mock_init.call_args.kwargs + assert kwargs["auto_session_tracking"] is True + assert kwargs["traces_sample_rate"] == 0.0 + assert kwargs["profiles_sample_rate"] == 0.0 + + def test_init_registers_atexit_flush(self) -> None: + from deepctl_telemetry.client import _flush_on_exit + + with patch( + "deepctl_telemetry.client.atexit.register" + ) as mock_register, patch("sentry_sdk.init"), patch("sentry_sdk.set_tag"): + init_telemetry(_config(True)) + + mock_register.assert_called_once_with(_flush_on_exit) + + def test_disabled_does_not_register_atexit(self) -> None: + with patch( + "deepctl_telemetry.client.atexit.register" + ) as mock_register, patch("sentry_sdk.init") as mock_init: + init_telemetry(_config(False)) + + assert not mock_init.called + assert not mock_register.called + + def test_flush_on_exit_calls_sentry_flush_with_2s_budget(self) -> None: + from deepctl_telemetry.client import _flush_on_exit + + with patch("sentry_sdk.flush") as mock_flush: + _flush_on_exit() + + mock_flush.assert_called_once_with(timeout=2.0) + + def test_flush_on_exit_swallows_exceptions(self) -> None: + from deepctl_telemetry.client import _flush_on_exit + + with patch("sentry_sdk.flush", side_effect=RuntimeError("boom")): + _flush_on_exit()