| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- #!/usr/bin/env bash
- # End-to-end check of the nightly rollup + retention purge against a real
- # `wrangler dev` and the local D1 state.
- #
- # Seeds three synthetic days of events straight into local D1 (the ingest path clamps
- # client timestamps to the last 30 days, so backdating far enough to exercise the purge
- # has to bypass it), drives the rollup through the admin endpoint and the cron handler,
- # then inspects what actually landed against hand-computed numbers.
- #
- # What it pins:
- # * rollup numbers match the events they came from, including the two that are easy
- # to get wrong — usage_rollup SUMs its `count` prop, and array props unnest
- # * running a day twice changes nothing (idempotent upserts, no double counting)
- # * ?reset=1 drops stale rollup rows on a live day and REFUSES to blank a day whose
- # raw events are already purged
- # * the purge deletes only rows past the window, and leaves machine_days /
- # machine_first_seen alone
- # * /admin/rollup does not exist without ADMIN_TOKEN, and rejects a wrong one
- #
- # Re-runnable: it wipes its own synthetic days first, and they are chosen to sit
- # outside the cron's 3-day lookback so the nightly run never rewrites them.
- #
- # npm run smoke:rollup # or: ROLLUP_PORT=8792 ./scripts/smoke-rollup.sh
- set -euo pipefail
- cd "$(dirname "$0")/.."
- PORT="${ROLLUP_PORT:-8788}"
- BASE="http://127.0.0.1:$PORT"
- DB=codegraph-telemetry
- TOKEN=smoke-admin-token
- SEED_SQL=/tmp/cg-smoke-rollup-seed.sql
- LOG=/tmp/cg-smoke-rollup.log
- pass=0; fail=0
- ok() { pass=$((pass + 1)); printf ' ok %s\n' "$1"; }
- bad() { fail=$((fail + 1)); printf ' FAIL %s — expected %s, got %s\n' "$1" "$2" "$3"; }
- is() { [ "$2" = "$3" ] && ok "$1" || bad "$1" "$2" "$3"; }
- day_ago() { node -e 'console.log(new Date(Date.now()-process.argv[1]*864e5).toISOString().slice(0,10))' "$1"; }
- # Synthetic days. MAIN/RESET sit inside the 90-day retention window but outside the
- # cron's 3-day lookback; OLD sits past the window so the purge takes it.
- DAY_MAIN=$(day_ago 40)
- DAY_RESET=$(day_ago 41)
- DAY_OLD=$(day_ago 200)
- CUTOFF=$(day_ago 90)
- # First column of the first row of a query against the LOCAL D1 state.
- q() {
- npx wrangler d1 execute "$DB" --local --json --command "$1" 2>/dev/null |
- node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
- const r=JSON.parse(s.slice(s.indexOf("[")))[0]?.results?.[0];
- console.log(r===undefined?"":String(Object.values(r)[0]));})'
- }
- # A daily_dim_counts cell as "count/machines" — "" when the row does not exist.
- dim() { q "select count||'/'||machines from daily_dim_counts
- where day='$1' and event='$2' and dim='$3' and value='$4'"; }
- # POST /admin/rollup, printing the HTTP status.
- roll() { curl -s -o /dev/null -w '%{http_code}' -X POST -H "x-admin-token: $TOKEN" "$BASE/admin/rollup?$1"; }
- boot() { # extra wrangler dev args
- npx wrangler dev --port "$PORT" "$@" >"$LOG" 2>&1 &
- DEV_PID=$!
- trap 'kill "$DEV_PID" 2>/dev/null || true; wait "$DEV_PID" 2>/dev/null || true' EXIT
- local up=
- for _ in $(seq 1 60); do
- curl -sf -o /dev/null "$BASE/" && { up=1; break; }
- kill -0 "$DEV_PID" 2>/dev/null || { echo "wrangler dev died:"; cat "$LOG"; exit 1; }
- sleep 1
- done
- [ -n "$up" ] || { echo "worker never came up:"; cat "$LOG"; exit 1; }
- # If wrangler could not bind the port, something else answers every probe and the
- # whole run silently grades a different server. Check who picked up.
- case "$(curl -s "$BASE/")" in
- *'codegraph anonymous-telemetry ingest'*) : ;;
- *) echo "port $PORT is serving something else — set ROLLUP_PORT to a free one"; exit 1 ;;
- esac
- }
- shutdown() {
- kill "$DEV_PID" 2>/dev/null || true
- wait "$DEV_PID" 2>/dev/null || true
- trap - EXIT
- sleep 1 # let miniflare release the local sqlite file
- }
- # ---------------------------------------------------------------------------
- # Seed
- # ---------------------------------------------------------------------------
- echo "applying migrations to local D1"
- npx wrangler d1 migrations apply "$DB" --local >/dev/null 2>&1
- echo "seeding $DAY_MAIN / $DAY_RESET / $DAY_OLD"
- node -e '
- const [main, reset, old, seedFile] = process.argv.slice(1);
- const sq = (v) => `'"'"'${String(v).replace(/'"'"'/g, "'"'"''"'"'")}'"'"'`;
- const M = ["11111111-1111-4111-8111-111111111111", "22222222-2222-4222-8222-222222222222",
- "33333333-3333-4333-8333-333333333333", "44444444-4444-4444-8444-444444444444",
- "99999999-9999-4999-8999-999999999999"];
- const out = [];
- // Re-runnable: every table this script touches, scoped to its own synthetic days.
- for (const t of ["events", "daily_event_counts", "daily_dim_counts", "daily_machines", "machine_days"]) {
- out.push(`DELETE FROM ${t} WHERE day IN (${[main, reset, old].map(sq).join(", ")});`);
- }
- out.push(`DELETE FROM machine_first_seen WHERE machine_id IN (${M.map(sq).join(", ")});`);
- // day, machine, event, os, arch, version, node_major, ci, props
- const rows = [
- [main, M[0], "install", "darwin", "arm64", "1.5.0", 22, 0, {targets:["claude","cursor"], scope:"local", kind:"fresh"}],
- [main, M[0], "index", "darwin", "arm64", "1.5.0", 22, 0, {languages:["typescript","go"], file_count_bucket:"100-1k", duration_bucket:"10-60s"}],
- [main, M[0], "usage_rollup", "darwin", "arm64", "1.5.0", 22, 0, {kind:"mcp_tool", name:"codegraph_explore", count:10, error_count:2, client_name:"Claude Code"}],
- [main, M[1], "index", "darwin", "x64", "1.5.0", 20, 0, {languages:["typescript"], file_count_bucket:"1k-10k", duration_bucket:"10-60s"}],
- [main, M[1], "usage_rollup", "darwin", "x64", "1.5.0", 20, 0, {kind:"mcp_tool", name:"codegraph_explore", count:5, error_count:0, client_name:"Cursor"}],
- [main, M[2], "install", "linux", "x64", "1.4.1", 22, 1, {targets:["claude"], scope:"global", kind:"upgrade"}],
- [main, M[2], "uninstall", "linux", "x64", "1.4.1", 22, 1, {targets:["claude"]}],
- [reset, M[3], "index", "darwin", "arm64", "1.5.0", 22, 0, {languages:["python"], file_count_bucket:"<100", duration_bucket:"<10s"}],
- [old, M[4], "install", "linux", "x64", "1.0.0", 20, 0, {targets:["codex"], scope:"local", kind:"fresh"}],
- [old, M[4], "index", "linux", "x64", "1.0.0", 20, 0, {languages:["rust"], file_count_bucket:"<100", duration_bucket:"<10s"}],
- ];
- for (const [day, m, event, os, arch, version, node, ci, props] of rows) {
- out.push(`INSERT INTO events (received_at, ts, day, event, machine_id, codegraph_version, os, arch, node_major, ci, schema_version, props)
- VALUES (${sq(day + "T12:00:00.000Z")}, ${sq(day + "T12:00:00.000Z")}, ${sq(day)}, ${sq(event)}, ${sq(m)},
- ${sq(version)}, ${sq(os)}, ${sq(arch)}, ${node}, ${ci}, 1, ${sq(JSON.stringify(props))});`);
- }
- // What the ingest path would have written alongside those events.
- for (const [m, day, prod] of [[M[0], main, 1], [M[1], main, 1], [M[2], main, 0], [M[3], reset, 1], [M[4], old, 1]]) {
- out.push(`INSERT INTO machine_days (machine_id, day, prod) VALUES (${sq(m)}, ${sq(day)}, ${prod});`);
- out.push(`INSERT INTO machine_first_seen (machine_id, first_day) VALUES (${sq(m)}, ${sq(day)})
- ON CONFLICT (machine_id) DO UPDATE SET first_day = min(machine_first_seen.first_day, excluded.first_day);`);
- }
- // A rollup row from a dimension that no longer exists — only ?reset=1 should clear it.
- out.push(`INSERT INTO daily_dim_counts (day, event, dim, value, count, machines)
- VALUES (${sq(reset)}, ${sq("index")}, ${sq("obsolete_dim")}, ${sq("stale")}, 99, 99);`);
- require("fs").writeFileSync(seedFile, out.join("\n"));
- ' "$DAY_MAIN" "$DAY_RESET" "$DAY_OLD" "$SEED_SQL"
- npx wrangler d1 execute "$DB" --local --file "$SEED_SQL" >/dev/null
- # ---------------------------------------------------------------------------
- # The admin route does not exist without a token
- # ---------------------------------------------------------------------------
- echo
- echo "admin route, no ADMIN_TOKEN configured"
- boot
- is "POST /admin/rollup → 404" 404 "$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/admin/rollup")"
- is "…even with a token header" 404 \
- "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "x-admin-token: $TOKEN" "$BASE/admin/rollup")"
- shutdown
- # ---------------------------------------------------------------------------
- # Drive the rollup
- # ---------------------------------------------------------------------------
- echo
- echo "admin route, ADMIN_TOKEN configured"
- boot --test-scheduled --var "ADMIN_TOKEN:$TOKEN"
- is "no token → 401" 401 "$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/admin/rollup")"
- is "wrong token → 401" 401 \
- "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H 'x-admin-token: nope' "$BASE/admin/rollup")"
- is "GET → 405" 405 "$(curl -s -o /dev/null -w '%{http_code}' -H "x-admin-token: $TOKEN" "$BASE/admin/rollup")"
- is "impossible day → 400" 400 "$(roll 'day=2026-02-31')"
- is "malformed day → 400" 400 "$(roll 'day=yesterday')"
- is "days out of range → 400" 400 "$(roll "day=$DAY_MAIN&days=99")"
- echo
- echo "rollup"
- is "rollup $DAY_MAIN → 200" 200 "$(roll "day=$DAY_MAIN")"
- is "rollup $DAY_MAIN again → 200" 200 "$(roll "day=$DAY_MAIN")"
- is "rollup $DAY_OLD, whose events are still there → 200" 200 "$(roll "day=$DAY_OLD")"
- is "rollup $DAY_RESET with reset → 200" 200 "$(roll "day=$DAY_RESET&reset=1")"
- # The cron body: rolls up the last three days and purges everything past the window.
- is "cron trigger → 200" 200 "$(curl -s -o /dev/null -w '%{http_code}' "$BASE/__scheduled?cron=30+0+*+*+*")"
- sleep 2
- # Rolling a purged day with reset=1 must NOT blank the rollups it already has: past
- # the window the reset is ignored, so the delete-then-rebuild can't find zero events.
- is "rollup $DAY_OLD after the purge, with reset → 200" 200 "$(roll "day=$DAY_OLD&reset=1")"
- is "…and reports the reset it refused to run" "[\"$DAY_OLD\"]" \
- "$(curl -s -X POST -H "x-admin-token: $TOKEN" "$BASE/admin/rollup?day=$DAY_OLD&reset=1" |
- node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.stringify(JSON.parse(s).reset_ignored)))')"
- shutdown
- # ---------------------------------------------------------------------------
- # What actually landed — every number below is hand-computed from the seed above
- # ---------------------------------------------------------------------------
- echo
- echo "daily_machines"
- is "3 machines, 2 of them production (one is ci)" "3/2" \
- "$(q "select machines||'/'||prod_machines from daily_machines where day='$DAY_MAIN'")"
- echo
- echo "daily_event_counts"
- is "install: 2 events from 2 machines" "2/2" \
- "$(q "select count||'/'||machines from daily_event_counts where day='$DAY_MAIN' and event='install'")"
- is "index: 2 events from 2 machines" "2/2" \
- "$(q "select count||'/'||machines from daily_event_counts where day='$DAY_MAIN' and event='index'")"
- is "uninstall: 1 event from 1 machine" "1/1" \
- "$(q "select count||'/'||machines from daily_event_counts where day='$DAY_MAIN' and event='uninstall'")"
- # The one that is easy to get wrong: 2 rows carrying count 10 and 5 is 15 tool calls.
- is "usage_rollup: SUMs the count prop (10+5), not the rows" "15/2" \
- "$(q "select count||'/'||machines from daily_event_counts where day='$DAY_MAIN' and event='usage_rollup'")"
- is "one row per event type" 4 "$(q "select count(*) from daily_event_counts where day='$DAY_MAIN'")"
- echo
- echo "daily_dim_counts"
- is "os / index / darwin" "2/2" "$(dim "$DAY_MAIN" index os darwin)"
- is "os / usage_rollup / darwin sums counts" "15/2" "$(dim "$DAY_MAIN" usage_rollup os darwin)"
- is "arch / uninstall / x64" "1/1" "$(dim "$DAY_MAIN" uninstall arch x64)"
- is "codegraph_version / index / 1.5.0" "2/2" "$(dim "$DAY_MAIN" index codegraph_version 1.5.0)"
- is "node_major / install / 22 (stored as text)" "2/2" "$(dim "$DAY_MAIN" install node_major 22)"
- is "file_count_bucket / index / 100-1k" "1/1" "$(dim "$DAY_MAIN" index file_count_bucket 100-1k)"
- is "duration_bucket / index / 10-60s" "2/2" "$(dim "$DAY_MAIN" index duration_bucket 10-60s)"
- is "scope / install / global" "1/1" "$(dim "$DAY_MAIN" install scope global)"
- is "kind / install / fresh" "1/1" "$(dim "$DAY_MAIN" install kind fresh)"
- is "kind / usage_rollup / mcp_tool (same dim, other event)" "15/2" "$(dim "$DAY_MAIN" usage_rollup kind mcp_tool)"
- is "name / usage_rollup / codegraph_explore" "15/2" "$(dim "$DAY_MAIN" usage_rollup name codegraph_explore)"
- is "client_name / usage_rollup / Claude Code" "10/1" "$(dim "$DAY_MAIN" usage_rollup client_name 'Claude Code')"
- # languages and targets are JSON arrays: one row per element, counted once per event.
- is "language / index / typescript (unnested, 2 events)" "2/2" "$(dim "$DAY_MAIN" index language typescript)"
- is "language / index / go (unnested, 1 event)" "1/1" "$(dim "$DAY_MAIN" index language go)"
- is "target / install / claude (unnested, 2 events)" "2/2" "$(dim "$DAY_MAIN" install target claude)"
- is "target / install / cursor" "1/1" "$(dim "$DAY_MAIN" install target cursor)"
- is "target / uninstall / claude" "1/1" "$(dim "$DAY_MAIN" uninstall target claude)"
- # Only groups with at least one error are stored, so machines = machines that saw one.
- is "name_error / usage_rollup / codegraph_explore" "2/1" "$(dim "$DAY_MAIN" usage_rollup name_error codegraph_explore)"
- is "no dimension row for a machine with no errors" "" "$(dim "$DAY_MAIN" usage_rollup name_error nothing)"
- is "40 dimension rows in total (no strays, no doubles)" 40 \
- "$(q "select count(*) from daily_dim_counts where day='$DAY_MAIN'")"
- # Independent of the hand-computed numbers: recompute two of them straight off `events`.
- echo
- echo "cross-check against the raw events"
- is "machines matches count(distinct machine_id)" \
- "$(q "select count(distinct machine_id) from events where day='$DAY_MAIN' and event='index'")" \
- "$(q "select machines from daily_event_counts where day='$DAY_MAIN' and event='index'")"
- is "usage count matches sum(props.count)" \
- "$(q "select sum(json_extract(props,'\$.count')) from events where day='$DAY_MAIN' and event='usage_rollup'")" \
- "$(q "select count from daily_event_counts where day='$DAY_MAIN' and event='usage_rollup'")"
- echo
- echo "reset"
- is "?reset=1 drops a rollup row whose dimension no longer exists" 0 \
- "$(q "select count(*) from daily_dim_counts where day='$DAY_RESET' and dim='obsolete_dim'")"
- is "…and recomputes the day correctly" "1/1" "$(dim "$DAY_RESET" index language python)"
- is "…leaving exactly the 7 dimensions that day has" 7 \
- "$(q "select count(*) from daily_dim_counts where day='$DAY_RESET'")"
- echo
- echo "retention purge"
- is "raw events past the window are gone" 0 "$(q "select count(*) from events where day='$DAY_OLD'")"
- is "nothing older than the cutoff survives" 0 "$(q "select count(*) from events where day<'$CUTOFF'")"
- is "events inside the window are untouched" 7 "$(q "select count(*) from events where day='$DAY_MAIN'")"
- is "machine_days is NOT purged (retention cohorts need it)" 1 \
- "$(q "select count(*) from machine_days where day='$DAY_OLD'")"
- is "machine_first_seen is NOT purged" "$DAY_OLD" \
- "$(q "select first_day from machine_first_seen where machine_id='99999999-9999-4999-8999-999999999999'")"
- echo
- echo "rollups outlive the events they came from"
- is "daily_event_counts survives the purge" "1/1" \
- "$(q "select count||'/'||machines from daily_event_counts where day='$DAY_OLD' and event='index'")"
- is "daily_dim_counts survives the purge" "1/1" "$(dim "$DAY_OLD" index language rust)"
- is "…all 14 rows of it, even after a reset run over the purged day" 14 \
- "$(q "select count(*) from daily_dim_counts where day='$DAY_OLD'")"
- is "daily_machines is still rebuilt for a purged day (machine_days survives)" "1/1" \
- "$(q "select machines||'/'||prod_machines from daily_machines where day='$DAY_OLD'")"
- echo
- echo "$pass passed, $fail failed"
- [ "$fail" -eq 0 ]
|