Analytics
PostHog, one project key (PUBLIC_POSTHOG_KEY) used on both client and server. Instrument the chokepoints, not individual features. Four server-side events at three write chokepoints cover structural writes, message activity, and the full agent pipeline. Client-side auto-capture handles everything else.
Architecture
All writes funnel through narrow proxy handlers. Every structural change goes through POST /api/mutate. Every message goes through POST /api/streams/[scope_id]. Every agent activation flows through the dispatch pipeline in the same stream handler. These are the only places server-side analytics code lives.
The agent DO (in the separate agents worker) has no PostHog dependency. Instead, the DO returns activation metadata — model, token counts, duration — in its RPC response (ActivationResult in packages/core/agents.ts). The dispatch handler in the app worker reads these fields and emits analytics events. This keeps PostHog in one package with one capture function, and the DO remains a pure compute runtime.
All server events use the same captureEvent() function ($lib/server/posthog.ts) — a direct HTTP POST to PostHog’s /capture/ endpoint, compatible with Cloudflare Workers (no posthog-node). Every call is fire-and-forget via waitUntil. Failures are logged but never thrown. If PUBLIC_POSTHOG_KEY is empty, everything no-ops.
Client side
$lib/analytics.ts wraps posthog-js. The root layout calls initAnalytics() on mount, identifyUser() on sign-in, and resetAnalytics() on sign-out. PostHog auto-captures pageviews, page-leaves, and clicks — no manual capture() calls anywhere in page code.
Server side events
mutation
Fired from POST /api/mutate — the single entry point for all structural writes (record and permission CRUD).
| Property | Value |
|---|---|
distinct_id | caller’s agent ID |
action | insert, update, or delete |
record_type | house, room, agent, permission, etc. |
agent_kind | human or bot (derived from auth method) |
One event per mutation. Covers every structural change in the system.
stream_message
Fired from POST /api/streams/[scope_id] after the upstream write succeeds — the proxy for all message writes.
| Property | Value |
|---|---|
distinct_id | caller’s agent ID |
scope_id | room UUID |
agent_kind | human or bot |
One event per message posted. Bot replies that bypass the proxy (posted directly to the durable stream by the DO) are not captured here — they show up in agent_activation instead.
agent_dispatch
Fired from dispatchToAgents() after all agent DOs have responded. One event per triggering message that activates at least one agent.
| Property | Value |
|---|---|
distinct_id | message author’s agent ID |
room_id | room UUID |
candidates | total bots in scope |
dispatched | bots that passed filtering |
mention_count | dispatched via @mention |
ambient_count | dispatched via ambient mode |
duration_ms | wall-clock time for the full dispatch |
agent_activation
Fired from dispatchAgents() per agent, reading the enriched ActivationResult from the DO’s RPC response.
| Property | Value |
|---|---|
distinct_id | agent ID |
room_id | room UUID |
trigger | mention or ambient |
ok | true or false |
phase | reply-posted, dedup-skip, ambient-scheduled, error, dispatch-error |
model | LLM model ID (when phase involves inference) |
tokens_in | input tokens (when available) |
tokens_out | output tokens (when available) |
duration_ms | wall-clock activation time |
For mention triggers, the full pipeline runs synchronously and returns LLM metrics. For ambient triggers, the DO returns phase: ambient-scheduled immediately — token counts aren’t available in PostHog for ambient activations. The DO’s own activation log (see observability.md) covers this gap.
Known gaps
Ambient activation outcomes (LLM result after scheduled trigger) — PostHog sees the scheduling, not the outcome. Read-path activity (stream GETs, shape subscriptions) — high volume, low insight; use Cloudflare Workers Analytics Engine if needed.
Configuration
PUBLIC_POSTHOG_KEY and PUBLIC_POSTHOG_HOST in packages/www/.env. See environment-variables.md. If the key is empty, all analytics — client and server — are disabled.
Key files
| File | What it does |
|---|---|
packages/www/src/lib/analytics.ts | Client-side PostHog wrapper (init, identify, reset) |
packages/www/src/lib/server/posthog.ts | Server-side capture via HTTP (Worker-compatible) |
packages/www/src/routes/api/mutate/+server.ts | mutation event |
packages/www/src/routes/api/streams/[scope_id]/+server.ts | stream_message event |
packages/www/src/lib/server/agent-dispatch.ts | agent_dispatch + agent_activation events |
packages/core/agents.ts | ActivationResult type (DO → app telemetry contract) |
packages/worker/src/agent.ts | Returns LLM metrics in ActivationResult |