Skip to content

Commit 8cdc1e6

Browse files
iteloclaude
andauthored
feat(wizard): send analytics straight to PostHog (#71)
The funnel events landed in #68 posting to a Seam endpoint that would forward them to PostHog, on the reasoning that a published CLI cannot hold a PostHog key. The proxy bought nothing — a PostHog project key is write-only — and it did not exist yet, so every batch a wizard run sent was a 404 it silently dropped. Report through the PostHog Node SDK instead, to the same project and the same host the Console reports to (`https://e.seam.co`, Seam's PostHog proxy). The SDK owns batching, gzip, retries and the `/batch/` wire format; `flushAnalytics` becomes its `shutdown()`, still the only analytics a developer waits for, still bounded. The key stays out of this repository. `analytics-key.ts` ships an empty placeholder that `prepack.ts` fills in from SEAM_WIZARD_POSTHOG_KEY when the package is packed — the mechanism `version.ts` already uses for the version — and the publish workflow passes it from a repository secret. A build without one reports nothing at all rather than reporting somewhere unintended, which also keeps a clone and a pull request build silent. **The secret has to exist before the next publish**, or the published wizard reports nothing. The public surface of analytics.ts is unchanged, so app.tsx keeps its call sites: what changed is the transport underneath. Two things the Seam side used to do move here, since nothing sits between the CLI and PostHog now: - `distinct_id` is namespaced `wizard_cli_<uuid>` — a Console person's distinct_id is a UUID too, and a run is an install, not a person; - the workspace group comes from the SDK's own `groups` argument rather than a hand-set `$groups` property. `postWizardEvents` and its wire types are deleted with the proxy. The SDK sets `$geoip_disable`, so the events still carry no location. Tests assert against the real gzipped `/batch/` payload rather than a shape of our own, which is what a local ingest server was used to verify. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 061faee commit 8cdc1e6

13 files changed

Lines changed: 312 additions & 247 deletions

‎.github/workflows/_build.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ on:
99
type: string
1010
required: false
1111
default: '24'
12+
secrets:
13+
posthog_project_key:
14+
description: >-
15+
Seam's PostHog project key, injected into the package when it is
16+
packed. Without it the packed wizard reports no analytics, which is
17+
what a pull request build should do.
18+
required: false
1219
outputs:
1320
artifact_name:
1421
description: The artifact name.
@@ -30,6 +37,8 @@ jobs:
3037
run: npm run build
3138
- name: Package
3239
run: npm pack
40+
env:
41+
SEAM_WIZARD_POSTHOG_KEY: ${{ secrets.posthog_project_key }}
3342
- name: Upload artifact
3443
uses: actions/upload-artifact@v7
3544
with:

‎.github/workflows/publish.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ jobs:
1212
build:
1313
name: Build
1414
uses: ./.github/workflows/_build.yml
15+
secrets:
16+
posthog_project_key: ${{ secrets.SEAM_WIZARD_POSTHOG_KEY }}
1517
release:
1618
name: GitHub Releases
1719
runs-on: ubuntu-latest

‎README.md‎

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ await wizard({
108108
- `SEAM_CONSOLE_URL`: Points the browser connection flow at a
109109
non-production Console.
110110
Defaults to `https://console.seam.co`.
111+
- `SEAM_WIZARD_POSTHOG_KEY`: The PostHog project
112+
[analytics](#analytics) report to.
113+
A packed package carries the one injected at pack time;
114+
set this to report from a checkout, or to report elsewhere.
115+
Without it the wizard reports nothing.
116+
- `SEAM_WIZARD_POSTHOG_HOST`: Points analytics at another PostHog host,
117+
e.g. a local instance.
118+
Defaults to `https://e.seam.co`, the host the Console reports to.
111119

112120
## Development and Testing
113121

@@ -159,7 +167,9 @@ The wizard is an [Ink] application under `src/lib`:
159167
- `app.tsx` is the state machine driving the run,
160168
one phase per step, and holds all of the rendering.
161169
- `steps/` holds the logic for each step, with no UI in it.
162-
- `analytics.ts` reports how far a run got (see [Analytics](#analytics)).
170+
- `analytics.ts` reports how far a run got (see [Analytics](#analytics)),
171+
with `analytics-key.ts` holding the project key the package is packed
172+
with.
163173
- `version.ts` holds the package version reported by `--version`.
164174
It ships a `0.0.0` placeholder that `prepack.ts` replaces with the
165175
version from `package.json` when the package is packed,
@@ -192,25 +202,29 @@ and `wizard_run_finished` closes the run with the screen it ended on —
192202
including `outcome: 'abandoned'` for a run left with Ctrl-C
193203
or a closed terminal.
194204

195-
Events are queued and posted in batches to Seam,
196-
which forwards them to PostHog:
197-
a published CLI cannot hold a PostHog key,
198-
so the key, sampling, and rate limiting all stay server side.
199-
The endpoint is unauthenticated,
200-
because the earliest and most interesting events happen before the
201-
developer has connected a key at all:
202-
203-
```
204-
POST {endpoint}/seam/wizard/v1/events
205-
{ "events": [{ "event": "…", "distinct_id": "…", "timestamp": "…", "properties": {} }] }
206-
```
207-
208-
Events are in PostHog's own capture shape so a batch can be forwarded
209-
straight through.
210-
`distinct_id` is an anonymous install id
211-
kept in the host CLI's settings — it identifies an install, not a
212-
developer — and `properties.workspace_id` is present once a run has
213-
connected.
205+
Events go to Seam's PostHog project through the
206+
[PostHog Node SDK][posthog-node], reported to `https://e.seam.co` —
207+
Seam's PostHog proxy, the same host the Console reports to.
208+
The SDK batches events and the wizard flushes once on the way out.
209+
210+
The PostHog project key is not in this repository.
211+
`analytics-key.ts` ships an empty placeholder that `prepack.ts` fills in
212+
from `SEAM_WIZARD_POSTHOG_KEY` when the package is packed,
213+
the same way `version.ts` gets the version,
214+
and the publish workflow takes it from a repository secret.
215+
A build without one — a clone, or a pull request build — reports nothing
216+
at all rather than reporting somewhere unintended,
217+
so a run from a checkout needs `SEAM_WIZARD_POSTHOG_KEY` in the
218+
environment to report anywhere.
219+
220+
`distinct_id` is an anonymous install id kept in the host CLI's
221+
settings, namespaced as `wizard_cli_<uuid>`:
222+
a Console person's `distinct_id` is a UUID too, and a run is an install,
223+
not a person.
224+
Once a run connects, its events carry `workspace_id` and join that
225+
workspace's PostHog group.
226+
227+
[posthog-node]: https://posthog.com/docs/libraries/node
214228

215229
Reporting is best effort and never affects a run:
216230
`track` does not throw or block the render,

‎package-lock.json‎

Lines changed: 37 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@
9090
"ink-text-input": "^6.0.0",
9191
"minimist": "^1.2.8",
9292
"open": "^11.0.0",
93-
"pi-mcp-adapter": "^2.27.0"
93+
"pi-mcp-adapter": "^2.27.0",
94+
"posthog-node": "^5.51.6"
9495
},
9596
"devDependencies": {
9697
"@seamapi/cli": "^0.34.0",

‎prepack.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@ import { fileURLToPath } from 'node:url'
44
import { $ } from 'execa'
55

66
const versionFile = './src/lib/version.ts'
7+
const analyticsKeyFile = './src/lib/analytics-key.ts'
78

89
const main = async (): Promise<void> => {
910
const version = await injectVersion(resolveFile(versionFile))
1011
// eslint-disable-next-line no-console
1112
console.log(`✓ Version ${version} injected into ${versionFile}`)
1213

14+
const injected = await injectPostHogProjectKey(resolveFile(analyticsKeyFile))
15+
// eslint-disable-next-line no-console
16+
console.log(
17+
injected
18+
? `✓ PostHog project key injected into ${analyticsKeyFile}`
19+
: `▲ No SEAM_WIZARD_POSTHOG_KEY set — this package reports no analytics`,
20+
)
21+
1322
const { command } = await $`tsc --project tsconfig.prepack.json`
1423
// eslint-disable-next-line no-console
1524
console.log(`✓ Rebuilt with '${command}'`)
@@ -31,6 +40,23 @@ const injectVersion = async (path: string): Promise<string> => {
3140
return version
3241
}
3342

43+
// The key is a build input, not a checked-in constant: it comes from the
44+
// environment (a repository secret in the publish workflow). A pack without it
45+
// still succeeds — the package simply reports nothing — because a missing
46+
// analytics key must never be what stops a release.
47+
const injectPostHogProjectKey = async (path: string): Promise<boolean> => {
48+
const key = process.env['SEAM_WIZARD_POSTHOG_KEY']
49+
if (key == null || key.length === 0) return false
50+
51+
await replaceInFile(
52+
path,
53+
"const seamPostHogProjectKey = ''",
54+
`const seamPostHogProjectKey = '${key}'`,
55+
)
56+
57+
return true
58+
}
59+
3460
const replaceInFile = async (
3561
path: string,
3662
placeholder: string,

‎src/lib/analytics-key.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Replaced with Seam's PostHog project key when the package is packed, the same
2+
// way version.ts gets the version. Empty in a clone — and in any package built
3+
// without the key — which leaves analytics off rather than reporting somewhere
4+
// unintended.
5+
const seamPostHogProjectKey = ''
6+
7+
export default seamPostHogProjectKey

0 commit comments

Comments
 (0)