Error Reporting
Send server, browser, desktop and mobile errors to Sentry or PostHog from committed config - off by default, and off loads nothing.
config.observability wires error reporting into the Hono API, the browser, and the desktop and mobile apps if you ship them. It ships off, and off loads nothing: each surface checks its provider's credential before dynamically importing an SDK. This is capture, not tracing - for per-request timing logs see performance monitoring.
Turn it on
phc_…) plus your instance host.config.observability in packages/config/src/index.ts.observability: {
sentry: { dsn: "https://examplePublicKey@o0.ingest.sentry.io/0" },
posthog: { publicKey: "phc_yourkey", host: "https://us.i.posthog.com", llmContent: false }
},Presence is the switch - no enabled flag, no provider selector. A provider reports when its credential is non-empty, and both may run at once. Keys are committed rather than env because they authorize sending only; SENTRY_DSN and POSTHOG_API_KEY override them server-side, but posthog.host does not, so an env key alone never arms PostHog.
No key, no reporting - silently. Keep the values literal: the bundler drops the SDK branch only when it sees the value inline, so a computed credential ships the SDK to every visitor.
The SDK has to be in your project. The observability manifest option decides at generation time which SDKs you get, stripping the rest with their adapters. To add one, put it in the observability array in .generatesaas/manifest.json and run generatesaas update.
Choose a provider
| Sentry | PostHog | |
|---|---|---|
| API / browser SDK | @sentry/node, @sentry/browser | posthog-node, posthog-js |
| Desktop app | @sentry/electron | No Electron SDK exists |
| Mobile app | @sentry/react-native | posthog-react-native |
| Crashes outside a request | Reported | Not reported - no process handlers |
| Environment tag | development / production | None - use a separate PostHog project locally |
| LLM analytics | - | $ai_generation events |
A PostHog desktop build reports nothing - it falls back to a no-op reporter plus one startup warning. Pick Sentry if desktop or boot-time crashes matter.
On the phone
The mobile app reads the same config.observability, and EXPO_PUBLIC_SENTRY_DSN / EXPO_PUBLIC_POSTHOG_KEY override it per build so one project can report to its own Sentry or PostHog. PostHog still needs its config block, which is where host lives.
| What it does | |
|---|---|
| Starts | Once, from the root layout, behind the credential check - no credential loads no SDK |
| Reader's switch | /settings/privacy, two toggles, both on by default; turning one off stops that sink at once, not at the next launch |
| Account identifiers | Never sent - sendDefaultPii: false, and PostHog's identify is never called |
| Environment tag | development or production, from Metro's __DEV__ |
The phone gets only the provider you picked. The observability manifest option decides which React Native SDK, which half of apps/mobile/lib/observability.ts and which EXPO_PUBLIC_* key you receive - and the Sentry Expo config plugin ships only with Sentry. Adding a provider later means editing the manifest and running generatesaas update.
What gets captured
| Surface | Where it lives | Reports |
|---|---|---|
| API | packages/api/src/observability.ts, from app.onError | Unhandled request errors, with the session user |
| Browser startup | apps/web/instrumentation-client.ts | Arms window.onerror, rejections, breadcrumbs at boot |
| Error boundaries | apps/web/lib/observability.ts | Render errors React catches |
| Desktop main | apps/desktop/src/main/observability.ts | Main-process exceptions and Electron's crash reporter |
| Desktop renderer | apps/desktop/src/renderer/lib/observability.ts | Uncaught and React boundary errors, over Sentry's IPC channel |
| Mobile app | apps/mobile/lib/observability.ts, from the root layout | Native and JS crashes (Sentry); product analytics (PostHog) |
From your own code: await captureServerError(err, { userId }) (@repo/api/observability) on the server, captureClientError(err) (apps/web/lib/observability.ts) in the browser. Both no-op when off, and neither needs a flush.
404s and 403s are never reported. isReportableError (@repo/observability) drops anything with a status below 500 and always reports status-less errors. It sits inside captureClientError, so your own calls inherit it.
A visitor who declines cookies still has their errors reported, with their user id when signed in - capture never consults the consent store. Analytics is untouched. Gate it yourself by returning early from the browser wrapper.
LLM analytics (PostHog only)
With a PostHog key and AI enabled, every model call is recorded as a $ai_generation. Built-in and BYOK runs both report; BYOK sends tokens and latency but no cost, since that is the user's own provider bill.
| Property | Value |
|---|---|
$ai_trace_id | the run id credit settlements are keyed by |
$ai_model, $ai_provider | the model that ran, after any fallback |
$ai_input_tokens, $ai_output_tokens | the usage the provider reported |
$ai_input_cost_usd, $ai_output_cost_usd, $ai_total_cost_usd | LIVE rates credits are debited from, not estimates |
$ai_latency | wall-clock seconds around the call |
$ai_is_error, $ai_error | set when the call failed |
$ai_input, $ai_output_choices | prompt and completion, only when llmContent: true - files, tool calls and tool results become placeholders, and reasoning is dropped |
Prompts and completions stay put unless you opt in. llmContent is false until you set it to true, so nothing your users type reaches PostHog by default. Opting in sends their message text verbatim to a third party - check it against your privacy policy first. Every number is captured either way.
What this does not include
Capture only. Each gap is a deliberate opt-in:
- Automatic tracing - needs
init()before every other module loads. Move it toinstrument.mjs:node --import ./instrument.mjs .... - Source-map upload - traces arrive minified. Add the Sentry bundler plugin (or
posthog-cli) plusSENTRY_AUTH_TOKENin CI. - The framework SDK - this is vanilla
@sentry/browser+@sentry/node. Run the@sentry/nextjswizard, then dropinitBrowserReportingfrominstrumentation-client.ts. Replace the body oflib/observability.tsrather than deleting it (the boundaries import from it), keeping itsisReportableErrorline. - Boot crashes on
fullstack- the frontend loads@repo/apion the first request, so addinitServerReporting()to the existingregister()inapps/web/instrumentation.ts; never replace that file.separatearms at process start already.
Sentry adds up to 2 seconds to a 500 response - flush(2000) is awaited before the response goes out, because serverless freezes the function the instant it returns.
Performance Monitoring
Log every API request's method, path, status, duration, and cache outcome via a Hono timing middleware, gated by config.performanceMonitor.
Internationalization
Translate the app from one shared @repo/i18n message store consumed by next-intl, with locale-prefixed routing and an auto-generated translation pipeline.