Files
moeka-project/apps/server/package.json
T
RainbowBird 0642ede8d1 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

79 lines
3.0 KiB
JSON

{
"name": "@proj-airi/server",
"type": "module",
"version": "0.11.0",
"private": true,
"scripts": {
"apply:env": "dotenvx run -f .env.local --overload --ignore=MISSING_ENV_FILE",
"auth:generate": "pnpm run apply:env -- better-auth generate --config src/scripts/auth.ts --output src/schemas/accounts.ts -y",
"dev": "pnpm run apply:env -- tsx --import ./instrumentation.ts --watch src/bin/run.ts api",
"start": "pnpm run apply:env -- tsx --import ./instrumentation.ts src/bin/run.ts api",
"server": "pnpm run apply:env -- tsx --import ./instrumentation.ts src/bin/run.ts",
"build": "tsc -b",
"typecheck": "tsc --noEmit",
"db:generate": "drizzle-kit generate",
"db:push": "pnpm run apply:env -- drizzle-kit push",
"otel:dashboards": "tsx otel/grafana/dashboards/build.ts"
},
"dependencies": {
"@better-auth/drizzle-adapter": "catalog:",
"@better-auth/oauth-provider": "catalog:",
"@dotenvx/dotenvx": "catalog:",
"@electric-sql/pglite": "catalog:",
"@guiiai/logg": "catalog:",
"@hono/node-server": "catalog:",
"@hono/node-ws": "catalog:",
"@hono/otel": "catalog:",
"@langfuse/otel": "catalog:",
"@langfuse/tracing": "catalog:",
"@moeru/eventa": "catalog:",
"@moeru/std": "catalog:",
"@opentelemetry/api": "catalog:",
"@opentelemetry/api-logs": "catalog:",
"@opentelemetry/exporter-logs-otlp-proto": "catalog:",
"@opentelemetry/exporter-metrics-otlp-proto": "catalog:",
"@opentelemetry/exporter-trace-otlp-proto": "catalog:",
"@opentelemetry/instrumentation": "catalog:",
"@opentelemetry/instrumentation-http": "catalog:",
"@opentelemetry/instrumentation-ioredis": "catalog:",
"@opentelemetry/instrumentation-pg": "catalog:",
"@opentelemetry/instrumentation-runtime-node": "catalog:",
"@opentelemetry/instrumentation-undici": "catalog:",
"@opentelemetry/resources": "catalog:",
"@opentelemetry/sdk-logs": "catalog:",
"@opentelemetry/sdk-metrics": "catalog:",
"@opentelemetry/sdk-node": "catalog:",
"@opentelemetry/sdk-trace-node": "catalog:",
"@opentelemetry/semantic-conventions": "catalog:",
"@proj-airi/drizzle-orm-browser-migrator": "catalog:",
"@proj-airi/server-schema": "workspace:*",
"@proj-airi/server-sdk-shared": "workspace:*",
"better-auth": "catalog:",
"cac": "catalog:",
"drizzle-orm": "catalog:",
"drizzle-valibot": "catalog:",
"es-toolkit": "catalog:",
"hono": "catalog:",
"hono-rate-limiter": "catalog:",
"injeca": "catalog:",
"ioredis": "catalog:",
"jose": "catalog:",
"nanoid": "catalog:",
"ofetch": "catalog:",
"pg": "catalog:",
"posthog-node": "catalog:",
"resend": "catalog:",
"stripe": "catalog:",
"unspeech": "catalog:xsai",
"valibot": "catalog:",
"ws": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@better-auth/cli": "catalog:",
"@types/pg": "catalog:",
"@types/ws": "catalog:",
"drizzle-kit": "catalog:"
}
}