Trovella Wiki

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_DSN is 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 -- onRouterTransitionStart hooks into Next.js App Router navigation to create performance spans for client-side route changes.
  • NEXT_PUBLIC_SENTRY_DSN -- the client DSN uses the NEXT_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 .map files 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

VariableRuntimePurpose
SENTRY_DSNServer/EdgeServer-side Sentry DSN. Set in .env and GCP Secret Manager.
NEXT_PUBLIC_SENTRY_DSNClientClient-side DSN. Inlined at build time via NEXT_PUBLIC_ prefix.
SENTRY_AUTH_TOKENBuild onlyAuthentication 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

ScenarioUse
Unhandled exception crashes a requestSentry captures automatically via onRequestError
React component throws during renderSentry captures via global-error.tsx boundary
You catch an error but want Sentry to knowSentry.captureException(error)
Operational visibility into a handled errorlogger.error({ err }, "message") via Pino
Business event logging (user signed up, job completed)logger.info(...) via Pino
Performance profiling for a specific requestSentry 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.

On this page