Observability

Error tracking, logging, and production error handling

Overview

The application uses @repo/observability (following the next-forge pattern) to wire 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 wrappers

Wiring (apps/web)

Five files connect the web app to the observability package:

FilePurpose
instrumentation.tsexport const register = initializeSentry — server + edge init via Next.js register hook
instrumentation-client.tsinitializeSentry() + initializeAnalytics() — browser init
sentry.server.config.tsinitializeSentry() from server — Sentry webpack plugin entry
sentry.edge.config.tsinitializeSentry() from edge — Sentry webpack plugin entry
next.config.tswithLogging() 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:

  1. Extracts a message string from any error type (Error, object with .message, or stringifies)
  2. Calls Sentry.captureException(error)
  3. Calls log.error() (Logtail in prod, console in dev)
  4. 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:

  1. Calls parseError(error) — Sentry capture + structured logging
  2. Returns the error message string

Use this in server actions, queries, server components, and any server-side catch blocks.

When to use which

ContextUseWhy
Server actions ("use server")handleServerErrorServer-only import, Sentry + logging
Query functionshandleServerErrorServer-only import, Sentry + logging
API routes (route.ts)parseErrorNo server-only restriction
WebhooksparseErrorExternal entry point
Client componentscaptureException directlyCan't import server-only modules
Error boundariescaptureException in useEffectClient-side, follows next-forge pattern
Fire-and-forget .catch()parseErrorInline 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:

FileScope
app/global-error.tsxFatal app-level errors
app/[locale]/error.tsxRoute-level errors
app/(admin)/admin/error.tsxAdmin panel errors

All follow the same pattern:

useEffect(() => {
  captureException(error);
}, [error]);

Action clients

@repo/features/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

FeatureConfig
Error trackingAll runtimes (server, edge, client)
Session replaysClient only: 100% on error, 10% normal
Console captureconsole.log/error/warn → Sentry logs
Tunnel route/monitoring — bypasses ad-blockers
Source mapsUploaded on CI builds (widenClientFileUpload)
Cron monitorsAutomatic Vercel cron instrumentation

Environment variables

VariableRequiredPurpose
NEXT_PUBLIC_SENTRY_DSNOptionalSentry data source name
SENTRY_ORGOptionalSentry org (for source map uploads)
SENTRY_PROJECTOptionalSentry project name

All variables are optional. Without them, Sentry silently no-ops and logging falls back to console.

Graceful degradation

FeatureWithout env varsWith env vars
Error boundariesRender error UI, no SentryRender UI + report to Sentry
parseError / handleServerErrorLogs to consoleSentry + Logtail
Session replaysDisabledActive (client only)
Source map uploadsSkippedUploaded on CI
BetterStack statusComponent returns nullShows uptime badge
Tunnel route (/monitoring)No-opProxies Sentry requests