chore: import upstream snapshot with attribution
Deploy site to GitHub Pages / build (push) Failing after 1s
Deploy site to GitHub Pages / deploy (push) Has been skipped

This commit is contained in:
wehub-resource-sync
2026-07-13 12:02:56 +08:00
commit 5f98960d22
495 changed files with 149129 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# Copy to .dev.vars for local development (`npm run dev`) and so that
# `wrangler types` includes POSTHOG_KEY in the generated Env.
# The real key lives only in the deployed secret (`wrangler secret put POSTHOG_KEY`).
POSTHOG_KEY="phc_dev_placeholder"
+5
View File
@@ -0,0 +1,5 @@
node_modules/
.wrangler/
.dev.vars
# generated by `wrangler types` (npm run types) — includes .dev.vars keys
worker-configuration.d.ts
+60
View File
@@ -0,0 +1,60 @@
# codegraph telemetry ingest worker
The first-party endpoint behind `telemetry.getcodegraph.com`. This directory is in the
public repo **on purpose**: it is the exact code that receives codegraph's anonymous usage
telemetry, so anyone can audit what is stored. The schema contract (every event, every
field, and everything that is never collected) is in
[`docs/design/telemetry.md`](../docs/design/telemetry.md).
What it does, in one breath: validates incoming batches against a strict allowlist (unknown
events dropped, unknown properties stripped), never reads or forwards the client IP,
rate-limits per machine ID, and forwards to PostHog off the response path. It ships nowhere
with the npm package — the engine's `files` allowlist excludes it.
## Endpoint contract
- `POST /v1/events` — JSON body: envelope (`machine_id` UUID, `codegraph_version`, `os`,
`arch`, `node_major`, `ci`, `schema_version`) + `events: [{event, ts?, props?}]`.
Responds `204` when accepted (including events dropped by the allowlist), honest `4xx`
for malformed/oversized/rate-limited requests. Clients treat every response as final —
no retries.
- `GET /` — plain-text pointer to the docs and the off-switches.
## Deploy
Prereqs: the `getcodegraph.com` zone on the deploying Cloudflare account (the custom
domain route auto-provisions DNS + cert), wrangler ≥ 4.36 (the `ratelimits` binding).
```bash
cd telemetry-worker
npm install
npx wrangler login # once
npx wrangler secret put POSTHOG_KEY # the phc_… project write key — never committed
npm run deploy
```
The PostHog project itself must have **"Discard client IP data"** enabled — defense in
depth on top of this worker never forwarding IPs (`$geoip_disable` is also set per event).
## Local dev & checks
```bash
cp .dev.vars.example .dev.vars # placeholder key; also feeds `wrangler types`
npm run check # wrangler types + tsc --noEmit + deploy --dry-run
npm run dev # http://localhost:8787
curl -i localhost:8787/v1/events -H 'content-type: application/json' -d '{
"machine_id": "00000000-0000-4000-8000-000000000000",
"codegraph_version": "0.9.9", "os": "darwin", "arch": "arm64",
"node_major": 22, "ci": false, "schema_version": 1,
"events": [{ "event": "usage_rollup",
"props": { "kind": "mcp_tool", "name": "codegraph_explore",
"count": 12, "error_count": 0, "client_name": "Claude Code" } }]
}'
```
## Changing the schema
The allowlist in `src/index.ts` mirrors `docs/design/telemetry.md` (and the user-facing
`TELEMETRY.md`). A field is added by one PR touching all of them together — that is the
whole point of the design.
+1520
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"name": "codegraph-telemetry-worker",
"private": true,
"description": "First-party ingest endpoint for codegraph's anonymous usage telemetry (telemetry.getcodegraph.com)",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"types": "wrangler types",
"check": "wrangler types && tsc --noEmit && wrangler deploy --dry-run"
},
"devDependencies": {
"typescript": "^5.0.0",
"wrangler": "^4.36.0"
}
}
+260
View File
@@ -0,0 +1,260 @@
/**
* codegraph telemetry ingest — telemetry.getcodegraph.com
*
* This file is public on purpose: it is the exact code that receives codegraph's
* anonymous usage telemetry, so anyone can audit what is (and is not) stored.
* The schema contract lives in docs/design/telemetry.md.
*
* Guarantees enforced here:
* - strict allowlist: unknown events are dropped, unknown properties are stripped
* - the client IP is never read, logged, or forwarded
* - per-machine rate limiting, bounded body/batch sizes
* - forwarding happens off the response path (ctx.waitUntil); bodies are never logged
*/
const MAX_BODY_BYTES = 64 * 1024;
const MAX_EVENTS_PER_BATCH = 100;
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
// Bare identifiers: tool/command/target/language names, versions.
const TOKEN_RE = /^[A-Za-z0-9_.:+-]+$/;
// Human-ish labels: MCP clientInfo names like "Claude Code", "cursor-vscode/1.2".
const LABEL_RE = /^[A-Za-z0-9_.:+/ @()-]+$/;
const INFO_TEXT = `codegraph anonymous-telemetry ingest.
What gets collected (and what never does) is documented field-by-field:
https://github.com/colbymchenry/codegraph/blob/main/docs/design/telemetry.md
This endpoint's full source:
https://github.com/colbymchenry/codegraph/tree/main/telemetry-worker
Disable any time: codegraph telemetry off | CODEGRAPH_TELEMETRY=0 | DO_NOT_TRACK=1
`;
type JsonObject = Record<string, unknown>;
/** Returns the sanitized value, or undefined to strip the property. */
type Sanitize = (v: unknown) => unknown;
const oneOf =
(allowed: readonly string[]): Sanitize =>
(v) =>
typeof v === 'string' && allowed.includes(v) ? v : undefined;
const matching =
(re: RegExp, maxLen: number): Sanitize =>
(v) =>
typeof v === 'string' && v.length > 0 && v.length <= maxLen && re.test(v) ? v : undefined;
const token = (maxLen: number): Sanitize => matching(TOKEN_RE, maxLen);
const label = (maxLen: number): Sanitize => matching(LABEL_RE, maxLen);
const tokenArray =
(maxItems: number, maxLen: number): Sanitize =>
(v) =>
Array.isArray(v) &&
v.length <= maxItems &&
v.every((s) => typeof s === 'string' && s.length > 0 && s.length <= maxLen && TOKEN_RE.test(s))
? v
: undefined;
const nonNegInt =
(max: number): Sanitize =>
(v) =>
typeof v === 'number' && Number.isInteger(v) && v >= 0 && v <= max ? v : undefined;
/**
* THE allowlist. This mirrors docs/design/telemetry.md exactly — changing one
* without the other is a bug. Anything not listed here does not exist as far
* as this endpoint is concerned.
*/
// `sqlite_backend` (`native`/`wasm`) below is a LEGACY field: pre-schema-v2 clients
// (≤ June 2026) sent it, but node:sqlite is now the only backend so current clients
// omit it. Kept here so old clients' events still validate; safe to drop once their
// share is negligible. Never `required`.
const EVENTS: Record<string, { required: readonly string[]; props: Record<string, Sanitize> }> = {
install: {
required: ['scope', 'kind'],
props: {
targets: tokenArray(12, 24),
scope: oneOf(['local', 'global']),
kind: oneOf(['fresh', 'upgrade', 'reinstall']),
sqlite_backend: oneOf(['native', 'wasm']),
},
},
index: {
required: [],
props: {
languages: tokenArray(32, 24),
file_count_bucket: oneOf(['<100', '100-1k', '1k-10k', '10k+']),
duration_bucket: oneOf(['<10s', '10-60s', '1-5m', '5m+']),
sqlite_backend: oneOf(['native', 'wasm']),
},
},
usage_rollup: {
required: ['kind', 'name', 'count'],
props: {
kind: oneOf(['mcp_tool', 'cli_command']),
name: token(64),
count: nonNegInt(1_000_000),
error_count: nonNegInt(1_000_000),
client_name: label(64),
client_version: label(32),
},
},
uninstall: {
required: [],
props: { targets: tokenArray(12, 24) },
},
};
/** Envelope fields shared by every event in a batch (sanitized, all optional). */
const ENVELOPE_PROPS: Record<string, Sanitize> = {
codegraph_version: token(32),
os: token(16),
arch: token(16),
node_major: nonNegInt(99),
ci: (v) => (typeof v === 'boolean' ? v : undefined),
schema_version: nonNegInt(99),
};
interface PostHogEvent {
event: string;
distinct_id: string;
timestamp?: string;
properties: JsonObject;
}
function clampTimestamp(v: unknown): string | undefined {
if (typeof v !== 'string') return undefined;
const t = Date.parse(v);
if (!Number.isFinite(t)) return undefined;
const now = Date.now();
// Rollups arrive up to a few days late (offline buffers); reject implausible times.
if (t > now + 10 * 60_000 || t < now - 30 * 86_400_000) return undefined;
return new Date(t).toISOString();
}
function sanitizeEvent(raw: unknown, machineId: string, common: JsonObject): PostHogEvent | null {
if (typeof raw !== 'object' || raw === null) return null;
const e = raw as JsonObject;
if (typeof e.event !== 'string') return null;
const spec = EVENTS[e.event];
if (!spec) return null;
const rawProps = (typeof e.props === 'object' && e.props !== null ? e.props : {}) as JsonObject;
const props: JsonObject = {};
for (const [key, sanitize] of Object.entries(spec.props)) {
const val = sanitize(rawProps[key]);
if (val !== undefined) props[key] = val;
}
for (const req of spec.required) {
if (!(req in props)) return null;
}
const out: PostHogEvent = {
event: e.event,
distinct_id: machineId,
properties: {
...props,
...common,
// Anonymous events: no person profiles, no geo enrichment.
$process_person_profile: false,
$geoip_disable: true,
$lib: 'codegraph-telemetry-worker',
},
};
const ts = clampTimestamp(e.ts);
if (ts !== undefined) out.timestamp = ts;
return out;
}
async function forwardToPostHog(env: Env, batch: PostHogEvent[]): Promise<void> {
try {
const res = await fetch(`${env.POSTHOG_HOST}/batch/`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ api_key: env.POSTHOG_KEY, batch }),
signal: AbortSignal.timeout(5000),
});
if (!res.ok) {
console.error(JSON.stringify({ msg: 'posthog forward failed', status: res.status, events: batch.length }));
}
} catch (err) {
console.error(JSON.stringify({ msg: 'posthog forward error', err: String(err), events: batch.length }));
}
}
export default {
async fetch(request, env, ctx): Promise<Response> {
try {
const url = new URL(request.url);
if (request.method === 'GET' && url.pathname === '/') {
return new Response(INFO_TEXT, { headers: { 'content-type': 'text/plain; charset=utf-8' } });
}
if (url.pathname !== '/v1/events') {
return new Response('not found\n', { status: 404 });
}
if (request.method !== 'POST') {
return new Response('method not allowed\n', { status: 405, headers: { allow: 'POST' } });
}
const contentLength = Number(request.headers.get('content-length'));
if (!Number.isFinite(contentLength) || contentLength <= 0) {
return new Response('length required\n', { status: 411 });
}
if (contentLength > MAX_BODY_BYTES) {
return new Response('payload too large\n', { status: 413 });
}
let body: JsonObject;
try {
const text = await request.text();
if (text.length > MAX_BODY_BYTES) return new Response('payload too large\n', { status: 413 });
const parsed: unknown = JSON.parse(text);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
return new Response('bad request\n', { status: 400 });
}
body = parsed as JsonObject;
} catch {
return new Response('bad request\n', { status: 400 });
}
const machineId = body.machine_id;
if (typeof machineId !== 'string' || !UUID_RE.test(machineId)) {
return new Response('bad request\n', { status: 400 });
}
// Best-effort rate limit; fails open — losing a data point beats losing availability.
try {
const { success } = await env.MACHINE_RATE_LIMITER.limit({ key: machineId });
if (!success) return new Response('rate limited\n', { status: 429 });
} catch (err) {
console.error(JSON.stringify({ msg: 'rate limiter unavailable', err: String(err) }));
}
const common: JsonObject = {};
for (const [key, sanitize] of Object.entries(ENVELOPE_PROPS)) {
const val = sanitize(body[key]);
if (val !== undefined) common[key] = val;
}
const rawEvents = Array.isArray(body.events) ? body.events.slice(0, MAX_EVENTS_PER_BATCH) : [];
const batch: PostHogEvent[] = [];
for (const raw of rawEvents) {
const sanitized = sanitizeEvent(raw, machineId, common);
if (sanitized) batch.push(sanitized);
}
if (batch.length > 0) {
ctx.waitUntil(forwardToPostHog(env, batch));
}
// Accepted (including "everything was dropped by the allowlist") — the
// client treats every response as final and never retries.
return new Response(null, { status: 204 });
} catch (err) {
console.error(JSON.stringify({ msg: 'unhandled error', err: String(err) }));
return new Response('internal error\n', { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noEmit": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": []
},
"include": ["src/**/*", "worker-configuration.d.ts"]
}
+30
View File
@@ -0,0 +1,30 @@
// codegraph telemetry ingest — see README.md and docs/design/telemetry.md.
// Secrets are NOT configured here: POSTHOG_KEY is set via `wrangler secret put POSTHOG_KEY`.
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "codegraph-telemetry",
"main": "src/index.ts",
"compatibility_date": "2026-06-12",
"compatibility_flags": ["nodejs_compat"],
// First-party endpoint. The custom domain auto-provisions DNS + cert when the
// getcodegraph.com zone is on the deploying account. workers.dev stays off so
// the only public surface is the documented one.
"routes": [{ "pattern": "telemetry.getcodegraph.com", "custom_domain": true }],
"workers_dev": false,
"observability": { "enabled": true, "head_sampling_rate": 1 },
// Non-secret config. Swap host here if the backend ever moves (EU, self-hosted…).
"vars": { "POSTHOG_HOST": "https://us.i.posthog.com" },
// Per-machine_id rate limit. Legit clients flush a handful of times per day;
// 6/min absorbs install+index bursts while capping abuse.
"ratelimits": [
{
"name": "MACHINE_RATE_LIMITER",
"namespace_id": "1001",
"simple": { "limit": 6, "period": 60 }
}
]
}