Live demo — shared accounts, sample data, reset regularly. Don't enter anything real.Get the source

Observability

Observability

What it does

Three independent, individually-optional pieces:

  1. Error tracking — Sentry, wired into both server and browser, plus Next's own server-error hook and the root error boundary.
  2. Health check — GET /api/health, unauthenticated, for uptime monitors/load balancers/orchestrators.
  3. 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

VarPurpose
NEXT_PUBLIC_SENTRY_DSNYour Sentry project's DSN. Unset → Sentry is never initialized, client or server.
SENTRY_ORG / SENTRY_PROJECT / SENTRY_AUTH_TOKENBuild/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_IDPlausible'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). Reads process.env.NEXT_PUBLIC_SENTRY_DSN directly rather than through @/lib/env — this file runs before Next's module graph is fully set up, and lib/env.ts carries a server-only import not meant for this dual server/edge context. No-ops entirely when the DSN is unset. Also exports onRequestError = 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 reach app/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 of server-only, same pattern as lib/auth-client.ts.
  • app/global-error.tsx — root-layout-level error boundary; replaces the entire tree (including <html>/<body>) when an error escapes even app/layout.tsx, so it defines its own minimal markup rather than reusing RootLayout's. Calls Sentry.captureException(error) in a useEffect (a no-op if Sentry was never initialized). Its retry prop is named unstable_retry, not reset — a breaking rename in this Next version from what you may know as the stable API (see AGENTS.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. Runs select 1 against 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. uptimeSeconds is measured from a module-level startedAt timestamp (set once at boot), not per-request.
  • components/marketing/analytics-script.tsx — a Server Component that renders nothing unless both NEXT_PUBLIC_ANALYTICS_PROVIDER and NEXT_PUBLIC_ANALYTICS_ID are set and the visitor's consent cookie (written by components/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, beforeSend scrubbing, session replay) — add them to the Sentry.init() calls in both instrumentation.ts and instrumentation-client.ts (they're separate calls; keep them in sync for shared options like tracesSampleRate).
  • A deeper health check (Redis, S3, an upstream API) — add another checks.* entry in app/api/health/route.ts alongside the DB check; decide whether that dependency failing should also flip status to "degraded"/503, or just report itself while the endpoint stays healthy.
  • A third analytics provider — add a branch in analytics-script.tsx and extend NEXT_PUBLIC_ANALYTICS_PROVIDER's enum in lib/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, the Sentry.captureException call and import in app/global-error.tsx, @sentry/nextjs from package.json, and the withSentryConfig wrapper in next.config.ts.
  • Health check: delete app/api/health/.
  • Analytics: delete components/marketing/analytics-script.tsx, components/marketing/consent-banner.tsx, its mount in app/layout.tsx, and the two NEXT_PUBLIC_ANALYTICS_* vars from lib/env.ts — see content.md for the cookie-consent side of this.