airi/apps/server
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
..
docs feat(analytics): expand PostHog coverage and forward server business facts (#2038) 2026-07-08 15:33:39 +08:00
drizzle fix(server): harden provider catalog gating 2026-07-01 23:08:14 +08:00
otel feat(analytics): add official provider ops metrics 2026-07-01 21:19:42 +08:00
production/railway feat(admin-ui): implement standalone admin UI with Cloudflare Pages deployment 2026-06-09 17:09:06 +08:00
scripts chore: cleanup and lint fix 2026-06-07 20:31:22 +08:00
sql fix(server): docker-compose fix 2026-01-05 16:26:44 +08:00
src feat(analytics): expand PostHog coverage and forward server business facts (#2038) 2026-07-08 15:33:39 +08:00
tests/verifications fix(server): make analytics best-effort 2026-06-05 16:27:10 +08:00
.env chore(server): remove unused CLIENT_URL from env template (#1884) 2026-06-08 13:52:19 +08:00
CLAUDE.md refactor(server): split services into domain/adapter layers, drop dead code 2026-05-18 23:36:45 +08:00
docker-compose.otel.yml feat(server): replace legacy health endpoints with K8s-style /livez and /readyz probes 2026-05-18 23:32:20 +08:00
docker-compose.yml feat(server): replace legacy health endpoints with K8s-style /livez and /readyz probes 2026-05-18 23:32:20 +08:00
Dockerfile feat(admin-ui): implement standalone admin UI with Cloudflare Pages deployment 2026-06-09 17:09:06 +08:00
drizzle.config.ts fix(server): database with duplicated index 2026-01-05 16:27:10 +08:00
instrumentation.ts feat(server): langfuse 2026-05-30 19:35:37 +08:00
package.json feat(analytics): expand PostHog coverage and forward server business facts (#2038) 2026-07-08 15:33:39 +08:00
railway.toml feat(admin-ui): implement standalone admin UI with Cloudflare Pages deployment 2026-06-09 17:09:06 +08:00
README.md chore: remove admin dashboard 2026-07-02 02:22:16 +08:00
tsconfig.json feat(server/otel): restructure observability metrics and add active sessions gauge 2026-05-12 23:10:13 +08:00
vitest.config.ts feat(ui-server-auth): init server auth page 2026-04-03 01:26:38 +08:00

@proj-airi/server

HTTP and WebSocket backend for AIRI. This app owns auth, billing, chat synchronization, gateway forwarding, and server-side observability export.

What It Does

  • Serves the Hono-based API and WebSocket endpoints.
  • Uses Postgres as the source of truth for users, billing, and durable state.
  • Uses Redis for cache, KV, Pub/Sub, and Streams.
  • Forwards GenAI requests to the configured upstream gateway and records billing from usage.
  • Exports traces, metrics, and logs through OpenTelemetry.

How To Use It

Install dependencies from the repo root and run scoped commands:

pnpm -F @proj-airi/server typecheck
pnpm -F @proj-airi/server exec vitest run
pnpm -F @proj-airi/server build

For local observability infrastructure, use:

docker compose -f apps/server/docker-compose.otel.yml up -d

AUTH_UI_URL

apps/ui-server-auth is deployed separately from the server image. The API server still owns the historical /auth/* entrypoints and redirects them to AUTH_UI_URL.

Default:

AUTH_UI_URL=https://accounts.airi.build/ui

Set this when previewing or deploying auth UI to a different Cloudflare URL.

ADMIN_UI_URL

The admin UI is deployed from the standalone proj-airi repository. The API server still owns the historical /admin/* entrypoints and redirects them to ADMIN_UI_URL.

Default:

ADMIN_UI_URL=https://admin.airi.build

Set this when previewing or deploying admin UI to a different Cloudflare URL.

ADDITIONAL_TRUSTED_ORIGINS (LAN / Capacitor dev)

When the mobile dev server uses a non-localhost origin (for example https://10.x.x.x:5273 from cap copy ios / capacitor.config.json), set ADDITIONAL_TRUSTED_ORIGINS in apps/server/.env.local to a comma-separated list of exact origins (parsed and normalized at startup). Example:

ADDITIONAL_TRUSTED_ORIGINS=https://10.0.0.129:5273,https://198.18.0.1:5273

Restart the API server after changing this variable.