Skip to content

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).

PropertyValue
distinct_idcaller’s agent ID
actioninsert, update, or delete
record_typehouse, room, agent, permission, etc.
agent_kindhuman 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.

PropertyValue
distinct_idcaller’s agent ID
scope_idroom UUID
agent_kindhuman 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.

PropertyValue
distinct_idmessage author’s agent ID
room_idroom UUID
candidatestotal bots in scope
dispatchedbots that passed filtering
mention_countdispatched via @mention
ambient_countdispatched via ambient mode
duration_mswall-clock time for the full dispatch

agent_activation

Fired from dispatchAgents() per agent, reading the enriched ActivationResult from the DO’s RPC response.

PropertyValue
distinct_idagent ID
room_idroom UUID
triggermention or ambient
oktrue or false
phasereply-posted, dedup-skip, ambient-scheduled, error, dispatch-error
modelLLM model ID (when phase involves inference)
tokens_ininput tokens (when available)
tokens_outoutput tokens (when available)
duration_mswall-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

FileWhat it does
packages/www/src/lib/analytics.tsClient-side PostHog wrapper (init, identify, reset)
packages/www/src/lib/server/posthog.tsServer-side capture via HTTP (Worker-compatible)
packages/www/src/routes/api/mutate/+server.tsmutation event
packages/www/src/routes/api/streams/[scope_id]/+server.tsstream_message event
packages/www/src/lib/server/agent-dispatch.tsagent_dispatch + agent_activation events
packages/core/agents.tsActivationResult type (DO → app telemetry contract)
packages/worker/src/agent.tsReturns LLM metrics in ActivationResult