airi/apps/server/docs/ai-context/verifications/posthog-forwarding-and-pageview.md
RainbowBird 0642ede8d1
Some checks failed
CI / Lint (push) Has been cancelled
CI / Build Test (stage-tamagotchi) (push) Has been cancelled
CI / Build Test (stage-tamagotchi-godot) (push) Has been cancelled
CI / Build Test (stage-web) (push) Has been cancelled
CI / Build Test (ui-loading-screens) (push) Has been cancelled
CI / Build Test (ui-transitions) (push) Has been cancelled
CI / Unit Test (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Check Provenance (push) Has been cancelled
Cloudflare Pages (Auth UI) / Deploy - ui-server-auth (push) Has been cancelled
Cloudflare Workers / Deploy - stage-web (push) Has been cancelled
Update Nix pnpmDeps Hash / update (push) Has been cancelled
feat(analytics): expand PostHog coverage and forward server business facts (#2038)
## What

Closes the three structural gaps in AIRI's product analytics: the signup
surface had zero instrumentation, the payment funnel had no terminator
in PostHog, and SPA route changes emitted no pageviews. Also adds
semantic events for character cards, desktop-only features, and
data-maintenance actions.

## User paths

- User signs up / logs in / verifies email / resets password / links
OAuth / deletes account → each step now emits a PostHog event from
`apps/ui-server-auth` (previously fully uninstrumented), with
`identify()` wired on session load so anonymous funnel events merge into
the user person.
- User pays via Stripe → webhook writes `product_events` as before, and
the product-events service now forwards `payment_completed` (plus signup
and subscription lifecycle facts) to PostHog via posthog-node
`captureImmediate`, keyed by the Better Auth user id → the
`checkout_started → payment_completed` funnel closes end-to-end.
Per-request LLM/TTS volume is explicitly not forwarded.
- User navigates between routes in any surface (web / desktop / pocket /
docs) → `$pageview` + `$pageleave` fire per route change via the
posthog-js `defaults: '2025-05-24'` preset in the shared
`posthog.config.ts`.
- User creates / imports / duplicates / edits a ccv3 card, switches
stage background, runs destructive data actions (export / import / clear
chats, reset providers, wipe app data), or uses desktop differentiators
(Spotlight send, widget windows, in-app updater, MCP server management,
pairing QR) → dedicated low-cardinality events.

## Notable decisions

- Server forwarding defaults on: `POSTHOG_PROJECT_KEY` defaults to the
shared browser-safe phc_* project key; set it to an empty string to
disable. Postgres `product_events` remains the source of truth.
- Cross-surface events (`oauth_callback_failed`, account lifecycle)
share one stage vocabulary exported from stage-ui so the two emitters
cannot drift silently.
- Events captured right before full-page navigation use `sendBeacon` so
they survive the redirect (checkout, OAuth consent handoff, login
redirect).
- Removed dead wrappers (`trackSignup`, `trackFirstModelSelected`,
`trackModelChanged`) that duplicated live event streams under second
names.

## How tested

- `pnpm -F @proj-airi/server exec vitest run
src/services/domain/product-events.test.ts` — 6 passed, covering the
forwarding allowlist, the `user_signed_up → signup_completed` mapping,
non-forwarded per-request actions, and a throwing sink not failing the
webhook path nor losing the DB row.
- stage-ui suites (`use-analytics`, `use-linked-accounts`, exports
contract) — 22 passed, including new account/card/data/desktop event
assertions.
- Real transport smoke: posthog-node `captureImmediate` against
`us.i.posthog.com` with the production key resolved in 1380ms (one
`server_forwarding_smoke_test` event left in the project; filter by
event name).
- Browser-tested pageviews: `VITE_ENABLE_POSTHOG=true` dev build, two
`history.pushState` route changes each produced a `$pageview` with
`$pathname`, `navigation_type: pushState`, previous-page dwell time, and
the `surface` super property; batched POST to `us.i.posthog.com/e/`
returned 200. Note: posthog-js drops events from automated browsers
(`navigator.webdriver`) by default — the verification session bypassed
the bot filter locally; production config is untouched.
- Typecheck and lint pass for server, stage-ui, stage-pages, stage-web,
stage-tamagotchi, ui-server-auth.

Full verification record:
`apps/server/docs/ai-context/verifications/posthog-forwarding-and-pageview.md`

## Follow-ups (not in this PR)

- Bot channel usage stats (Discord / Telegram) once they route through
server-runtime counters.
- Main-process desktop events (tray menu, global shortcut fire) need
renderer relay plumbing.
- Confirm `payment_completed` arrives in PostHog after the first real
Stripe payment post-deploy.

https://claude.ai/code/session_01Q1yGavkQ1P41YhTWE4XKex
2026-07-08 15:33:39 +08:00

2.5 KiB
Raw Blame History

Verification: 服务端 PostHog 转发 + SPA 路由 pageview

Status: transport 与 pageview 已真实验证;线上 Stripe webhook 端到端待部署后确认 Owner: Product Analytics Last updated: 2026-07-08 Environment: commit 689f02ac4 + 本次工作区改动posthog-node 5.39.4posthog-js 1.306.1Node 26.3.0

用户路径

  • 场景 1:用户完成 Stripe 支付 → webhook 写 product_events → 服务端把 payment_completed 转发到 PostHogdistinctId = Better Auth user id→ PostHog 付费漏斗在 checkout_started 后闭环。
  • 场景 2:用户在 stage-web 内切换路由(如进入 /settings/flux)→ PostHog 收到 $pageview,带 $pathname、上一页路径与停留时长。

已验证证据

验证项 命令 / 方式 实际输出
转发白名单与映射 pnpm exec vitest run src/services/domain/product-events.test.tsapps/server 6 passedpayment_completed 原名转发、user_signed_up→signup_completed 映射、per-request 动作不转发、sink 抛错时 DB 行仍落库且 track 不抛
posthog-node 真实传输 node posthog-smoke.mjscaptureImmediate → us.i.posthog.com生产 project key事件名 server_forwarding_smoke_test captureImmediate resolved in 1380ms + shutdown clean
SPA 路由 pageview VITE_ENABLE_POSTHOG=true pnpm -F @proj-airi/stage-web dev + agent-browser 两次 history.pushState 两条 $pageview$pathname 分别为 /settings/flux/settings/airi-cardnavigation_type: pushState,携带 $prev_pageview_durationsurface: web super property批量 POST us.i.posthog.com/e/ 返回 200
服务端 typecheck / lint pnpm -F @proj-airi/server typecheck、eslint 改动文件 均通过

注意事项

  • 自动化浏览器(navigator.webdriver = true)会被 posthog-js 默认 bot 过滤静默丢弃事件;本次浏览器验证通过会话内 set_config({ opt_out_useragent_filter: true }) 绕过,仅影响该验证会话,生产配置未改。人工复测时用普通浏览器即可,无需任何绕过。
  • 服务端转发默认开启:POSTHOG_PROJECT_KEY 的默认值就是前端共用的 phc_* project key置空字符串可关闭。部署后在 PostHog 里确认 payment_completed 事件出现在真实支付后,即完成端到端收尾。
  • PostHog 项目里会留有一条 server_forwarding_smoke_testdistinctId verification-smoke)测试事件,分析时按事件名过滤。