Add PostHog integration runbook

75cde356d7cf · AtlantisPleb · · parent 575b99f108e8

Add PostHog integration runbook

The PostHog AI wizard does not support Phoenix or Elixir, so record
what the wizard automates and how to replicate it manually: server
capture through the posthog package, bundled posthog-js with LiveView
pageviews, one canonical distinct_id, and an event taxonomy covering
every product surface.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • added docs/2026-08-21-posthog-integration-runbook.md

Diff

1 file changed, +309 -0

docs/2026-08-21-posthog-integration-runbook.md added +309

@@ -0,0 +1,309 @@

1
# PostHog integration runbook
2
3
Date: 2026-08-21
4
5
Status: Proposed
6
7
PostHog ships an agentic installer, the [AI wizard](https://github.com/PostHog/wizard), that wires PostHog into a codebase end to end. The wizard does not support Phoenix or Elixir: the framework is registered as "coming soon" in PostHog's own documentation, so running `npx @posthog/wizard` against this repository would not produce a usable integration.
8
9
This document is the manual replacement. Part 1 records what the wizard does so we know the full surface we are replicating. Part 2 turns that into a concrete runbook for this Phoenix application: server-side capture from Elixir, browser analytics through our asset bundle, pageviews that survive LiveView navigation, user identification, and an event taxonomy covering every product surface. Session replay and self-driving are out of scope.
10
11
Use this document as the single checklist for the integration work. Nothing in it requires the wizard.
12
13
## Part 1: What the PostHog wizard does
14
15
### What it is
16
17
The wizard (`npx @posthog/wizard`) is a TypeScript CLI, roughly 20K lines, distributed through npm. It authenticates the user against PostHag cloud, detects the project's framework, then hands the work to an LLM agent that edits the project directly. The agent runs inside a harness (the Claude Agent SDK or an alternative runtime) with two tool sources:
18
19
- A remote PostHog MCP server that can query project data and create insights, dashboards, and notebooks.
20
- A local in-process tool server with file-scoped helpers: `.env` inspection and writes, package-manager detection, skill installation, and a structured "ask the user" channel.
21
22
A run streams progress to a terminal UI, then finishes with an outro screen summarizing what changed.
23
24
### The default integration flow
25
26
Running the wizard with no arguments executes the default program. Its steps, in order:
27
28
1. **Detect the framework.** A registry of about 27 framework configs runs detection predicates against the install directory: presence of `package.json` dependencies for JavaScript frameworks, `manage.py` for Django, `mix.exs` defining a project for Elixir, and so on. Each config also gathers context (router type, project type) and carries version checks.
29
2. **Confirm setup.** An intro screen shows what will happen and asks for confirmation.
30
3. **Check health.** Before spending any work, the wizard polls status pages and liveness endpoints for its critical dependencies (the LLM provider, PostHog ingest, npm). If a critical dependency is down, it refuses to run.
31
4. **Ask disambiguation questions.** If detection cannot resolve a variant (for example, which router a framework uses), the user picks one.
32
5. **Authenticate.** Either an OAuth browser flow with a local callback server, or a personal API key for non-interactive runs. Tokens carry scoped permissions; the wizard verifies the grant was not narrowed.
33
6. **Install knowledge.** The wizard downloads a markdown "skill" for the detected framework from PostHog's skill registry. Skills are versioned independently of the CLI, so integration guidance improves without a wizard release.
34
7. **Run the agent.** The agent receives the credentials, the skill, and project context, then edits the codebase. For a typical web app it does all of the following:
35
   - Installs the relevant SDKs (client and server packages).
36
   - Initializes the SDKs with the project token and ingest host.
37
   - Enables autocapture and `$pageview` tracking in the browser.
38
   - Adds `identify` calls so events attach to real users.
39
   - Reads the actual product flows in the codebase and instruments meaningful custom events.
40
   - Writes credentials to `.env` files through a fenced tool so secret values never enter the model conversation.
41
   - Creates starter insights and a first dashboard in the PostHog app through the MCP tools.
42
8. **Finish.** Post-run hooks upload environment variables to detected hosting providers, the outro summarizes changes and next steps, and the wizard offers to install the PostHog MCP server into the user's AI coding clients.
43
44
### What lands in the integrated project
45
46
Regardless of framework, a completed default run produces the same product-analytics foundation:
47
48
- **Pageviews** (`$pageview`) on every navigation, including client-side routing.
49
- **Autocapture**: clicks, form submissions, and element visibility with data attributes, no per-element code required.
50
- **Identification**: anonymous browsers merge into identified persons at login.
51
- **Custom events** named after real product actions, with properties.
52
- **Web analytics basics**: UTM attribution, referrers, devices, entry pages, all derived from the events above.
53
- **Credentials** stored as environment variables, never hardcoded.
54
- **Starter dashboards** in the PostHog app.
55
56
Optional products the wizard can add but that this runbook treats separately: session replay, error tracking, feature flags, surveys, revenue analytics, data warehouse sources, and self-driving.
57
58
### Concepts worth carrying over
59
60
Three design ideas from the wizard transfer directly to a manual integration:
61
62
- **One canonical identity per user.** Browser events and server events must use the same `distinct_id`, or backend events become orphaned persons that cannot be joined to frontend behavior. The wizard enforces this in every framework template; we must enforce it ourselves.
63
- **Instrument at real product moments, not arbitrary ones.** The agent reads the codebase and places events where meaningful state changes happen (sign-up, first message, created object). A checklist of surfaces, like the taxonomy in part 2, is how you get the same coverage without an agent.
64
- **Credentials stay in configuration.** Tokens come from environment variables read at boot; nothing is committed.
65
66
## Part 2: Runbook for OpenAgents
67
68
### Scope
69
70
In scope:
71
72
- Server-side event capture from Elixir using the official [`posthog`](https://hex.pm/packages/posthog) package.
73
- Browser analytics using `posthog-js`, bundled into `assets/js/app.js` by esbuild (no snippet tag; see the CSP constraint below).
74
- Pageviews for full loads and LiveView push navigation.
75
- Autocapture.
76
- User identification with a single canonical `distinct_id` across client and server.
77
- A custom-event taxonomy covering every user-facing surface.
78
- Web analytics fundamentals that fall out of the above: traffic, referrers, UTMs, retention inputs.
79
80
Out of scope for this effort:
81
82
- Session replay and self-driving (explicitly skipped).
83
- Error tracking. Note that the Elixir package enables exception capture by default; step 2 disables it so scope stays controlled. Revisit later as its own decision.
84
- Feature flags, surveys, revenue analytics, warehouse sources. Feature flags are the most likely follow-up; the SDK support comes free once step 2 is done.
85
- Installing the PostHog MCP server into coding agents. Useful later, unrelated to instrumentation.
86
87
### Prerequisites
88
89
1. Get the public project token (`phc_...`) from PostHog project settings for the OpenAgents project at `us.posthog.com`. This token is safe to ship in the browser bundle; it identifies the project, not a person.
90
2. Confirm the ingest host: `https://us.i.posthog.com`.
91
3. Decide the rollout environment. Staging first; production remains locked behind the existing staging gate, so the integration lands in staging configuration before any production decision.
92
93
### Step 1: Route credentials through runtime configuration
94
95
Follow the `OpenAgents.RuntimeConfig` pattern documented in [runtime-configuration.md](runtime-configuration.md):
96
97
| Setting | Requirement |
98
| --- | --- |
99
| `OPENAGENTS_POSTHOG_PROJECT_TOKEN` | `phc_...` public project token; empty disables all capture |
100
| `OPENAGENTS_POSTHOG_API_HOST` | Defaults to `https://us.i.posthog.com` |
101
102
When the token is empty, the application boots normally with capture fully disabled. This keeps local development and tests free of network calls without extra flags.
103
104
### Step 2: Add the Elixir SDK
105
106
Add the dependency in `mix.exs`:
107
108
```elixir
109
{:posthog, "~> 2.0"}
110
```
111
112
Configure it in `config/config.exs` with values supplied at runtime:
113
114
```elixir
115
config :posthog,
116
  enable: true,
117
  api_host: {:system, "OPENAGENTS_POSTHOG_API_HOST", "https://us.i.posthog.com"},
118
  api_key: {:system, "OPENAGENTS_POSTHOG_PROJECT_TOKEN", nil},
119
  in_app_otp_apps: [:openagents],
120
  enable_error_tracking: false
121
```
122
123
Notes:
124
125
- `enable_error_tracking: false` keeps `$exception` capture off until error tracking is a separate approved decision.
126
- In `config/test.exs`, set `test_mode: true` so events are dropped instead of sent.
127
- The package batches events and flushes them from its own supervision tree; captures do not block request handling.
128
129
### Step 3: Create one capture boundary
130
131
Add a single wrapper module, for example `OpenAgents.Analytics`, and route every server-side capture through it. The wrapper exists to enforce policy in one place:
132
133
- No-op when the project token is unset.
134
- Merge standard properties onto every event: `environment` (from `OPENAGENTS_ENVIRONMENT`), `app_version`, and `surface` (`web`, `api`, `live`).
135
- Never raise: a capture failure must not fail the caller. Rescue, log, and continue.
136
- Redact by default: reject property keys matching a denylist (`token`, `secret`, `password`, `ciphertext`, `credential`) and drop oversized values.
137
138
```elixir
139
OpenAgents.Analytics.capture("user_signed_up", distinct_id, %{
140
  github_login: login
141
})
142
```
143
144
Passing `distinct_id` explicitly beats relying on process context. The package supports `PostHog.set_context/1` via logger metadata when explicit passing is awkward, but explicit arguments keep call sites greppable.
145
146
### Step 4: Add posthog-js to the browser bundle
147
148
Install into the existing asset pipeline:
149
150
```console
151
npm install --prefix assets posthog-js
152
```
153
154
Initialize in `assets/js/app.js`:
155
156
```javascript
157
import posthog from "posthog-js"
158
159
if (window.POSTHOG_CONFIG.enabled) {
160
  posthog.init(window.POSTHOG_CONFIG.token, {
161
    api_host: window.POSTHOG_CONFIG.api_host,
162
    defaults: '2026-05-30',
163
    autocapture: true,
164
  })
165
}
166
```
167
168
Inject the config from the root layout so no token is hardcoded in JS:
169
170
```heex
171
<body data-posthog-enabled={@posthog_enabled}
172
      data-posthog-token={@posthog_token}
173
      data-posthog-api-host={@posthog_api_host}>
174
```
175
176
Read the data attributes in `app.js` before initializing. When disabled, render empty attributes and skip initialization entirely.
177
178
Two constraints specific to this repository:
179
180
- **No inline scripts.** The CSP allows exactly one nonce-bearing script (the theme bootstrap). Bundling `posthog-js` through esbuild satisfies the policy; do not add the copy-paste snippet.
181
- **CSP `connect-src`.** `lib/openagents_web/plugs/content_security_policy.ex` currently allows only `'self' ws: wss:`. Extend `connect-src` with the ingest hosts `https://us.i.posthog.com` and `https://us-assets.i.posthog.com`, or autocapture batches will silently fail to send.
182
183
### Step 5: Capture pageviews, including LiveView navigation
184
185
`posthog-js` records full page loads automatically. LiveView navigations change the URL without a reload, so add the documented listener after initialization:
186
187
```javascript
188
window.addEventListener("phx:navigate", ({ detail: { href } }) => {
189
  if (window.posthog?.__loaded) {
190
    posthog.capture("$pageview", { $current_url: href })
191
  }
192
})
193
```
194
195
Without this listener, every authenticated LiveView surface (chat, issues, projects, memory, computers) appears as one long session on the landing URL, which corrupts traffic and funnel analysis.
196
197
### Step 6: Identify users with one canonical ID
198
199
Choose the canonical `distinct_id` now: `user_<id>` where `<id>` is the database user ID. Every producer, client and server, uses exactly this string.
200
201
Client side:
202
203
1. Render the current user's `distinct_id` and stable profile properties (`github_login`, `github_id`) as data attributes in the root layout when a session exists.
204
2. After `posthog.init`, if a distinct ID attribute is present, call:
205
206
```javascript
207
posthog.identify(distinctId, {
208
  github_login: login,
209
})
210
```
211
212
Calling `identify` merges the anonymous ID that autocapture assigned before login into the identified person, so pre-authentication pageviews connect to the account. Guard the call so it fires once per browser session.
213
214
Server side:
215
216
1. In `AuthController.callback`, after a successful upsert, capture the auth events listed in the taxonomy below using `user_<id>`.
217
2. Distinguish sign-up from sign-in by comparing `inserted_at` and `updated_at` on the returned user: equality means the row was just created.
218
3. For requests, add `plug PostHog.Integrations.Plug` immediately before `plug OpenAgentsWeb.Router` in the endpoint. It attaches `$current_url`, method, and user-agent metadata to events captured during the request, and reads `X-PostHog-Distinct-Id` and `X-PostHog-Session-Id` headers when present.
219
4. Configure `posthog-js` tracing headers so browser fetches to this app carry those headers, linking server-side events to the originating browser session. Tracing headers are client-controlled analytics hints, never authorization input; server-side code must derive identity from the session, not from these headers.
220
221
For background processes (deployment workers, delegated work) there is no browser session: pass the owning `user_<id>` explicitly, or use a system distinct ID such as `system_<worker>` for unowned operational events.
222
223
### Step 7: Instrument the event taxonomy
224
225
Naming convention: snake_case, past tense, `object_verb` (`issue_created`, `chat_message_sent`). Every event gets the standard properties from step 3 plus the listed ones. Instrument domain contexts rather than individual controllers where one context serves both the LiveView UI and the GitHub-compatible JSON API, so both surfaces produce identical events.
226
227
Public and authentication:
228
229
| Event | Where | Properties |
230
| --- | --- | --- |
231
| `auth_started` | `AuthController.start` | none beyond standard |
232
| `auth_failed` | `AuthController` failure paths | `reason`: `consent_required`, `banned`, `denied`, `failed`, `unavailable` |
233
| `user_signed_up` | `AuthController.callback` on new user | `github_login` |
234
| `user_signed_in` | `AuthController.callback` on returning user | `github_login` |
235
| `user_logged_out` | `AuthController.logout` | none |
236
237
Chat and Sarah:
238
239
| Event | Where | Properties |
240
| --- | --- | --- |
241
| `chat_opened` | `ChatLive.mount` | none |
242
| `chat_message_sent` | turn submission handler | `length_bucket`, `tools_available` |
243
| `chat_turn_completed` | turn completion (server) | `duration_ms`, `model`, `tool_count`, `outcome` |
244
| `memory_saved` | memory write path | `kind` |
245
| `memory_viewed` | `MemoryLive.mount` | none |
246
| `delegated_work_created` | delegated-work creation path | `objective_kind` |
247
| `delegated_work_completed` | worker terminal result | `outcome`, `duration_ms` |
248
| `computer_paired` | pairing approval | none |
249
| `agent_job_created` | `ComputerAgentJobsController.create` | none |
250
| `voice_call_started` / `voice_call_completed` | voice call lifecycle | `duration_ms` on completion |
251
252
Issues, projects, and forge:
253
254
| Event | Where | Properties |
255
| --- | --- | --- |
256
| `issue_created` | issue creation context | `owner`, `repo`, `has_labels`, `has_assignees` |
257
| `issue_updated` | issue update context | `fields_changed` count |
258
| `issue_commented` | comment creation context | none |
259
| `label_created`, `milestone_created`, `project_created` | respective contexts | `owner`, `repo` |
260
| `project_item_added` | project item creation | none |
261
| `git_push_received` | push receipt path | `repo`, `commits_bucket` |
262
| `release_promoted` | promotion target path | `repo` |
263
| `deployment_started` / `deployment_completed` | deployment coordinator | `outcome`, `duration_ms` on completion |
264
265
Deliberate omissions:
266
267
- Admin surfaces stay uninstrumented. They are operator-only, low volume, and would add noise to activation funnels.
268
- API read endpoints (`GET`) stay uninstrumented except where they represent product activation. Volume without signal costs money per event.
269
- Never capture message bodies, objective text, token material, ciphertexts, or raw query strings. The `$current_url` property contains query parameters; rely on the wrapper's redaction and keep sensitive routes out of custom properties.
270
271
### Step 8: Build the starter dashboards
272
273
Create these in the PostHog app after the first events land:
274
275
1. **Web overview**: visitors, pageviews, sessions, bounce rate, top pages, referrers, UTMs, devices.
276
2. **Activation funnel**: `$pageview` → `auth_started` → `user_signed_up` → `chat_message_sent`.
277
3. **Engagement**: weekly active chatters, median `chat_turn_completed` duration, delegated-work completion rate.
278
4. **Product adoption**: issues and projects created per week, pushes received.
279
5. **Volume sanity**: total events per day by name, to catch runaway capture early.
280
281
### Step 9: Verify
282
283
Work through this checklist in staging:
284
285
1. Cold load of `/` produces a `$pageview`; navigating to `/docs` through a LiveView link produces a second one with the correct `$current_url`.
286
2. Autocapture shows click events in the live events tail.
287
3. Complete a GitHub OAuth login: `auth_started`, `user_signed_up` (or `user_signed_in`), and the identify merge are visible; the person shows both the anonymous pre-login events and the identified ones.
288
4. Send a chat message: `chat_message_sent` on the client and `chat_turn_completed` on the server attach to the same person.
289
5. Create an issue through the web UI and through the JSON API: both produce `issue_created` with a `surface` property distinguishing them.
290
6. Search captured event properties for token-shaped strings and message content; find nothing.
291
7. Stop the app with `OPENAGENTS_POSTHOG_PROJECT_TOKEN` unset: boot succeeds and no network calls go to PostHog.
292
8. Run the test suite: `test_mode` drops events and no test asserts on outbound PostHog traffic.
293
9. Check the browser console for CSP violations against the ingest host; fix `connect-src` if any appear.
294
295
### Rollout order
296
297
1. Steps 1-4: foundation (config, SDKs, wrapper, client init). No taxonomy yet.
298
2. Step 5-6: pageviews and identification. Verify the auth funnel end to end.
299
3. Step 7: taxonomy, starting with authentication and chat, then issues and forge.
300
4. Step 8: dashboards.
301
5. Step 9 gates each stage; do not stack stages without verification.
302
303
## References
304
305
- [AI wizard repository](https://github.com/PostHog/wizard)
306
- [Elixir library](https://posthog.com/docs/libraries/elixir)
307
- [Phoenix guide](https://posthog.com/docs/libraries/phoenix)
308
- [JavaScript Web SDK](https://posthog.com/docs/libraries/js)
309
- [Identifying users](https://posthog.com/docs/getting-started/identify-users)

This page updates live while a promote is in flight · changelog