PostHog analytics for agents
This guide shows how to track, quantify and analyse user behaviour across every agent in the marketplace. The design has two halves:
- Each agent captures events into PostHog, tagged with its own
agentslug. - The marketplace admin has one Behavior dashboard that queries PostHog and shows the Purchased → Activated → Used funnel for all agents at once, with a per-agent breakdown.
So an agent author only wires up capture; the moment events start flowing they appear in the central dashboard — no per-agent UI to build.
The reference capture implementation is Convrox (agent-poc); the central
dashboard lives in the marketplace service + marketplace frontend.
The marketplace service also uses PostHog for request-level audit logging
(api_request events — see Observability). This guide is
about product analytics: lifecycle funnels and engagement. Same PostHog
project, different events.
Architecture at a glance
| Concern | Credential | Where it runs | Files |
|---|---|---|---|
| Capture (server) | Project API key phc_… | Each agent backend | agent-poc/backend/posthog.js |
| Capture (browser) | Project API key phc_… | Each agent frontend | agent-poc/frontend/src/auth.jsx |
| Query (read) | Personal API key phx_… | Marketplace service, admin-only | marketplace-fleapoai-service/src/analytics/ |
| Dashboard | — | Marketplace admin | fleapo-marketplace/src/pages/admin/AdminBehavior.tsx |
The read side is admin-gated (PlatformAdminGuard) and lives only in the
marketplace — individual agents never query PostHog.
1. The lifecycle event taxonomy
Every agent emits the same three events, keyed to the marketplace user id, so one set of dashboards works for the whole platform:
| Funnel step | Event | When |
|---|---|---|
| Purchased | subscription_active | subscription resolves as active/trialing |
| Activated | dashboard_opened | first authenticated dashboard load |
| Used | agent_conversation | first real product use (e.g. a widget conversation) |
Every event must carry an agent property = the agent's marketplace slug.
That property is what the central dashboard breaks down by.
2. Capture — server side (per agent)
agent-poc/backend/posthog.js wraps posthog-node and stamps the agent slug
onto every event centrally, so individual track() calls don't have to:
const agentSlug = process.env.MARKETPLACE_AGENT_SLUG || "unknown";
export function track(distinctId, event, properties = {}) {
if (!posthog) return;
posthog.capture({
distinctId: distinctId || "anonymous",
event,
properties: { agent: agentSlug, ...properties },
});
}
Emit the lifecycle events at the right moments (Convrox, server.js):
// /api/me/tier — first authed call per dashboard load
track(userId, "dashboard_opened", { tier, status });
if (tierName && ["active", "trialing"].includes(status)) {
track(userId, "subscription_active", { tier, status });
}
// /api/chat — first real widget message, attributed to the KB owner (customer)
track(kb.ownerId, "agent_conversation", { domain, channel: "widget" });
The person chatting with a deployed widget is an anonymous end-visitor. Keep the funnel about customers by attributing the "Used" event to the agent's owner (the marketplace subscriber), not the visitor's session.
3. Capture — browser side (per agent)
agent-poc/frontend/src/auth.jsx initialises posthog-js once and identifies
the user after sign-in (autocapture handles pageviews/clicks):
posthog.init(POSTHOG_KEY, { api_host: POSTHOG_HOST, person_profiles: "identified_only" });
posthog.identify(user.id, { email, name, marketplace_tier, marketplace_status });
// on sign-out: posthog.reset();
Use the marketplace user id as the distinct id on both sides so browser and server events stitch into one person.
Agent env vars
POSTHOG_API_KEY=phc_xxx # backend capture
POSTHOG_HOST=https://us.i.posthog.com
MARKETPLACE_AGENT_SLUG=your-agent-slug # tags every event (already set for OAuth)
VITE_POSTHOG_KEY=phc_xxx # frontend capture
VITE_POSTHOG_HOST=https://us.i.posthog.com
4. The central read side (marketplace service)
marketplace-fleapoai-service/src/analytics/ adds an admin module that queries
PostHog's HogQL query API with a personal API key:
POST {POSTHOG_QUERY_HOST}/api/projects/{POSTHOG_PROJECT_ID}/query
Authorization: Bearer {POSTHOG_PERSONAL_API_KEY}
{ "query": { "kind": "HogQLQuery", "query": "<SQL>" } }
behavior-analytics.service.ts— runs the queries (axios, per repo convention).admin-analytics.controller.ts—GET /admin/analytics/behavior?days=&agent=, guarded byPlatformAdminGuard.- Config getters live in
app-config.service.ts(posthogPersonalApiKey,posthogProjectId,posthogQueryHost,posthogQueryEnabled) — never readprocess.envdirectly.
The funnel is a strict subset (each step requires all prior steps) so it's
always monotonic, even when subscription_active and dashboard_opened land in
the same request. The per-agent breakdown groups by the agent property:
SELECT agent,
countIf(has_purchase) AS purchased,
countIf(has_purchase AND has_activate) AS activated,
countIf(has_purchase AND has_activate AND has_used) AS used
FROM (
SELECT person_id, properties.agent AS agent,
max(event = 'subscription_active') AS has_purchase,
max(event = 'dashboard_opened') AS has_activate,
max(event = 'agent_conversation') AS has_used
FROM events
WHERE timestamp >= now() - INTERVAL 30 DAY
AND event IN ('subscription_active','dashboard_opened','agent_conversation')
GROUP BY person_id, properties.agent
)
GROUP BY agent ORDER BY purchased DESC
When the personal key / project id are unset the endpoint returns
{ enabled: false } so the dashboard shows a setup hint instead of erroring.
Marketplace service env vars
POSTHOG_PERSONAL_API_KEY=phx_xxx # Settings → Personal API keys, scoped to read Query/Insights
POSTHOG_PROJECT_ID=12345 # Settings → Project (numeric id)
POSTHOG_QUERY_HOST=https://us.posthog.com # app host (no `i.`); defaults to POSTHOG_HOST
5. The Behavior dashboard (marketplace frontend)
fleapo-marketplace/src/pages/admin/AdminBehavior.tsx renders the funnel, daily
trends, headline stats, and the per-agent breakdown table with recharts. It's
routed at /admin/behavior (under the RequirePlatformAdmin admin shell in
App.tsx) and linked from the admin sidebar nav in AdminLayout.tsx. Data comes
from the hand-rolled useBehaviorAnalytics hook (src/hooks/), which calls the
endpoint via customFetch. A range toggle (7/30/90d) and an agent filter drive
the query params.
Adding analytics to a new agent — checklist
Because the dashboard is central and breaks down by the agent property, a new
agent only needs the capture side:
- Add the agent env vars (step 3):
POSTHOG_API_KEY,POSTHOG_HOST,MARKETPLACE_AGENT_SLUG,VITE_POSTHOG_KEY,VITE_POSTHOG_HOST. - Copy
posthog.js(with theagentslug tagging) into the agent backend and initposthog-js+identify()in the frontend auth gate. - Emit
subscription_active,dashboard_opened,agent_conversationat your equivalents of subscribe / sign-in / first product use, keyed to the marketplace user id. - That's it — the agent's slug shows up automatically in the marketplace Behavior dashboard's per-agent breakdown.
Keep the event names and the agent property identical across agents — the
shared taxonomy is what makes one dashboard cover the whole marketplace.