forked from colbymchenry/codegraph
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path0001_init.sql
More file actions
205 lines (193 loc) · 12.2 KB
/
Copy path0001_init.sql
File metadata and controls
205 lines (193 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
-- codegraph telemetry — initial schema (Cloudflare D1)
--
-- This file is public on purpose, like the rest of telemetry-worker/: it is the
-- complete list of everything codegraph's anonymous telemetry stores. If a column
-- is not here, it is not kept. The field-by-field contract it implements lives in
-- docs/design/telemetry.md (and, user-facing, in TELEMETRY.md).
--
-- Nothing in this database identifies a person or a codebase. No IP addresses (the
-- ingest worker never reads them), no file paths, no repo, file, or symbol names, no
-- query strings. `machine_id` is a random UUIDv4 the client mints locally and the user
-- can delete at any time (`codegraph telemetry off`, or remove ~/.codegraph/telemetry.json).
--
-- Shape: raw events + daily rollups.
-- * The ingest worker (`src/index.ts`) writes ONLY to `events`, `machine_days` and
-- `machine_first_seen`, off the response path.
-- * The nightly cron recomputes the `daily_*` rollups from `events` with idempotent
-- upserts, then purges raw `events` past the retention window.
-- * The admin dashboard reads rollups first and falls back to `events` only for the
-- activation funnel and ad-hoc drill-down (both bounded by the retention window).
--
-- Apply: npm run db:migrate:local (local .wrangler state)
-- npm run db:migrate (remote codegraph-telemetry)
-- ---------------------------------------------------------------------------
-- Raw events
-- ---------------------------------------------------------------------------
-- One row per sanitized event accepted by POST /v1/events. Everything here has
-- already passed the worker's allowlist: unknown events dropped, unknown props
-- stripped, strings length- and charset-checked, timestamps clamped.
--
-- Deliberately NO `CHECK (event IN (...))` constraint: the worker's EVENTS allowlist
-- is the single source of truth, and the write path is fail-silent by design (a
-- rejected INSERT would lose data quietly rather than error visibly). Same reasoning
-- for `json_valid(props)` — the worker constructs that JSON itself.
CREATE TABLE events (
-- rowid alias, no AUTOINCREMENT: ids are never referenced anywhere, and the
-- retention purge only ever deletes the OLDEST rows, so max(id) never drops and
-- ids stay monotonic in practice. Gives the purge a cheap keyset batch:
-- DELETE FROM events WHERE id IN (SELECT id FROM events WHERE day < ? LIMIT 5000)
id INTEGER PRIMARY KEY,
received_at TEXT NOT NULL, -- ISO 8601 UTC, worker clock, always present
ts TEXT, -- ISO 8601 UTC client timestamp, already clamped
-- by the worker (>10min future / >30d past rejected);
-- NULL when the client sent none. For usage_rollup
-- the client sets it to <rollup day>T12:00:00Z, so it
-- attributes counters to the day they happened on.
day TEXT NOT NULL, -- UTC YYYY-MM-DD from substr(ts, 1, 10), else received_at.
-- Every rollup and every chart is keyed on this.
event TEXT NOT NULL, -- install | index | usage_rollup | uninstall
machine_id TEXT NOT NULL, -- random UUIDv4, client-minted (never fingerprinted)
-- Envelope, identical for every event in a batch. All nullable: the worker's
-- sanitizer strips anything malformed rather than rejecting the batch, so an old
-- or odd client shows up as NULLs instead of vanishing.
codegraph_version TEXT,
os TEXT, -- process.platform: darwin | linux | win32 | …
arch TEXT, -- process.arch: arm64 | x64 | …
node_major INTEGER,
ci INTEGER, -- 0/1 from the client's `ci` boolean; NULL if absent.
-- "Production users" = everything except ci = 1
-- (NULL counts as production — see machine_days.prod).
schema_version INTEGER,
props TEXT NOT NULL DEFAULT '{}' -- JSON object of the sanitized event-specific props
);
-- (day, event) subsumes a plain (day) index — SQLite uses the leading-column prefix —
-- so this pair covers day-range scans, per-event day-range scans AND the retention
-- purge with one fewer index than listing them separately. That matters: D1 bills an
-- extra row write per index touched, so every index on this table costs ~97k
-- writes/day. Do not add a third without re-checking the volume note below.
CREATE INDEX events_day_event ON events (day, event);
-- Ad-hoc per-machine drill-down and the activation funnel (install → first index).
CREATE INDEX events_machine_day ON events (machine_id, day);
-- ---------------------------------------------------------------------------
-- Rollups — written by the nightly cron, read by the dashboard
-- ---------------------------------------------------------------------------
-- Rollups are kept FOREVER (they are tiny); raw `events` are purged. So any number a
-- chart needs long-term has to be recoverable from these tables alone — that is why
-- the distinct-machine columns exist alongside the event counts.
-- Daily unique machines.
-- Serves: "Daily Production Users" line; the machine denominator on daily panels.
-- `prod_machines` excludes ci = 1 (CI runners), matching the dashboard's
-- "Production Users" framing. NOTE: these are per-day distinct counts and CANNOT be
-- summed across a range — a range-wide distinct count comes from `machine_days`.
CREATE TABLE daily_machines (
day TEXT PRIMARY KEY,
machines INTEGER NOT NULL DEFAULT 0,
prod_machines INTEGER NOT NULL DEFAULT 0
);
-- Daily event volume per event type.
-- Serves: "Install" / "Uninstall" big numbers; "Installs vs uninstalls over time";
-- "New installs (daily)"; the runs series of "Daily indexing activity".
-- `count` is a row count for install/index/uninstall, but for usage_rollup it is the
-- SUM of the events' `count` prop (the client pre-aggregates locally, so one row can
-- represent hundreds of tool calls). `machines` is the distinct machines that emitted
-- that event that day — the "active users" series of "Daily indexing activity", which
-- is unrecoverable once the raw rows are purged.
CREATE TABLE daily_event_counts (
day TEXT NOT NULL,
event TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
machines INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (day, event)
) WITHOUT ROWID;
-- One generic (dimension, value) table behind every bar and pie on the dashboard,
-- so a new breakdown is a cron change, never a migration.
-- Serves, by `dim`:
-- os → "Users by operating system" (pie)
-- arch → arch mix
-- codegraph_version → "Users by app version" (bar)
-- node_major → Node version mix
-- language → "Most-indexed programming languages" (bar; unnested from index.languages)
-- file_count_bucket → "Codebase size (files per project)" (bar)
-- duration_bucket → "Session run length" (pie) and "Indexing speed" (bar),
-- plus "indexing duration buckets over time" (stacked line)
-- target → "AI Agent Targets" (bar; unnested from install.targets / uninstall.targets)
-- scope → install local vs global
-- kind → install fresh / upgrade / reinstall
-- name → usage by MCP tool / CLI command (incl. prompt-hook-gate-* outcomes)
-- client_name → usage by agent (Claude Code, Cursor, …), from MCP clientInfo
-- `event` is kept in the key so the same dim can be sliced per event type (e.g. os for
-- install vs os for index). `count` is event volume (SUM of the usage_rollup `count`
-- prop where applicable); `machines` is distinct machines — the honest number for the
-- "users by …" panels, which are machine counts, not event counts.
CREATE TABLE daily_dim_counts (
day TEXT NOT NULL,
event TEXT NOT NULL,
dim TEXT NOT NULL,
value TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
machines INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (day, event, dim, value)
) WITHOUT ROWID;
-- Cross-event slices of one dimension over a date range ("languages, all events, last 30d").
CREATE INDEX daily_dim_counts_dim_day ON daily_dim_counts (dim, day);
-- First day a machine was ever seen.
-- Serves: "New installs over time"; the denominator of the install → first-use
-- activation funnel; the cohort key for retention.
-- Written by the ingest worker on every batch (upsert keeps the MINIMUM day, so a
-- late-arriving offline buffer can move a machine's first day earlier but never later).
CREATE TABLE machine_first_seen (
machine_id TEXT PRIMARY KEY,
first_day TEXT NOT NULL
);
-- Cohort scans: "machines first seen between X and Y".
CREATE INDEX machine_first_seen_day ON machine_first_seen (first_day);
-- Machine × day activity matrix — the only table that can answer "distinct machines
-- over a RANGE" (daily rollups can't: summing them double-counts returning machines).
-- Serves: "Daily retention cohorts" (day 0–14 curve, joined to machine_first_seen);
-- the "Production Users" big number over the picker's range;
-- active-machine lines beyond the raw-event retention window.
-- `prod` is 0 only if EVERY event that machine sent that day carried ci = 1; a missing
-- `ci` counts as production. Kept per (machine, day) rather than as a per-machine flag
-- because the same install can run inside and outside CI on different days.
-- ~10k rows/day at current volume — WITHOUT ROWID keeps it compact (the PK is the table).
-- NOT purged by the retention job: retention cohorts need the full history.
CREATE TABLE machine_days (
machine_id TEXT NOT NULL,
day TEXT NOT NULL,
prod INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (machine_id, day)
) WITHOUT ROWID;
-- Day-keyed scans ("distinct machines active in this range").
CREATE INDEX machine_days_day ON machine_days (day);
-- ---------------------------------------------------------------------------
-- Volume & storage sanity check (Workers Paid, limits as of 2026-07)
-- ---------------------------------------------------------------------------
-- Included per month: 50M rows written, 25B rows read, 5 GB storage
-- (then $0.75/GB-mo). Hard cap: 10 GB per database.
--
-- Current ingest is ~97k accepted POSTs/day. D1 counts one row write PER INDEX
-- touched in addition to the table row, so with ~2 events per request:
--
-- events 97k × 2 × (1 table + 2 indexes) ≈ 0.6M writes/day
-- machine_days 97k × (1 table + 1 index) ≈ 0.2M writes/day
-- first_seen 97k × (1 table + 1 index) ≈ 0.2M writes/day
-- rollup cron ~1k rows/day negligible
-- ─────────────────
-- ≈ 1.0M writes/day ≈ 30M/month
--
-- Comfortably inside the 50M included, with ~1.6× headroom. (The epic's "~10M/month"
-- estimate predates counting index writes; the arithmetic above is the one to trust.)
-- Reads are trivial: the dashboard hits rollups, ~thousands of rows per page load.
--
-- STORAGE is the tighter constraint, and it decides the retention window. A raw event
-- row is ~250 B plus ~130 B of index entries, so ~74 MB/day:
--
-- 90-day retention ≈ 6.7 GB under the 10 GB cap, ~$1.30/mo over the 5 GB included
-- 180-day retention ≈ 13 GB EXCEEDS the 10 GB per-database cap
--
-- So the retention job should start at 90 days, not 180 — and the real row size must be
-- measured after cutover (`SELECT count(*), sum(length(props)) FROM events`) before
-- widening it. Rollups are kept forever regardless, so shortening the raw window costs
-- ad-hoc drill-back, never a chart. If writes or storage ever get tight, the levers, in
-- order: drop events_machine_day (drill-down only), move the machine_first_seen upsert
-- off the hot path into the nightly cron, then store timestamps as INTEGER epoch ms.