## 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
- Removed voice pack related imports and functionality from speech.vue and Stage.vue.
- Simplified speech input handling by eliminating voice pack parameters in speech store.
- Updated tests to reflect changes in voice pack handling and speech input resolution.
- Adjusted Airi card store to remove voice pack binding logic, focusing on speech configuration updates.
- Enhanced voice pack list item structure for clarity and maintainability.
- Introduced upstreamVoiceId field in voice pack schema and database.
- Updated voice pack service to handle upstreamVoiceId in CRUD operations.
- Modified API routes and tests to accommodate upstreamVoiceId.
- Enhanced UI components to include upstreamVoiceId in forms and displays.
- Adjusted speech processing logic to utilize upstreamVoiceId where applicable.
- Updated related tests to ensure proper functionality with new field.
- Implemented `buildBedrockSlice` function to handle multi-kilobyte Bedrock bearer tokens.
- Enhanced `createAdminRouterConfigService` to classify Bedrock and OpenAI-compatible LLM upstreams by baseURL.
- Added new interfaces for `AdminRouterBedrockSlice` and `AdminRouterOpenAICompatibleSlice`.
- Updated router config form to support Bedrock and OpenAI-compatible slices.
- Created tests for Bedrock and OpenAI-compatible slice compilation and behavior.
- Modified UI components to accommodate new slice types and improve user experience.
- Ensured proper normalization of API server URLs to HTTPS when necessary.
- Updated various components in the llm-router to include dark mode styles, ensuring better visibility and aesthetics in dark theme.
- Added tests for API URL helpers to validate sign-in URL construction and redirection logic.
- Improved the sign-in flow to handle trusted redirects for standalone admin origins.
- Enhanced CSS styles for dark mode, including adjustments to backgrounds, borders, and text colors for better contrast and readability.
- Refactored input components to maintain consistent styling in both light and dark modes.
Add a form-first LLM/TTS router config editor and a redacted admin config snapshot so operators can inspect configKV state before applying changes.
Preserve existing encrypted key entries when loaded slices are submitted without new plaintext keys.
Signed-off-by: RainbowBird <git@luoling.moe>
Commit-Message-Assisted-by: Claude (via Claude Code)
- Added `prompt: 'select_account'` for Google and GitHub providers to force account selection during OAuth linking.
- Introduced localized error messages for account linking issues in multiple languages.
- Implemented a function to resolve OAuth error codes to specific i18n keys for better user feedback.
- Updated account settings page to display relevant error messages based on OAuth linking status.
- Removed specific catalog references for '@unocss/core', 'es-toolkit', 'nanoid', and 'valibot' in pnpm-workspace.yaml and various service package.json files.
- Updated versions for '@vueuse/core', 'es-toolkit', 'nanoid', 'reka-ui', and 'valibot' to their latest compatible versions.
- Cleaned up unused catalog entries in pnpm-workspace.yaml.
Move Voice Pack creation and editing to dedicated admin routes, add catalog-backed free-text fields, and wire test audio generation through the public speech API.
Document the mock-API browser verification workflow so future local UI checks can avoid auth and tooling dead ends.
Signed-off-by: RainbowBird <git@luoling.moe>
Add PostHog product events for manual TTS stop clicks and conversation controls across Web, mobile, and Electron chat surfaces. Document the events in metrics ownership and cover the analytics API with Vitest.
Signed-off-by: RainbowBird <git@luoling.moe>
Commit-Message-Assisted-by: Codex
Match the mobile manual TTS stop control to the subtler Web chat button treatment by removing the bright filled circle and using neutral icon styling with primary hover feedback.
Signed-off-by: RainbowBird <git@luoling.moe>
Commit-Message-Assisted-by: Codex
Add the stop-speaking control to the mobile shared input layout and the Electron Tamagotchi chat input so Web/shared, mobile, and Electron all expose the same manual TTS interruption path.
Signed-off-by: RainbowBird <git@luoling.moe>
Commit-Message-Assisted-by: Codex
Track per-app TTS concurrency in Redis, route capped upstreams by
available pool capacity, and surface pool saturation metrics. Document
the Voice Pack plan so the remaining backend and card-binding work has
an explicit implementation map.
Upgrade @moeru/eventa to v1.0.0-beta.8 through the pnpm catalog and remove the local Hono adapter copy.
Keep AIRI on business-level Eventa usage while adapter contract tests live upstream in @moeru/eventa.
Signed-off-by: RainbowBird <git@luoling.moe>
Commit-Message-Assisted-by: Claude (via Claude Code)
Separate OpenAI-compatible route wiring from chat, speech, catalog, billing, and telemetry pipeline code.
Signed-off-by: RainbowBird <git@luoling.moe>
Commit-Message-Assisted-by: Claude (via Claude Code)