Skip to content

feat: CLI telemetry for all commands + crash reporting#122

Open
nicknisi wants to merge 10 commits intomainfrom
nicknisi/telemetry
Open

feat: CLI telemetry for all commands + crash reporting#122
nicknisi wants to merge 10 commits intomainfrom
nicknisi/telemetry

Conversation

@nicknisi
Copy link
Copy Markdown
Member

@nicknisi nicknisi commented Apr 14, 2026

Summary

Right now we only have telemetry on the installer flow. This PR adds telemetry coverage to every CLI command, plus crash reporting and a debug flag.

What changed:

  • Event type enrichment -- session.start/end now carry environment fingerprint (OS, Node version, CI detection, shell). Step and tool events get explicit startTimestamp for accurate span reconstruction on the backend. Two new event types: command and crash.

  • Command telemetry for all commands -- every yargs command (both registerSubcommand and inline handlers) now emits a command event with canonical name, duration, success/failure, and which flags were used. Alias resolution normalizes org -> organization, claim -> env.claim so metrics don't fragment.

  • Crash reporting -- global uncaughtException/unhandledRejection handlers capture unhandled crashes with sanitized stack traces (home dir stripped, truncated to 4KB). Handlers are synchronous to avoid Node's async-handler footgun.

  • Store-and-forward -- events are persisted to a PID-based temp file on process exit via writeFileSync. Next CLI invocation recovers and sends them. Handles the ~25 locations that call process.exit() directly by queuing a provisional event in the middleware before the handler runs.

  • Flush improvements -- flush() now returns a boolean (sent vs retryable), uses splice instead of clearing the whole queue (protects events queued during an in-flight fetch), drops events on 4xx (permanent failures like 401 won't accumulate), and retains on 5xx/network errors for store-forward. Commands flush in-process via the handler wrapper rather than always deferring to next invocation.

  • WORKOS_DEBUG=1 -- enables verbose debug logging for all commands (not just the installer's --debug flag). Shows telemetry event details on flush.

Design decisions worth noting:

  • Non-installer telemetry only works for JWT-authenticated users. API-key-only users don't have a token the LLM gateway guard accepts, so their events are silently dropped. Acceptable for v1.
  • The install, dashboard, and default $0 commands are excluded from command-level telemetry since they have their own session-based telemetry.
  • The crash reporter is sync-only -- it queues the event and relies on store-forward's exit handler to persist it. No async flush attempts in the crash path.

nicknisi added 10 commits April 14, 2026 14:05
…amps, and new event types

Add environment fingerprint fields (OS, Node version, CI detection, shell)
to session.start and session.end events. Add startTimestamp to step and
agent.tool events for span reconstruction. Define command and crash event
types with stub emission methods. Add discriminated union Zod schema
validation tests mirroring the API schema.
… persistence

Wire up yargs middleware that emits a provisional command event before each
handler runs, then replaces it with actual duration/success on completion.
This covers the ~25 process.exit() call sites without modifying them.

- Command telemetry middleware with canonical name resolution and flag extraction
- Crash reporter with sanitized stack traces (sync handlers, no async)
- Store-forward: persist unsent events to temp file on exit, recover on next run
- Fix flush() to retain events until HTTP success (was clearing before fetch)
- Auto-wrap handlers in registerSubcommand() (single change point)
- Shared COMMAND_ALIASES map for telemetry and help-json
- analytics.initForNonInstaller() sets gatewayUrl + JWT from stored creds
- Enable debug output for non-installer commands via env var
- Log telemetry event details (type, name, duration, attributes) on flush
- Register in debug command's env var catalog
- Wrap inline command handlers (seed, setup-org, doctor, etc.) with
  wrapCommandHandler so they report real duration/success
- Skip provisional telemetry event for install command (has own session telemetry)
- Add claim -> env.claim to canonical alias map
- Defer store-forward file deletion until after successful flush
Client errors (401, 403) are permanent failures that won't succeed on
retry. Only retain events for 5xx (transient server errors) and network
failures where store-forward retry is meaningful.
- flush() returns true (sent/dropped) or false (retryable) so callers
  can act on the result
- Use splice(0, count) instead of clearing all events, protecting
  events queued concurrently during the fetch
- wrapCommandHandler flushes in-process so events are sent immediately
  instead of always deferring to next invocation via store-forward
- Store-forward recovery deletes files after loading into memory
  (events are re-persisted by exit handler if flush fails)
- Skip provisional events for dashboard and $0 (installer entry points)
- Add 4xx drop test coverage
Add a section to CLAUDE.md explaining which commands auto-emit telemetry
(registerSubcommand) versus which need manual wrapCommandHandler wrapping
(inline top-level .command() calls). Add a pointer comment in bin.ts near
the workflow commands block.

Prevents new top-level commands from silently emitting duration=0 telemetry.
- Add workos.user_id to command and crash events (from stored credentials
  or unclaimed environment clientId) so dashboards can count unique users
- Add cli.version to command and crash events for release adoption tracking
- Support claim-token auth path on the telemetry client, so unclaimed
  environments' telemetry reaches the API (guard accepts this path too)
- Rename CrashEvent's installer.version to cli.version (crashes happen
  outside the installer too)
- initForNonInstaller() now wires up user_id and claim-token auth
Closes security-audit finding #1 on PR #122 (telemetry message
sanitization). `error.message` was flowing into 4 capture sites
unsanitized, leaking homedir paths (and rarely, credentials) to the
WorkOS gateway.

- Add `sanitizeMessage()` in crash-reporter.ts: homedir strip + Bearer/
  sk_*/JWT redaction + 1KB truncation.
- Factor secret redaction into shared `redactSecrets()` used by both
  `sanitizeMessage` and `sanitizeStack` (Node echoes `.message` into the
  leading `Error.stack` line, so message-only sanitization was
  insufficient).
- Add private `extractErrorFields()` chokepoint on `Analytics`; route
  all 4 capture sites through it (`captureException`, `stepCompleted`,
  `commandExecuted`, `captureUnhandledCrash`). `replaceLastCommandEvent`
  inherits sanitization via its delegation to `commandExecuted`.
- `captureUnhandledCrash` now uses `sanitizeStack` instead of inline
  truncation, providing defense-in-depth for callers that bypass the
  crash-reporter wrapper.
- Add regression guard test (`telemetry-sanitize.spec.ts`): poisons
  every capture method with homedir + Bearer + sk_live_ + JWT, asserts
  no marker reaches the serialized queue.

Reviewed: ideation:reviewer cycle 1 PASS (0 critical, 0 high).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant