Error Tracking
Sentry SDK configuration, instrumentation hooks, session replay, the /monitoring tunnel, and when to use Sentry vs Pino.
Trovella uses Sentry (@sentry/nextjs) for error tracking, performance monitoring, and session replay. Sentry captures errors that escape application handling -- unhandled exceptions, error boundary catches, and request failures. It is not a general-purpose logging tool.
SDK Configuration
Sentry is configured through three files in apps/web/, each targeting a different Next.js runtime.
Server Runtime (sentry.server.config.ts)
Loaded via instrumentation.ts when NEXT_RUNTIME === "nodejs":
import * as Sentry from "@sentry/nextjs";
const dsn = process.env["SENTRY_DSN"];
if (dsn) {
Sentry.init({
dsn,
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
enableLogs: true,
});
}
- Conditional initialization -- Sentry only activates when
SENTRY_DSNis set. In local development without a DSN, the SDK is a no-op. - 10% trace sampling in production -- keeps performance monitoring costs within the free tier (5,000 errors/month).
- 100% sampling in development -- full visibility during local debugging.
Edge Runtime (sentry.edge.config.ts)
Loaded via instrumentation.ts when NEXT_RUNTIME === "edge". Uses the same configuration as the server runtime. Edge functions (like middleware) run in a restricted V8 isolate, so the Sentry SDK automatically adjusts its transport.
Client Runtime (instrumentation-client.ts)
Loaded automatically by Next.js on the browser side:
import * as Sentry from "@sentry/nextjs";
const dsn = process.env["NEXT_PUBLIC_SENTRY_DSN"];
if (dsn) {
Sentry.init({
dsn,
integrations: [Sentry.replayIntegration()],
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
enableLogs: true,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
}
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
Client-specific features:
- Session replay --
replayIntegration()records user sessions as lightweight DOM snapshots. 10% of normal sessions are recorded; 100% of sessions where an error occurs are recorded. - Router transitions --
onRouterTransitionStarthooks into Next.js App Router navigation to create performance spans for client-side route changes. NEXT_PUBLIC_SENTRY_DSN-- the client DSN uses theNEXT_PUBLIC_prefix so Next.js inlines it into the client bundle at build time.
Instrumentation Hooks
The Next.js instrumentation file (apps/web/src/instrumentation.ts) serves two purposes:
export async function register() {
if (process.env["NEXT_RUNTIME"] === "nodejs") {
await import("../sentry.server.config");
// Also bootstraps Typesense collections (non-observability concern)
}
if (process.env["NEXT_RUNTIME"] === "edge") {
await import("../sentry.edge.config");
}
}
export const onRequestError = Sentry.captureRequestError;
The onRequestError export is key: Next.js calls this hook for any uncaught request error (server component render failures, route handler exceptions, middleware errors). This is how Sentry captures errors without explicit try/catch in every route.
Global Error Boundary
The global-error.tsx component is the top-level React error boundary. When a server or client component throws an unrecoverable error, React catches it here:
export default function GlobalError({ error, reset }) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
// Minimal error UI with "Try again" button
);
}
This ensures client-side rendering errors that bubble to the root are reported to Sentry even if onRequestError does not fire (which covers server-side errors).
The /monitoring Tunnel
Sentry error reports from the browser are routed through a Next.js tunnel at /monitoring instead of being sent directly to *.ingest.sentry.io:
// next.config.ts
export default withSentryConfig(withMDX(nextConfig), {
tunnelRoute: "/monitoring",
// ...
});
This avoids ad blockers and browser privacy extensions that block requests to Sentry's ingest domains. The tunnel is a first-party route that proxies error payloads to Sentry's backend. The CSP middleware in Application -- Routing & Pages allows connect-src 'self' https://*.ingest.sentry.io as a fallback.
Source Maps
Source maps are uploaded to Sentry during CI builds but hidden from clients:
// next.config.ts
{
hideSourceMaps: true,
widenClientFileUpload: true,
}
hideSourceMaps: true-- the.mapfiles are not served to browsers, preventing source code exposure.widenClientFileUpload: true-- uploads a broader set of source maps for better stack trace deobfuscation in the Sentry dashboard.
The SENTRY_AUTH_TOKEN secret is required at build time for source map uploads. It is passed as a Docker build arg in CI:
# .github/workflows/ci.yml
build-args: |
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
Environment Variables
| Variable | Runtime | Purpose |
|---|---|---|
SENTRY_DSN | Server/Edge | Server-side Sentry DSN. Set in .env and GCP Secret Manager. |
NEXT_PUBLIC_SENTRY_DSN | Client | Client-side DSN. Inlined at build time via NEXT_PUBLIC_ prefix. |
SENTRY_AUTH_TOKEN | Build only | Authentication token for source map uploads during CI. |
In local development, Sentry is active only when the DSN variables are set. Without them, all Sentry calls are no-ops.
When to Use Sentry vs Pino
| Scenario | Use |
|---|---|
| Unhandled exception crashes a request | Sentry captures automatically via onRequestError |
| React component throws during render | Sentry captures via global-error.tsx boundary |
| You catch an error but want Sentry to know | Sentry.captureException(error) |
| Operational visibility into a handled error | logger.error({ err }, "message") via Pino |
| Business event logging (user signed up, job completed) | logger.info(...) via Pino |
| Performance profiling for a specific request | Sentry traces (automatic with 10% sampling) |
The key rule: if an error will reach Sentry automatically, do not also log it with logger.error. Log when you are handling an error. Sentry captures when errors escape your handling.