Skip to content

Add session analytics via Claude Code OpenTelemetry#137

Merged
dhilgaertner merged 5 commits intomainfrom
feature/crow-136-session-analytics-otel
Apr 21, 2026
Merged

Add session analytics via Claude Code OpenTelemetry#137
dhilgaertner merged 5 commits intomainfrom
feature/crow-136-session-analytics-otel

Conversation

@dhilgaertner
Copy link
Copy Markdown
Contributor

@dhilgaertner dhilgaertner commented Apr 8, 2026

Summary

Adds session analytics to Crow by running an in-process OTLP HTTP/JSON receiver that ingests telemetry exported by Claude Code. Per-session metrics (cost, tokens, tool usage, active time, lines changed) appear in the session detail header.

Closes #136

What's new

New package: CrowTelemetry

Self-contained, zero third-party dependencies:

  • OTLPReceiverNWListener HTTP server bound to loopback only, handles POST /v1/metrics and POST /v1/logs
  • HTTPParser — Minimal HTTP/1.1 parser (partial-read buffering, Content-Length handling)
  • OTLPModels — Codable types for OTLP HTTP/JSON payloads
  • TelemetryDatabase — SQLite actor with metrics, events, and session_map tables (WAL mode)
  • TelemetryService — Public facade coordinating receiver + database

New dependencies

Only system frameworks — no SPM packages added:

  • SQLite3 (import SQLite3) — first SQLite usage in the project. Telemetry DB at ~/Library/Application Support/crow/telemetry.db.
  • Network framework (NWListener) — first inbound-HTTP usage in Crow (previously only Unix domain sockets via CrowIPC).

Integration

  • Env var injection: Both launch paths (SessionService.launchClaude() for restored sessions and the send RPC handler for new sessions created via /crow-workspace) prepend OTEL env vars to the claude command.
  • Session mapping: OTEL_RESOURCE_ATTRIBUTES=crow.session.id={UUID} reliably correlates OTLP data with Crow sessions.
  • UI: SessionAnalyticsStrip renders as Row 4 in SessionDetailView when data exists (cost, tokens, tools, active time, lines, errors).
  • Lifecycle cleanup: onDeleteSession purges all telemetry for that session.

Settings (Settings → General → Telemetry)

  • Enable toggle — off by default, opt-in
  • OTLP receiver port — defaults to 4318
  • Retention window — 30 days / 90 days / 6 months (default) / 1 year / Forever. Pruned on app launch to bound disk growth.

Architecture decisions

  • Loopback-only (NWListener with acceptLocalOnly=true) — no firewall exposure
  • Opt-in with a 6-month retention default — bounds disk growth (~0.5 MB/day per active session)
  • SessionAnalytics data struct lives in CrowCore so CrowUI doesn't need to depend on CrowTelemetry
  • Per-session @Observable scoping via SessionHookState.analytics — only the visible session re-renders

Commits in this PR

Test plan

  • make build — clean compile
  • OTLP receiver starts and binds to configured port
  • Env vars inject correctly (env \| grep OTEL in session terminal)
  • Analytics strip appears in session header after Claude Code activity
  • Works for both new sessions (via crow send) and restored sessions
  • Port conflict → graceful failure with log message, no crash
  • Session deletion → telemetry data removed from SQLite
  • Retention pruning removes rows older than configured window on launch
  • Telemetry disabled → no receiver, no env vars, no DB writes

Release considerations

  • Safe default: telemetry ships disabled — zero user impact until opted in
  • Forward-compatible config: existing config.json files decode correctly (new fields fall back to defaults)
  • New on-disk artifact: telemetry.db created only when feature is enabled
  • Performance footprint: ~3-5 MB RAM, near-zero idle CPU, ~0.5 MB/day disk per active session — well below noise threshold

🤖 Generated with Claude Code

@dhilgaertner dhilgaertner requested a review from dgershman as a code owner April 8, 2026 19:56
@dhilgaertner dhilgaertner force-pushed the feature/crow-136-session-analytics-otel branch from 47f9529 to 1360e14 Compare April 8, 2026 19:59
dhilgaertner and others added 2 commits April 14, 2026 12:37
Adds an in-process OTLP HTTP/JSON receiver so Crow can ingest telemetry
from Claude Code sessions and surface per-session analytics in the UI.

New CrowTelemetry package:
- OTLP HTTP receiver using NWListener on localhost
- SQLite storage for metrics and events
- TelemetryService facade coordinating receiver + database

Integration:
- Injects OTEL env vars when launching Claude Code sessions
- Maps sessions via OTEL_RESOURCE_ATTRIBUTES=crow.session.id
- Analytics strip in session detail header (cost, tokens, tools, time)
- Telemetry settings in preferences (enable/disable, port config)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs fixed:

1. New sessions via `crow send` were missing OTEL env vars entirely.
   The send RPC handler now injects them when it detects a managed
   terminal receiving a claude command (same path that writes hooks).

2. The telemetry onDataReceived callback referenced self.telemetryService
   which was nil because it was assigned after start(). Restructured so
   init + assignment happens synchronously before start() runs in a Task.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dhilgaertner dhilgaertner force-pushed the feature/crow-136-session-analytics-otel branch from 50c9087 to 766ba88 Compare April 14, 2026 17:38
Users enable session analytics via Settings → General → Telemetry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@dgershman dgershman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code & Security Review

Critical Issues

None found — this is a well-structured feature addition.

Security Review

Strengths:

  • OTLP receiver binds to NWEndpoint.hostPort(host: .ipv4(.loopback)) with params.acceptLocalOnly = true — no external network exposure
  • Session correlation uses UUID (from crow.session.id resource attribute), preventing cross-session data leakage
  • SQLite writes use parameterized queries (sqlite3_bind_*) throughout insertMetric, insertEvent, registerSessionMapping, sumMetric, sumMetricWithAttribute, and countEvents — no SQL injection risk on the write/read paths
  • Telemetry is opt-in (disabled by default), respecting user privacy
  • Input validation: only POST requests are accepted; unknown paths return 404; malformed payloads return 400

Concerns:

  • TelemetryDatabase.swift:215-217deleteSessionData uses string interpolation ('\(sid)') instead of parameterized queries. While sid comes from UUID.uuidString (which is always safe hex+hyphens), this breaks the otherwise consistent pattern of parameterized queries. Consider using sqlite3_bind_text for consistency and defense-in-depth.

Code Quality

Well done:

  • Clean separation of concerns: OTLPReceiverTelemetryDatabaseTelemetryServiceAppDelegate wiring
  • TelemetryDatabase is an actor, providing safe concurrent access
  • WAL mode enabled for SQLite — good choice for concurrent reads during UI updates
  • OTLP model types are comprehensive and correctly handle OTLP's quirks (int64-as-string encoding)
  • HTTP parser has sensible bounds checking (8KB header limit)
  • Session cleanup wired into onDeleteSession — no data leak when sessions are removed
  • SessionService properly passes telemetry port for both new sessions (send handler) and restored sessions (launchClaude)
  • Analytics strip UI is conditionally shown only when data exists

Minor observations:

  • HTTPResponse.badRequest at HTTPParser.swift:38 uses string interpolation for the JSON error body ("{\"error\":\"\(message)\"}"). If a decode error message contains quotes or special characters, this could produce malformed JSON. Low risk since these messages are only seen by the local OTLP client, but using JSONSerialization would be more robust.
  • No unit tests are included for the new CrowTelemetry package (test target declared in Package.swift but no test files). Given the SQLite query logic in sessionAnalytics() and the HTTP parser, tests would add confidence.
  • The onDataReceived callback in AppDelegate.swift:267-272 creates a new Task per telemetry data event. Under high telemetry throughput, this could queue many analytics(for:) DB reads. Not likely a real issue given the local single-client scenario, but worth noting.

Summary Table

Priority Issue
🟡 Yellow deleteSessionData uses string interpolation instead of parameterized SQL — consistency concern (TelemetryDatabase.swift:215-217)
🟡 Yellow No unit tests for CrowTelemetry package (HTTP parser, DB queries, OTLP model decoding)
🟢 Green HTTPResponse.badRequest could produce malformed JSON with special characters in error messages (HTTPParser.swift:38)
🟢 Green Per-event Task creation in onDataReceived callback could be coalesced for efficiency

Recommendation: Approve — this is a clean, well-architected feature addition. The OTLP receiver is properly locked to localhost, telemetry is opt-in, and the code follows good patterns throughout. The yellow items are worth addressing in a follow-up but are not blocking.

@dhilgaertner dhilgaertner deleted the feature/crow-136-session-analytics-otel branch April 16, 2026 02:48
@dhilgaertner dhilgaertner restored the feature/crow-136-session-analytics-otel branch April 16, 2026 02:51
@dhilgaertner dhilgaertner reopened this Apr 16, 2026
dhilgaertner and others added 2 commits April 20, 2026 10:35
Bounds telemetry database growth with a configurable retention window,
defaulting to 6 months. Old metrics and events are pruned on app launch.
Presets in Settings: 30 days, 90 days, 6 months, 1 year, Forever.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dhilgaertner dhilgaertner merged commit 32e14b0 into main Apr 21, 2026
2 checks passed
@dhilgaertner dhilgaertner deleted the feature/crow-136-session-analytics-otel branch April 21, 2026 16:32
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.

Feature: Session Analytics & Metrics via Claude Code OpenTelemetry

2 participants