Telemetry
Cotext's extension can POST anonymous events to a receiver of your choice. It is opt-in and off by default — no requests are made until both an endpoint URL is configured AND the toggle is on.
The full client lives at src/telemetry/client.ts in the repo.
Why
The maintainer is otherwise flying blind in the first weeks after a release. Telemetry surfaces:
- Install funnel — how many users got past the personality step, picked a provider, saw their first prompt rendered
- Setup errors — which provider paths fail and why (disk space on Chrome AI, WebGPU init failures, Ollama unreachable, etc.)
- Uncaught crashes — unhandled promise rejections +
errorevents in the popup, service worker, and offscreen document
It does NOT surface:
- Signal content (the AI response you reacted to)
- User notes, prompts, or messages
- API keys, tokens, or model weights
- Profile content (rules, metrics, customInstructions)
- Filenames or filesystem paths
Wire format (schema v1)
Every event is a single JSON POST. Two type values: event (funnel /
milestone) and error (caught/uncaught failure).
{
"v": 1,
"type": "event | error",
"name": "<event name or error class>",
"ts": 1778862917445,
"installId": "9c1a8e3b-…",
"ext": {
"version": "0.1.0",
"browser": "chrome | edge | unknown"
},
"ctx": { "...": "low-cardinality categorical only" },
"err": { "message": "...", "stack": "..." }
}
ctx is for low-cardinality categorical data (provider name, model
tier, error category). Callers must NOT put free-form text into it.
Events the extension emits today
| Name | Type | When | ctx fields |
|---|---|---|---|
install | event | chrome.runtime.onInstalled fires | reason ("install" / "update" / etc.) |
startup | event | Service worker boots | — |
personality_accepted | event | User clicks "Looks right — power it up" in setup preview | entryCount, hasDescription, traitCount, packCount |
provider_picked | event | User picks Chrome AI / On-device / Bring-your-own | provider, tier (webgpu only) |
account_choice | event | User picks "Local only" or "Connect to cotext.io" on the account step | choice ("local" / "cloud") |
account_connected | event | /connect handshake succeeds and the extension has a publish token | username |
ChromeAiSetupFailed | error | Chrome AI verification call throws | — |
WebgpuSetupFailed | error | web-llm preload throws | tier |
InterpretHintSaveFailed | error | Per-profile interpretation hint fails to persist | — |
UnhandledRejection | error | Any unhandled promise rejection in SW / popup / offscreen | context ("background" / "popup" / "offscreen") |
UncaughtError | error | Any uncaught error event in SW / popup / offscreen | context |
Add more by importing reportEvent / reportError from
src/telemetry/client.ts. Keep ctx categorical.
Receiver
Cotext doesn't ship a receiver. You stand one up wherever you want. Anything that accepts JSON POSTs works.
Cheapest path: webhook.site
For testing or solo use:
- Open webhook.site, copy your unique URL
- Paste it into the popup → Telemetry → Endpoint
- Toggle "Send anonymous events" on
- Trigger an event (open a popup, pick a provider, install/reinstall)
- The event lands in the webhook.site dashboard within seconds
Good for "is the wire alive?" smoke testing. Not a long-term home.
Cloudflare Worker (~20 lines)
A free-tier receiver that aggregates by installId and name:
// worker.js — deploy via `wrangler deploy`
export default {
async fetch(req, env) {
if (req.method !== "POST") return new Response("OK", { status: 200 });
const event = await req.json().catch(() => null);
if (!event || event.v !== 1) return new Response("Bad request", { status: 400 });
// Validate the categorical fields you care about
if (!event.installId || !event.name) {
return new Response("Missing required fields", { status: 400 });
}
// Write to D1 / KV / Analytics Engine — whichever you prefer.
// Keep it boring; this is just appending rows.
await env.EVENTS.put(`${event.installId}/${event.ts}`, JSON.stringify(event));
return new Response(JSON.stringify({ ok: true }), {
headers: { "content-type": "application/json" },
});
},
};
Endpoint URL: https://your-worker.your-domain.workers.dev/.
Existing server
If you already run the website (website/), add a route:
// website/app/api/events/route.ts (Next.js app router)
export async function POST(req) {
const event = await req.json().catch(() => null);
if (!event || event.v !== 1 || !event.installId || !event.name) {
return Response.json({ ok: false }, { status: 400 });
}
// Insert into Postgres; truncate stack to ~2KB
await prisma.telemetryEvent.create({ data: { /* ... */ } });
return Response.json({ ok: true });
}
Endpoint URL: https://your-cotext-instance.com/api/events.
Privacy posture
- No third-party processor. You configure the endpoint; nothing in the code points to a Cotext-operated server. Disabling telemetry (or leaving the endpoint empty) means zero network calls beyond what the extension already does for interpretation / publishing.
- No PII. The install ID is a random UUID; the extension version and browser string are non-identifying. No IP geocoding happens client-side (your receiver controls that).
- No payload introspection. The wire format is closed (schema v1 has fixed fields). To leak signal content we'd have to add new fields to the wire format — which would show up in this doc.
- Truncated stacks. Stack traces are capped at 2 KB. Error messages at 500 chars.
Off by default
The toggle defaults to off and there's no remote opt-in path. To enable, the user must:
- Set
telemetryEndpointin popup → Telemetry - Tick the "Send anonymous events" checkbox
Either step missing → no requests.
See also
- Architecture — where telemetry sits in the data flow
- Getting started — how to enable telemetry from the popup