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 + error events 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

NameTypeWhenctx fields
installeventchrome.runtime.onInstalled firesreason ("install" / "update" / etc.)
startupeventService worker boots
personality_acceptedeventUser clicks "Looks right — power it up" in setup previewentryCount, hasDescription, traitCount, packCount
provider_pickedeventUser picks Chrome AI / On-device / Bring-your-ownprovider, tier (webgpu only)
account_choiceeventUser picks "Local only" or "Connect to cotext.io" on the account stepchoice ("local" / "cloud")
account_connectedevent/connect handshake succeeds and the extension has a publish tokenusername
ChromeAiSetupFailederrorChrome AI verification call throws
WebgpuSetupFailederrorweb-llm preload throwstier
InterpretHintSaveFailederrorPer-profile interpretation hint fails to persist
UnhandledRejectionerrorAny unhandled promise rejection in SW / popup / offscreencontext ("background" / "popup" / "offscreen")
UncaughtErrorerrorAny uncaught error event in SW / popup / offscreencontext

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:

  1. Open webhook.site, copy your unique URL
  2. Paste it into the popup → Telemetry → Endpoint
  3. Toggle "Send anonymous events" on
  4. Trigger an event (open a popup, pick a provider, install/reinstall)
  5. 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:

  1. Set telemetryEndpoint in popup → Telemetry
  2. Tick the "Send anonymous events" checkbox

Either step missing → no requests.

See also