Skip to content

Feature Request: Secure OTEL Telemetry Export #625

Description

@Enclavet

Summary

Add an export subcommand that sends AI usage telemetry to a remote OpenTelemetry-compatible endpoint, authenticated via OIDC browser login with refresh tokens stored in the OS credential store.

Motivation

codeburn tracks AI usage locally per-developer. Teams want to aggregate this data across developers for:

  • Adoption dashboards — which tools/models are being used, by whom, how much
  • Budget tracking — team-level AI spend with alerts
  • ROI measurement — correlate AI usage with velocity/quality metrics

Today there's no standard way to get codeburn data off a developer's machine into a shared backend. Teams resort to custom scripts parsing codeburn report --format json output. A first-class export with secure auth solves this cleanly.

UX

Setup (one-time)

codeburn export setup https://metrics.example.com

What happens:

  1. Fetches https://metrics.example.com/.well-known/openid-configuration
  2. Opens system browser for OIDC Authorization Code + PKCE login
  3. Stores refresh token in OS credential store (Keychain / Credential Manager / libsecret)
  4. Saves { "baseUrl": "https://metrics.example.com" } to ~/.config/codeburn/export.json

That's it. One URL, zero prompts. The base URL serves as both the OIDC provider and the telemetry endpoint — .well-known provides auth config, and a known subpath (e.g. /v1/traces) receives OTEL data.

Export — Manual (on-demand)

# Export last 7 days (default)
codeburn export push

# Export specific period
codeburn export push --since 30d

# Dry-run (show what would be sent)
codeburn export push --dry-run

What happens:

  1. Reads refresh token from OS credential store
  2. Exchanges for access token (silent refresh, no browser)
  3. Collects ParsedProviderCall data for the period (same data as codeburn report)
  4. Sends as OTEL spans/metrics to {baseUrl}/v1/traces with Authorization: Bearer <access_token>
  5. Prints summary: "Exported 234 calls ($45.20) to metrics.example.com"

Export — Daemon (daily auto-export)

# Enable daily auto-export
codeburn export daemon start

# Check daemon status
codeburn export daemon status

# Disable
codeburn export daemon stop

What happens:

  1. Registers a daily background job (platform-native: launchd plist on macOS, systemd user timer on Linux, Task Scheduler on Windows)
  2. Runs once per day (configurable time, default: 9am local)
  3. Exports the previous day's activity (midnight-to-midnight)
  4. Silently succeeds or logs failures to ~/.cache/codeburn/export.log
  5. No terminal output, no user interaction — fully headless

The daemon uses the same stored refresh token as manual export. If the token expires and can't refresh, it logs a warning and stops retrying until the user runs codeburn export setup again.

// ~/.config/codeburn/export.json (with daemon enabled)
{
  "baseUrl": "https://metrics.example.com",
  "clientId": "codeburn-cli",
  "lastExport": "2026-07-05T14:00:00Z",
  "daemon": {
    "enabled": true,
    "schedule": "09:00",
    "lastRun": "2026-07-05T09:00:02Z",
    "lastStatus": "ok"
  }
}

Status / Logout

# Check config and auth status
codeburn export status
# Output: Endpoint: https://metrics.example.com | Auth: valid (expires in 47d) | Last export: 2h ago

# Remove stored credentials
codeburn export logout

Auth Design

OIDC Flow

  • Grant type: Authorization Code with PKCE (public/native client — no client secret)
  • Redirect URI: http://localhost:<ephemeral-port>/callback (temporary server during login, same pattern as aws sso login, gcloud auth login, gh auth login)
  • Scopes: openid offline_access (offline_access provides refresh token)
  • Token refresh: Automatic on every export push — transparent to user
  • Re-login: Only required when refresh token expires or is revoked

Credential Storage

Platform Store Suggested implementation
macOS Keychain keytar or security CLI
Windows Credential Manager keytar or @aspect-build/wincred
Linux libsecret (GNOME Keyring / KWallet) keytar or secret-tool CLI

keytar is the cross-platform standard (used by VS Code, GitHub CLI, Azure CLI). If keytar native module is undesirable for a pure-JS tool, fall back to encrypted file with OS-derived key.

Token Lifetime and Rotation

Recommended refresh token lifetime: 90 days. Balances convenience (quarterly re-auth) against exposure risk for a low-blast-radius telemetry endpoint.

Rotation: supported but not required. OIDC providers vary:

Provider Rotation Notes
Auth0 / Okta / Azure AD ✅ On by default for public clients Issues new RT on each use, invalidates old
Keycloak / Ory Hydra / Zitadel ✅ Configurable Off by default, enable per-client
Cognito / Google ❌ Not supported RT is reusable for full lifetime

codeburn's behavior is provider-agnostic:

  1. After every token refresh, store whatever refresh token the server returns (new or same)
  2. If the server returns a new RT (rotation), overwrite the old one in keychain immediately
  3. If the server returns the same RT (no rotation), keep using it
  4. On invalid_grant error — don't retry, prompt user: "Export auth expired. Run codeburn export setup"

This works correctly with Cognito (no rotation, 90-day reusable token), Auth0 (rotation on, new RT each use), and everything in between. The client doesn't need to know whether rotation is active.

Server-side recommendation (for reference implementations):

  • Enable rotation if the provider supports it
  • Set reuse grace window to 30 seconds (handles network-drop retries)
  • Enable token family invalidation on reuse detection (theft signal)
  • For non-rotating providers (Cognito): rely on the 90-day expiry as the primary security boundary

Config File (non-secret)

// ~/.config/codeburn/export.json
{
  "baseUrl": "https://metrics.example.com",
  "clientId": "codeburn-cli",
  "lastExport": "2026-07-05T14:00:00Z"
}

No tokens, secrets, or credentials in this file. clientId is auto-discovered from .well-known response or defaults to "codeburn-cli".

Data Schema

Each ParsedProviderCall becomes one OTEL span with these attributes:

ai.provider        = "kiro" | "cursor" | "claude" | ...
ai.model           = "claude-sonnet-4-6"
ai.input_tokens    = 12500
ai.output_tokens   = 3200
ai.cost_usd        = 0.085
ai.project         = "sample-prism-d1-velocity"
ai.session_id      = "421b64df-..."
ai.tools           = ["Edit", "Bash", "Read"]
ai.speed           = "standard"
ai.cost_estimated  = true | false

Data minimization (critical)

NOT exported by default:

  • userMessage — contains actual prompts (PII, proprietary code context)
  • File paths — may leak repo structure
  • Bash commands — may contain secrets

Opt-in flag for teams that want it:

codeburn export push --include-messages

Server-side Contract

The server at baseUrl must implement:

  1. GET /.well-known/openid-configuration — standard OIDC discovery
  2. POST /v1/traces — accepts OTEL protobuf or JSON, validates Bearer token

This is intentionally minimal. Any OTEL-compatible backend (Grafana Alloy, AWS ADOT Collector, Datadog Agent) behind an OIDC-aware reverse proxy satisfies it.

.well-known requirement

If /.well-known/openid-configuration returns 404 or is unreachable, exit with an error:

Error: https://metrics.example.com/.well-known/openid-configuration not found.
The server must implement OIDC discovery. See https://openid.net/specs/openid-connect-discovery-1_0.html

No fallback, no prompts, no alternative auth methods. The server either supports the protocol or it doesn't.

Alternatives Considered

Approach Why not
API key in env var Insecure, easy to leak, no rotation
API key in config file File permissions are unreliable across OS
mTLS client certs Complex setup, cert rotation burden
Multiple auth methods Complexity for marginal gain — one protocol, one flow, zero config

OIDC + keychain is the same pattern as aws sso login, gcloud auth login, gh auth login — developers already understand it. If the server doesn't support .well-known, it doesn't support codeburn export.

Implementation Notes

Gap Detection and Backfill

The daemon exports "yesterday" — but what if yesterday's export failed, or the machine was off for a week?

Approach: watermark-based catch-up

The config tracks a lastSuccessfulExport timestamp (the newest data point successfully sent). On each daemon run:

  1. Compare lastSuccessfulExport to now
  2. If gap > 1 day, export the entire gap (capped at 30 days to avoid first-run flood)
  3. Update lastSuccessfulExport only after the server acknowledges receipt
{
  "baseUrl": "https://metrics.example.com",
  "lastSuccessfulExport": "2026-07-03T00:00:00Z"
}

If today is Jul 5 and last successful was Jul 3 → daemon exports Jul 3 + Jul 4 in one batch before exporting today's window.

First-run behavior: lastSuccessfulExport doesn't exist → export last 7 days as initial seed (configurable via --seed-days). Not the full 6-month history — that's what codeburn export push --since 6m is for.

Manual backfill: If someone was offline for months:

codeburn export push --since 90d

This explicitly backfills the gap. The daemon's 30-day cap prevents accidental flood; manual push has no cap (other than codeburn's 6-month data retention).

Deduplication: The server uses deduplicationKey (already present on every ParsedProviderCall) as an idempotency key. Re-exporting the same period is safe — duplicates are ignored server-side. This means gap-fill and manual push can overlap without double-counting.

Failure modes:

Scenario Behavior
Network down Log error, lastSuccessfulExport unchanged, retry next run
Token expired (refresh fails) Log warning, stop daemon retries, notify on next interactive codeburn run: "Export auth expired. Run codeburn export setup to re-authenticate."
Server returns 5xx Retry with backoff (3 attempts), then defer to next day
Server returns 409 (duplicate) Treat as success, advance watermark
Machine off for 2 weeks Next daemon run exports the full 14-day gap in one batch
Machine off for 2 months Daemon exports last 30 days, prints note: "Gap exceeds 30d. Run codeburn export push --since 60d to backfill fully."
  • The export should be idempotent — re-exporting the same period deduplicates via deduplicationKey as span ID
  • Batch size: send in chunks of ~1000 spans to avoid payload limits
  • Retry: exponential backoff on 429/5xx, fail-fast on 401 (trigger re-login prompt)
  • Offline queue: if export fails, optionally queue to ~/.cache/codeburn/export-queue/ for retry on next run
  • --since should respect the same 6-month lookback cap as codeburn report -p all

Future Extensions

  • Team enrollment — server returns team config (which fields to include, export frequency) during setup
  • Anonymization mode — hash session IDs and project names for privacy-sensitive deployments
  • Export hooks — trigger export automatically after each codeburn report / dashboard refresh

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions