Skip to content

fix(telemetry): explicitly start session after init#74

Merged
lukeocodes merged 1 commit intomainfrom
fix/telemetry-session-mode
May 9, 2026
Merged

fix(telemetry): explicitly start session after init#74
lukeocodes merged 1 commit intomainfrom
fix/telemetry-session-mode

Conversation

@lukeocodes
Copy link
Copy Markdown
Member

The bug

#71 enabled auto_session_tracking=True expecting sentry-sdk to start sessions automatically. It doesn't — in sentry-sdk 2.x, that flag only tells integrations (Flask, Django, ASGI request handlers) to wrap each request in a session. CLIs have no integration that knows when an 'application session' begins, so no session is ever created.

Confirmed via SENTRY_DEBUG=1: the worker flushed but no Sending envelope line ever appeared, and the Releases dashboard showed zero deepctl@* releases despite #71 having shipped.

The fix

One line — call sentry_sdk.start_session() right after init. Default session_mode is already "application" per scope.py L1558. The atexit handler from #71 then catches the session in its flush.

sentry_sdk.init(...)             # unchanged
sentry_sdk.set_tag(...)          # unchanged
sentry_sdk.start_session()       # NEW
atexit.register(_flush_on_exit)  # unchanged

Proof

Pre-fix SENTRY_DEBUG=1 (worker flushes empty queue):

[sentry] DEBUG: Flushing HTTP transport
[sentry] DEBUG: background worker got flush request
[sentry] DEBUG: background worker flushed
[sentry] DEBUG: atexit: got shutdown signal
[sentry] DEBUG: Killing HTTP transport

Post-fix:

[sentry] DEBUG: atexit: got shutdown signal
[sentry] DEBUG: Flushing HTTP transport
[sentry] DEBUG: Sending envelope [envelope with 1 items (session)] project:4510993603362816 host:o206115.ingest.us.sentry.io

The session envelope ships to the dx-cli project (id 4510993603362816).

Bonus catch — the silent failure that masked this

While diagnosing, my first attempt was session_mode="application" as an init kwarg. sentry-sdk 2.59.0 raises TypeError: Unknown option 'session_mode' for that, which the try/except: pass wrapper in src/deepctl/main.py silently swallowed. The CLI ran fine; telemetry was just dead.

Added a regression test (assert 'session_mode' not in kwargs) so a future kwargs change can't reintroduce that failure.

Tests

11 passed in 0.04s

New: test_init_starts_session asserts sentry_sdk.start_session() is called once after init. Updated test_init_enables_auto_session_tracking with the session_mode regression guard.

PR #71 enabled auto_session_tracking=True expecting sentry-sdk to start
sessions automatically. It doesn't — in sentry-sdk 2.x, that flag only
tells *integrations* (Flask/Django/ASGI request handlers) to wrap each
request in a session. For CLIs there is no integration that knows when
an 'application session' begins, so no session is ever created and the
atexit flush has nothing to send.

Confirmed by SENTRY_DEBUG=1: the worker flushed but no 'Sending envelope'
line ever appeared, and Sentry's Releases dashboard showed zero
deepctl@* releases despite #71 having shipped.

The fix is one line: call sentry_sdk.start_session() right after init.
Default session_mode is already 'application' (per scope.py L1558).
The atexit handler from #71 then catches the session in its flush.

Verified locally with SENTRY_DEBUG=1, post-fix output now includes:

  [sentry] DEBUG: Sending envelope [envelope with 1 items (session)]
  project:4510993603362816 host:o206115.ingest.us.sentry.io

Tests: extends TestSessionFlush with test_init_starts_session asserting
sentry_sdk.start_session() is called, plus a guard in
test_init_enables_auto_session_tracking that 'session_mode' is NOT in
init kwargs (caught a regression: 'session_mode' is rejected by
sentry-sdk 2.59.0 as TypeError: Unknown option, which the try/except
in main.py was silently swallowing).
@lukeocodes lukeocodes merged commit ad75efa into main May 9, 2026
38 checks passed
@lukeocodes lukeocodes deleted the fix/telemetry-session-mode branch May 9, 2026 08:59
lukeocodes added a commit that referenced this pull request May 9, 2026
)

## Summary

Stacked on **#74** (`fix/telemetry-session-mode`). Three commits, one
logical change: take the CLI's Sentry telemetry from "errors-only" to a
full observability + product-analytics surface.

## What changed

### 1. SDK init kwargs — turn the dials on
(`packages/deepctl-telemetry/.../client.py`)

| Init kwarg | Before | After |
|---|---|---|
| `traces_sample_rate` | 0.0 | 1.0 |
| `profiles_sample_rate` | 0.0 | 1.0 |
| `enable_logs` | (default False) | True |
| `attach_stacktrace` | (default False) | True |
| `max_breadcrumbs` | 20 | 100 |
| `send_default_pii` | False | False (unchanged) |
| `before_send=_scrub_event` | yes | yes (unchanged) |

### 2. Transaction wrap (`src/deepctl/main.py`)

`cli()` is now invoked inside a
`sentry_sdk.start_transaction(op="cli.command")`. Initial name is a
fixed `"cli"` placeholder — `BaseCommand.execute` renames it once Click
has dispatched. (Previous heuristic in `_safe_command_name` produced
`json` for `dg --output json whoami` because flag values look like
command names. Dropped.)

### 3. Per-command usage tags
(`packages/deepctl-core/.../base_command.py`)

`BaseCommand.execute` now wraps `handle()` with two telemetry hooks:

- `_tag_telemetry_start(ctx)` — runs **before** handle. Renames the
active transaction to `ctx.command_path` (so `dg debug audio` becomes
`deepctl debug audio` instead of just `debug`), and tags the scope with:
- `cmd.flags` — sorted comma-separated list of flag **names** the user
passed (e.g. `diarize,model,output`). Never values. Sentinel `(none)`
when no flags were used.
  - `cmd.output_format` — one of `json | yaml | table | csv | default`.
- `_tag_telemetry_status(status)` — runs **after** handle. Tags
`cmd.status` with `ok`, `cancelled`, `error`, or whatever the
`BaseResult.status` enum returned.

Both methods are wrapped in bare `except Exception: pass` so a Sentry
hiccup, missing scope, or unknown Click parameter-source enum value can
never crash the user's command.

## What you'll see in Sentry per dg invocation

Per `dg listen URL --diarize --model nova-3 -o json`:

```
transaction:
  op:    cli.command
  name:  deepctl listen           ← from ctx.command_path
  tags:
    cli.os:          darwin
    cli.arch:        arm64
    cli.python:      3.13
    cli.version:     0.2.23
    cmd.flags:       diarize,model,output
    cmd.output_format: json
    cmd.status:      ok
```

Plus a session envelope (release health), profile envelope (when sampled
long enough), and any log envelopes for `logging.warning`+ records.

## Privacy

- Flag **names** only, never values. `--model nova-3` becomes
`cmd.flags: model`, not `cmd.flags: model=nova-3`.
- `cmd.output_format` is a bounded enum.
- `cmd.status` is a bounded enum.
- `transaction.name` is `ctx.command_path` — the dispatch tree, not user
input.
- `send_default_pii=False` and `before_send=_scrub_event` continue to
scrub request bodies, headers, cookies, and user identifiers from error
events.

## Proof

```
$ SENTRY_DEBUG=1 dg --output json whoami 2>&1 | grep -iE '(transaction <|envelope)'
[sentry] DEBUG: [Tracing] Starting <cli.command> transaction <cli>
[sentry] DEBUG: Sending envelope [envelope with 1 items (transaction)] project:4510993603362816 host:o206115.ingest.us.sentry.io
[sentry] DEBUG: Sending envelope [envelope with 1 items (internal)]    project:4510993603362816 host:o206115.ingest.us.sentry.io
[sentry] DEBUG: Sending envelope [envelope with 1 items (session)]     project:4510993603362816 host:o206115.ingest.us.sentry.io
```

`<cli>` is the placeholder — the transaction is renamed to `deepctl
whoami` by `_tag_telemetry_start` once Click dispatches. Sentry
serialises the final state on send, so the actual envelope arrives with
the resolved name.

## Tests

| File | Class | Cases |
|---|---|---|
| `packages/deepctl-telemetry/tests/unit/test_telemetry.py` |
`TestSessionFlush::test_init_enables_full_observability_stack` (renamed)
| asserts every new kwarg landed + `session_mode not in kwargs`
regression guard |
| `packages/deepctl-core/tests/unit/test_base.py` |
`TestTelemetryTagging` (new) | 10 cases — transaction rename, flag-name
extraction (only COMMANDLINE-sourced params), `(none)` sentinel for zero
flags, output format default, parametrised status pass-through,
exception suppression on both methods |

```
21 passed (telemetry: 11, base_command: 10)
```
@github-actions github-actions Bot mentioned this pull request May 9, 2026
lukeocodes added a commit that referenced this pull request May 9, 2026
Adds 19 unit tests across the BaseCommand seam and the three commands
this PR touches:

packages/deepctl-core/tests/unit/test_base.py
- TestIsGuided (4 cases) — bare invocation, COMMANDLINE arg breaks,
  ENVIRONMENT var breaks, _agentic short-circuits
- TestGuidedAttribute (3 cases) — __init__ defaults to True, execute()
  sets _guided=False when params come from COMMANDLINE, sets True for
  bare invocations
- TestConfirmPromptGating (7 cases) — confirm/prompt call click when
  guided + not agentic + ci_friendly; return default when not guided,
  agentic, or not ci_friendly; prompt with default=None still prompts
  even when not guided (pre-existing safety, now locked in)

packages/deepctl-cmd-login/tests/unit/test_login_command.py
- TestMaybePromptSkillsSetup (3 cases) — returns early when not guided,
  returns early when not tty, proceeds to detect_ai_clis when both

packages/deepctl-cmd-debug-browser/tests/unit/test_browser_command.py
- TestBrowserGuidedGate (2 cases) — guided invocation waits for Enter,
  non-guided opens browser immediately

The seam tests (TestConfirmPromptGating + TestGuidedAttribute) protect
every command using self.confirm/self.prompt — if the gate regresses,
all those commands silently break. Per-command tests verify the
high-impact behavioural changes (login skills-setup early return,
browser press-Enter skip).

Skills migrations are covered transitively by TestConfirmPromptGating
since the three direct click.* calls are now self.confirm calls.

Pre-existing test_output_result_* failures in TestBaseCommand are
unchanged (5 failures on main, same as flagged in #74).
lukeocodes added a commit that referenced this pull request May 9, 2026
## What

PR #68 fixed the "any arg = non-interactive" rule for `dg listen` only.
This PR extends the same rule uniformly to **every** command that
prompts.

## How

New `BaseCommand.is_guided(ctx)` helper — returns `True` only when the
user invoked with zero input (no positional, no flag, no env var, not in
agentic mode). `BaseCommand.execute` sets `self._guided` before calling
`handle()`.

Threaded through one seam plus three call-site fixes:

| Surface | Change |
|---|---|
| `BaseCommand.confirm` / `BaseCommand.prompt` | Now respect `_guided`
in addition to `_agentic` / `ci_friendly`. Every command using these
helpers gets the rule for free. |
| `dg login` `_maybe_prompt_skills_setup` | Bails on either non-tty OR
not-guided. (The other two login prompts are downstream of
`self.confirm` and inherit the gate.) |
| `dg skills` | 3 direct `click.confirm` / `click.prompt` calls migrated
to `self.confirm`. The `is_tty` skill-list prompt also gates on
`_guided` (existing 'non-TTY without --all installs all' fallback still
active). |
| `dg debug browser` | "Press Enter to open the debugger" prompt skipped
when not guided. Browser still opens automatically. (Per Luke: "should
have a flag to instantly open it" — `--non-interactive` is already that
flag.) |
| `dg listen` | **Unchanged.** Its local `guided_flow` boolean captures
listen-specific intent more precisely than the generic `_guided` (was
`_interactive_select_source` actually run?). Tests untouched. |

## Tests

```
4 passed (TestIsGuided)
```

- `test_bare_invocation_is_guided` — all params from DEFAULT/DEFAULT_MAP
→ guided
- `test_any_commandline_arg_breaks_guided` — one COMMANDLINE source
breaks it
- `test_env_var_breaks_guided` — ENVIRONMENT source also breaks it
- `test_agentic_short_circuits_to_false` — `_agentic=True` always
returns False

The 5 pre-existing `test_output_result_*` failures in `TestBaseCommand`
are unrelated and present on `main` (flagged in #74's body).

## Behaviour matrix

| Invocation | Outcome |
|---|---|
| `dg login` (bare) | guided login flow as before |
| `dg login --api-key X` | non-interactive: skips skills-setup prompt,
no profile-name prompt |
| `dg skills install` (bare in TTY) | shows numbered list + asks |
| `dg skills install --all` | installs all silently |
| `dg skills install --tool claude --non-interactive` | installs for
claude, no prompts |
| `dg debug browser` (bare) | press-Enter, then opens |
| `dg debug browser --port 9000` | opens immediately at port 9000 |
| `dg debug browser --non-interactive` | opens immediately |
lukeocodes pushed a commit that referenced this pull request May 9, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>0.2.24</summary>

## [0.2.24](v0.2.23...v0.2.24)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
* **telemetry:** track per-command usage via Sentry tags
([490b37e](490b37e))
* **telemetry:** turn on full Sentry observability for the CLI
([cc2c208](cc2c208))
* **web:** swap Sentry for Heap + GA4 on cli.deepgram.com
([e736528](e736528))
* **web:** swap Sentry for Heap + GA4 on cli.deepgram.com
([#76](#76))
([b12a55b](b12a55b))
* **web:** wire real GA4 measurement ID G-TYPC1TBCKT
([a6468e4](a6468e4))


### Bug Fixes

* **telemetry:** explicitly start session after init
([8dd2843](8dd2843))
* **telemetry:** explicitly start session after init
([#74](#74))
([ad75efa](ad75efa))
* uniform 'any arg = non-interactive' rule across all commands
([#78](#78))
([6370f32](6370f32))
</details>

<details><summary>deepctl-core: 0.2.12</summary>

##
[0.2.12](deepctl-core-v0.2.11...deepctl-core-v0.2.12)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
* **telemetry:** track per-command usage via Sentry tags
([490b37e](490b37e))


### Bug Fixes

* uniform 'any arg = non-interactive' rule across all commands
([#78](#78))
([6370f32](6370f32))
</details>

<details><summary>deepctl-shared-utils: 0.1.12</summary>

##
[0.1.12](deepctl-shared-utils-v0.1.11...deepctl-shared-utils-v0.1.12)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
</details>

<details><summary>deepctl-telemetry: 0.0.4</summary>

##
[0.0.4](deepctl-telemetry-v0.0.3...deepctl-telemetry-v0.0.4)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
* **telemetry:** turn on full Sentry observability for the CLI
([cc2c208](cc2c208))


### Bug Fixes

* **telemetry:** explicitly start session after init
([8dd2843](8dd2843))
* **telemetry:** explicitly start session after init
([#74](#74))
([ad75efa](ad75efa))
</details>

<details><summary>deepctl-cmd-login: 0.1.15</summary>

##
[0.1.15](deepctl-cmd-login-v0.1.14...deepctl-cmd-login-v0.1.15)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))


### Bug Fixes

* uniform 'any arg = non-interactive' rule across all commands
([#78](#78))
([6370f32](6370f32))
</details>

<details><summary>deepctl-cmd-debug-audio: 0.1.13</summary>

##
[0.1.13](deepctl-cmd-debug-audio-v0.1.12...deepctl-cmd-debug-audio-v0.1.13)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
</details>

<details><summary>deepctl-cmd-debug-browser: 0.1.12</summary>

##
[0.1.12](deepctl-cmd-debug-browser-v0.1.11...deepctl-cmd-debug-browser-v0.1.12)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))


### Bug Fixes

* uniform 'any arg = non-interactive' rule across all commands
([#78](#78))
([6370f32](6370f32))
</details>

<details><summary>deepctl-cmd-debug-network: 0.1.12</summary>

##
[0.1.12](deepctl-cmd-debug-network-v0.1.11...deepctl-cmd-debug-network-v0.1.12)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
</details>

<details><summary>deepctl-cmd-update: 0.2.5</summary>

##
[0.2.5](deepctl-cmd-update-v0.2.4...deepctl-cmd-update-v0.2.5)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
</details>

<details><summary>deepctl-cmd-plugin: 0.1.12</summary>

##
[0.1.12](deepctl-cmd-plugin-v0.1.11...deepctl-cmd-plugin-v0.1.12)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
</details>

<details><summary>deepctl-cmd-skills: 0.0.6</summary>

##
[0.0.6](deepctl-cmd-skills-v0.0.5...deepctl-cmd-skills-v0.0.6)
(2026-05-09)


### Bug Fixes

* uniform 'any arg = non-interactive' rule across all commands
([#78](#78))
([6370f32](6370f32))
</details>

<details><summary>deepctl-cmd-listen: 0.0.13</summary>

##
[0.0.13](deepctl-cmd-listen-v0.0.12...deepctl-cmd-listen-v0.0.13)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
</details>

<details><summary>deepctl-cmd-completion: 0.0.3</summary>

##
[0.0.3](deepctl-cmd-completion-v0.0.2...deepctl-cmd-completion-v0.0.3)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
</details>

<details><summary>deepctl-plugin-example: 0.1.12</summary>

##
[0.1.12](deepctl-plugin-example-v0.1.11...deepctl-plugin-example-v0.1.12)
(2026-05-09)


### Features

* **telemetry:** full Sentry observability + per-command usage tags
([#75](#75))
([0fe43d2](0fe43d2))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant