Observability
Error tracking, logging, and production error handling
Overview
@repo/observability wires up Sentry for error tracking and BetterStack Logtail for structured logging. Every unhandled error, catch block, and error boundary reports to Sentry. In production, logs go to BetterStack; in development, they go to the console.
Architecture
packages/observability/
├── keys.ts # env validation (Sentry DSN, BetterStack keys)
├── log.ts # Logtail (prod) / console (dev) logger
├── error.ts # parseError() — Sentry capture + logging
├── server-error.ts # handleServerError() — parseError for server contexts
├── instrumentation.ts # runtime dispatcher (Node.js / Edge)
├── server.ts # Sentry init for Node.js
├── edge.ts # Sentry init for Edge
├── client.ts # Sentry init for browser (+ session replays)
└── next-config.ts # withSentry(), withLogging() config wrappersWiring (apps/web)
Five files connect the web app to the observability package:
| File | Purpose |
|---|---|
instrumentation.ts | export const register = initializeSentry — server + edge init via Next.js register hook |
instrumentation-client.ts | initializeSentry() + initializeAnalytics() — browser init |
sentry.server.config.ts | initializeSentry() from server — Sentry webpack plugin entry |
sentry.edge.config.ts | initializeSentry() from edge — Sentry webpack plugin entry |
next.config.ts | withLogging() always, withSentry() on Vercel only |
Environment variables are pulled in through env.ts which extends @repo/observability/keys.
Error handling utilities
parseError(error) — universal error parser
Located in packages/observability/error.ts. Used everywhere.
import { parseError } from "@repo/observability/error";
try {
// ...
} catch (error) {
parseError(error); // → Sentry captureException + log.error
return NextResponse.json({ error: "..." }, { status: 500 });
}What it does:
- Extracts a message string from any error type (
Error, object with.message, or stringifies) - Calls
Sentry.captureException(error) - Calls
log.error()(Logtail in prod, console in dev) - Returns the message string
Use this in API routes, webhooks, and non-React server code.
handleServerError(error) — server catch handler
Located in packages/observability/server-error.ts. A thin wrapper around parseError for server-side catch blocks.
import { handleServerError } from "@repo/observability/server-error";
try {
// ...
} catch (error) {
handleServerError(error); // → parseError (Sentry + Logtail)
return [];
}What it does:
- Calls
parseError(error)— Sentry capture + structured logging - Returns the error message string
Use this in server actions, queries, server components, and any server-side catch blocks.
When to use which
| Context | Use | Why |
|---|---|---|
Server actions ("use server") | handleServerError | Server-only import, Sentry + logging |
| Query functions | handleServerError | Server-only import, Sentry + logging |
API routes (route.ts) | parseError | No server-only restriction |
| Webhooks | parseError | External entry point |
| Client components | captureException directly | Can't import server-only modules |
| Error boundaries | captureException in useEffect | Client-side |
Fire-and-forget .catch() | parseError | Inline promise catch |
handleError(error) — client-side toast
Located in packages/ui-web/lib/handle-error.ts. For client components that need to show error toasts.
import { handleError } from "@repo/ui-web/lib/handle-error";
try {
await someAction();
} catch (error) {
handleError(error); // → parseError + toast.error(message)
}Error boundaries
Three error boundaries catch unhandled errors and report to Sentry:
| File | Scope |
|---|---|
apps/web/app/global-error.tsx | Fatal app-level errors |
apps/web/app/[locale]/error.tsx | Route-level errors |
apps/admin/app/error.tsx | Admin app errors |
All follow the same pattern:
useEffect(() => {
captureException(error);
}, [error]);Action clients
@repo/lib/action-clients.ts uses parseError in handleServerError to report technical errors to Sentry before returning a translated user-facing message:
handleServerError: async (e) => {
if (isUserFacingError(e.message)) return e.message;
parseError(e); // → Sentry + Logtail
const t = await getTranslations({ locale, namespace: "global.errors" });
return t("unexpected");
},Sentry features
| Feature | Config |
|---|---|
| Error tracking | All runtimes (server, edge, client) |
| Session replays | Client only: 100% on error, 10% normal |
| Console capture | console.log/error/warn → Sentry logs |
| Tunnel route | /monitoring — bypasses ad-blockers |
| Source maps | Uploaded on CI builds (widenClientFileUpload) |
| Cron monitors | Automatic Vercel cron instrumentation |
Environment variables
| Variable | Required | Purpose |
|---|---|---|
NEXT_PUBLIC_SENTRY_DSN | Optional | Sentry data source name |
SENTRY_ORG | Optional | Sentry org (for source map uploads) |
SENTRY_PROJECT | Optional | Sentry project name |
All variables are optional. Without them, Sentry silently no-ops and logging falls back to console.
Graceful degradation
| Feature | Without env vars | With env vars |
|---|---|---|
| Error boundaries | Render error UI, no Sentry | Render UI + report to Sentry |
parseError / handleServerError | Logs to console | Sentry + Logtail |
| Session replays | Disabled | Active (client only) |
| Source map uploads | Skipped | Uploaded on CI |
| BetterStack status | Component returns null | Shows uptime badge |
Tunnel route (/monitoring) | No-op | Proxies Sentry requests |