Files
codegraph/docs/design/telemetry.md
T
0a91d0f512 perf(resolution): fix O(K²) import-node blowup in "Resolving refs" (#915) (#965)
* perf(resolution): resolve imports to definitions, not sibling import nodes (#915)

"Resolving refs" crawled (tens of minutes) on large projects — most painfully
ones mixing a big front-end and back-end. An external package or module imported
across hundreds/thousands of files (react, a shared UI package, Python
logging/typing) is re-declared as an `import` node in every importing file, so
its unresolved import ref fell through to the exact-name matcher, which scored
all K same-named import nodes via findBestMatch — K refs x K candidates = O(K^2)
per package, producing only meaningless import->import edges.

Fix: exclude `import`-kind nodes as name-match targets (they're statements, not
definitions; real import->definition resolution is the import resolver's job).
Plus two safe constant-factor wins in findBestMatch: hoist the per-candidate
ref.filePath split, and skip cross-language candidates when a same-language one
exists (provably the same winner — same-language scores >=50, cross-language
maxes at 35).

Measured: superset (Py+TS) candidates scored 7.5M -> 833K (9x), non-import edges
preserved (+1618 now resolve to real defs), ~22K useless import->import edges
removed; kubernetes (Go) computePathProximity 37.2s -> 5.0s; synthetic 8k-file
mixed repo (K=4000) resolution 16.0s -> 1.7s. Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: correct stale better-sqlite3/wasm references to node:sqlite

The SQLite backend has been Node's built-in node:sqlite (real SQLite, WAL + FTS5,
from the bundled runtime) for a while — there is no native build step and no
node-sqlite3-wasm fallback. README and the docs site were already updated; this
catches the stragglers:

- CLAUDE.md: the src/db/ backend description and the sqlite-backend test note.
- src/db/index.ts, src/mcp/tools.ts: two code comments that still blamed "the
  wasm backend" for non-WAL behavior (reworded to "when WAL isn't in effect").

Leaves tree-sitter grammar wasm (web-tree-sitter / --liftoff-only) untouched —
that's a different, still-current use of wasm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(telemetry): drop the dead sqlite_backend field (schema v2)

node:sqlite is now the only backend, so the `index` event's `sqlite_backend`
field was a constant ("native") carrying no signal — and the `install` event
never actually sent it. Remove the field and the backendKind() helper, bump the
telemetry SCHEMA_VERSION 1 -> 2, and update TELEMETRY.md + docs/design/telemetry.md.

The ingest worker is deliberately left tolerant: `index` doesn't require the
field and schema_version validates as nonNegInt(99), so v2 events ingest fine and
old clients still sending v1 + sqlite_backend keep validating too. Added a legacy
comment there explaining it's safe to drop once old-client share is negligible.

telemetry.test.ts: the assertion pinning schema_version and a stale-claim fixture
line updated 1 -> 2. All telemetry tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:26:23 -05:00

196 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Anonymous usage telemetry
Status: implemented — ingest Worker (`telemetry-worker/`), client (`src/telemetry/`),
`codegraph telemetry` CLI, MCP + installer wiring, `TELEMETRY.md`. Pending: Worker deploy
+ DNS, release.
Scope: public `codegraph` engine (CLI + MCP server + installer)
CodeGraph is a local-first tool whose whole pitch is "your code never leaves your machine."
Telemetry has to be designed so that sentence stays true and provable: a short, auditable list
of anonymous counters, documented field-by-field, easy to turn off, and impossible to grow
quietly. This doc is the contract; `TELEMETRY.md` (repo root, user-facing) restates it and the
implementation must never collect anything not listed there.
## Goals
Answer, in aggregate and anonymously:
- How many machines actively use codegraph (daily/weekly), and how does that change?
- Which agents drive usage (Claude Code, Cursor, Codex, opencode, …) — via MCP `clientInfo`.
- Which install targets people pick, local vs global, fresh vs upgrade.
- Which MCP tools and CLI commands get used, how often, and how often they error.
- Which languages people index (prioritize extractor/framework work by real usage).
- Version adoption speed, OS/arch/Node mix. (The SQLite backend is always the built-in `node:sqlite` now — there is no native-vs-wasm split left to measure.)
## Non-goals / never collected
- **No source code, ever.** No file paths, file names, repo names, symbol names, query
strings, search terms, or anything derived from the contents of an indexed project.
- No IP addresses (stripped at the edge; storage disabled at the backend too).
- No hardware fingerprinting — the machine ID is a random UUID, not derived from anything.
- No per-keystroke / per-call event stream — usage is aggregated locally into daily rollups
before anything is sent.
- No telemetry from the `codegraph-pro` fork (see "codegraph-pro rule" below).
## Principles
1. **The schema is the allowlist.** Client sends only the events below; the ingest Worker
validates against the same allowlist and drops anything else. Adding a field = PR that
edits this doc + `TELEMETRY.md` + the Worker allowlist together.
2. **Telemetry may never cost the user anything**: zero added latency on the MCP tool-call
hot path (the repo's core invariant), zero new npm dependencies (global `fetch`, Node ≥18),
zero bytes on stdout (stdio is the MCP protocol channel), zero retries, zero error noise.
Every failure mode is silence.
3. **Off is off.** When disabled, no process opens a socket to the telemetry endpoint — not
even an "opted out" ping.
4. **First-party endpoint.** Clients only ever talk to `telemetry.getcodegraph.com`. The URL
baked into a published npm version POSTs there forever, so the domain must be ours; the
backend behind it can change without a client release.
## Events
Common envelope on every batch (computed once per process):
| field | example | notes |
|---|---|---|
| `machine_id` | `b3a8…` (UUIDv4) | random, minted at first run, stored in global config |
| `codegraph_version` | `0.9.12` | from package.json |
| `os` / `arch` | `darwin` / `arm64` | `process.platform` / `process.arch` |
| `node_major` | `22` | major only |
| `ci` | `false` | `CI` env var present |
| `schema_version` | `1` | bump when the schema changes |
Event types:
- **`install`** — one per installer run. Props: `targets` (e.g. `["claude","cursor"]`),
`scope` (`local`/`global`), `kind` (`fresh`/`upgrade`/`reinstall`).
- **`index`** — one per full index (`init`/`index`, not per `sync`). Props: `languages`
(names only, e.g. `["typescript","go"]`), `file_count_bucket` (`<100`, `100-1k`, `1k-10k`,
`10k+`), `duration_bucket` (`<10s`, `10-60s`, `1-5m`, `5m+`).
- **`usage_rollup`** — the workhorse. One event per `(day, kind, name)` per machine,
aggregated locally. Props: `kind` (`mcp_tool`/`cli_command`), `name`
(e.g. `codegraph_explore`, `affected`), `count`, `error_count`, and for MCP:
`client_name`/`client_version` from the `initialize` handshake (`src/mcp/session.ts`
`case 'initialize'` — plumbing to add; currently unread).
- **`uninstall`** — one per `uninstall`/`uninit` run (churn signal). Props: `targets`.
Volume math: rollups mean monthly events ≈ active machines × active days × distinct
tools used (single digits) — the PostHog free tier (1M events/mo) covers tens of
thousands of MAU. There is no per-call event by design.
Events are sent as PostHog **anonymous events** (`$process_person_profile: false`):
cheaper, no person profiles, unique-machine counts still work on `distinct_id` =
`machine_id`. Revisit only if retention tooling demands profiles.
## Consent & controls
Resolution order (first match wins):
1. `DO_NOT_TRACK=1` (community standard — always honored) → off
2. `CODEGRAPH_TELEMETRY=0|1` → forced off/on for that process
3. Global config `~/.codegraph/telemetry.json` → stored user choice
4. Default: **on**, gated by the first-run notice below
Surfaces:
- **Installer (interactive):** a visible clack toggle in the existing prompt flow —
"Share anonymous usage data? (no code, paths, or names — see TELEMETRY.md)" — default
yes. Choice persisted with `consent_source: "installer"`. Re-runs/upgrades respect the
stored choice and don't re-ask.
- **Headless paths** (`npx codegraph init`, MCP server — no TTY, never prompt): right
before the **first actual send** (recording only buffers locally and stays silent — so
the installer's explicit toggle always precedes any notice), print one line to
**stderr** and record `first_run_notice_shown`:
`codegraph collects anonymous usage stats (no code or paths) — "codegraph telemetry off" or CODEGRAPH_TELEMETRY=0 disables. Details: TELEMETRY.md`
- **CLI:** `codegraph telemetry status|on|off` (status prints the machine ID, current
state, and what decided it). Deleting `~/.codegraph/telemetry.json` resets everything,
including the machine ID.
`~/.codegraph/telemetry.json`:
```json
{
"enabled": true,
"machine_id": "uuid-v4",
"consent_source": "installer | default-notice | cli",
"first_run_notice_shown": true,
"updated_at": "2026-06-12T00:00:00Z"
}
```
(`~/.codegraph/` is new — today nothing global exists. Coexists by filename if a user ever
indexes `$HOME` itself, since per-project data lives in `<project>/.codegraph/` with fixed
other filenames.)
## Client architecture
New module `src/telemetry/` (single small module, no deps):
- **Counters in memory** — recording a tool call/CLI command is an in-memory increment.
Nothing on the hot path touches disk or network. MCP tool handlers call
`telemetry.count('mcp_tool', name, ok)` and move on.
- **Buffer** — counters persist (debounced, async) to `~/.codegraph/telemetry-queue.jsonl`.
Hard cap ~256 KB; on overflow drop oldest lines. Corrupt buffer → truncate, never throw.
- **Flush** — many CLI actions end via `process.exit()`, where `beforeExit` never fires
and async sends die, so the design is: a tiny **synchronous append** on `process.on('exit')`
persists in-memory deltas (survives `process.exit`), and actual network sends happen
opportunistically — at the start of long-running commands (`init`/`index`/`sync`/
`uninit`/`upgrade`), on an unref'd interval in the long-lived MCP server/daemon, and
awaited-with-cap at the end of `install`/`init`/`index`/`uninit` where a second is
invisible. Sends POST completed-day rollups + lifecycle events to
`https://telemetry.getcodegraph.com/v1/events` with `AbortSignal.timeout(1500)`,
fire-and-forget: any response (or none) is final — no retry, no error surfaced. The
queue is claimed by atomic rename so concurrent processes can't double-send (a crashed
sender's claim merges back after an hour). `CODEGRAPH_TELEMETRY_DEBUG=1` echoes
payloads to stderr for development.
- **Offline / air-gapped:** flush fails silently, buffer stays within cap, steady state is
a bounded file and zero noise.
## Ingest endpoint (Cloudflare Worker)
`telemetry.getcodegraph.com` → small Worker living at `telemetry-worker/` in this repo —
public on purpose, so anyone can audit exactly what the endpoint stores. It ships nowhere
with the npm package (excluded by the `files` allowlist):
- `POST /v1/events`: validate against the event/property allowlist (drop unknown events,
strip unknown props), enforce sane sizes, **never forward or log the client IP**
(drop `CF-Connecting-IP`), light per-`machine_id` rate limit so abuse can't burn the
ingest cap, forward to `https://us.i.posthog.com/batch/` with the project key from a
Worker secret. Responds `204` on accept (including events dropped by the allowlist)
and honest `4xx` for malformed/oversized/rate-limited requests — the client treats
every response as final and never retries.
- Backend today: PostHog Cloud US, free plan, "discard client IP" enabled, GeoIP disabled,
autocapture/replay/heatmaps/web-vitals all off. The Worker is the seam: swapping the
backend later is a Worker change, not a client release.
## codegraph-pro rule (do not lose this in upstream merges)
The private `codegraph-pro` fork ships inside customer containers whose guarantee is
"nothing leaves the box" — including telemetry. In the fork, telemetry must be **default-off
and not enableable by the installer** (compile-time constant or stripped module), and the
container sets `CODEGRAPH_TELEMETRY=0` as belt-and-braces. This rule lives in the fork's
CLAUDE.md and must survive every upstream merge.
## Rollout
1. This doc + repo-root `TELEMETRY.md` (user-facing field-by-field list) + README section.
2. Worker + DNS live first (so the first shipping client never 404s), PostHog dashboards:
weekly active machines, installs by target, usage by tool × client, version adoption,
languages indexed.
3. Client module + config + `codegraph telemetry` subcommand + MCP `clientInfo` plumbing.
4. Installer toggle + first-run notice. CHANGELOG entry under `[Unreleased]` announcing
telemetry, the default, and every off-switch. Release.
Tests (no DB mocking, per repo convention; fetch mocked at `globalThis.fetch`):
consent precedence (env > config > default), off ⇒ zero fetch calls, rollup aggregation
across days, buffer cap + corrupt-buffer recovery, no-stdout invariant under MCP transport,
flush abort honors timeout, installer toggle persists + re-run doesn't re-ask
(`__tests__/installer-targets.test.ts` per house rules).
## Open questions
- Exact installer copy / notice wording — maintainer call before release.
- `uninstall` event: keep or drop? (Honest churn signal vs. "pinging on the way out" optics.)
- CI events are kept (tagged `ci: true`) because engine-in-CI is a real usage mode — revisit
if it ever dominates volume.