Observability
Observability
What it does
Three independent, individually-optional pieces:
- Error tracking — Sentry, wired into both server and browser, plus Next's own server-error hook and the root error boundary.
- Health check —
GET /api/health, unauthenticated, for uptime monitors/load balancers/orchestrators. - Privacy-friendly analytics — a single
<script>tag for Plausible or Umami, gated on cookie consent (see content.md).
All three degrade to "off, no crash" when unconfigured — the same contract every optional integration in this starter follows (Resend, Turnstile, S3, Google OAuth, Stripe).
How to configure it
| Var | Purpose |
|---|---|
NEXT_PUBLIC_SENTRY_DSN | Your Sentry project's DSN. Unset → Sentry is never initialized, client or server. |
SENTRY_ORG / SENTRY_PROJECT / SENTRY_AUTH_TOKEN | Build/CI-only — lets the Sentry Next.js build plugin upload source maps for readable stack traces. Not read at runtime; the app works without them, you just get minified traces in Sentry. |
NEXT_PUBLIC_ANALYTICS_PROVIDER | "plausible" or "umami". Unset → no analytics script, no cookie banner at all. |
NEXT_PUBLIC_ANALYTICS_ID | Plausible's domain, or Umami's website ID. |
/api/health needs no configuration — it's always on.
How it works
instrumentation.ts— Next's server/edge instrumentation hook (node_modules/next/dist/docs/01-app/03-api-reference/03-file-conventions/instrumentation.md— a file-convention export,register(), run once at server boot). Readsprocess.env.NEXT_PUBLIC_SENTRY_DSNdirectly rather than through@/lib/env— this file runs before Next's module graph is fully set up, andlib/env.tscarries aserver-onlyimport not meant for this dual server/edge context. No-ops entirely when the DSN is unset. Also exportsonRequestError = Sentry.captureRequestError, which wires Next's own server-error hook straight into Sentry — this is what catches Server Component render errors, Route Handler errors, and Server Action errors, none of which reachapp/global-error.tsx's boundary (that one only catches errors that escape all the way to the root layout).instrumentation-client.ts— the browser-side counterpart, also a Next file convention (instrumentation-client.md), executed before hydration.NEXT_PUBLIC_*is inlined into the client bundle regardless ofserver-only, same pattern aslib/auth-client.ts.app/global-error.tsx— root-layout-level error boundary; replaces the entire tree (including<html>/<body>) when an error escapes evenapp/layout.tsx, so it defines its own minimal markup rather than reusingRootLayout's. CallsSentry.captureException(error)in auseEffect(a no-op if Sentry was never initialized). Its retry prop is namedunstable_retry, notreset— a breaking rename in this Next version from what you may know as the stable API (seeAGENTS.md).app/api/health/route.ts— no auth, no rate limiting, deliberately: it's meant to be hit by infrastructure that doesn't carry a session or API key. Runsselect 1against the DB and returns{status, version, uptimeSeconds, checks: {database}}; 200 when the DB check passes, 503 when it fails — load balancers/orchestrators treat any 2xx as "route traffic here," which a DB-less instance shouldn't get.uptimeSecondsis measured from a module-levelstartedAttimestamp (set once at boot), not per-request.components/marketing/analytics-script.tsx— a Server Component that renders nothing unless bothNEXT_PUBLIC_ANALYTICS_PROVIDERandNEXT_PUBLIC_ANALYTICS_IDare set and the visitor'sconsentcookie (written bycomponents/marketing/consent-banner.tsx) reads"granted". Emits the one<script>tag each provider's own docs ask for — no event tracking helpers, no pageview wiring beyond what each provider's script does automatically.
How to extend it
- Custom Sentry options (release tracking,
beforeSendscrubbing, session replay) — add them to theSentry.init()calls in bothinstrumentation.tsandinstrumentation-client.ts(they're separate calls; keep them in sync for shared options liketracesSampleRate). - A deeper health check (Redis, S3, an upstream API) — add another
checks.*entry inapp/api/health/route.tsalongside the DB check; decide whether that dependency failing should also flipstatusto"degraded"/503, or just report itself while the endpoint stays healthy. - A third analytics provider — add a branch in
analytics-script.tsxand extendNEXT_PUBLIC_ANALYTICS_PROVIDER's enum inlib/env.ts. - Custom event tracking (not just pageviews) — both Plausible and Umami
expose a
window.plausible()/window.umami.track()global once their script loads; call it from client components where you need it. Nothing in this starter wraps that today.
How to remove it
- Sentry: delete
instrumentation.ts,instrumentation-client.ts, theSentry.captureExceptioncall and import inapp/global-error.tsx,@sentry/nextjsfrompackage.json, and thewithSentryConfigwrapper innext.config.ts. - Health check: delete
app/api/health/. - Analytics: delete
components/marketing/analytics-script.tsx,components/marketing/consent-banner.tsx, its mount inapp/layout.tsx, and the twoNEXT_PUBLIC_ANALYTICS_*vars fromlib/env.ts— see content.md for the cookie-consent side of this.