Monitoring & Error Tracking in Next.js Production
Monitoring & Error Tracking in Next.js Production
LinkedIn Hook
"Your app is on fire and you don't even know it."
Here's the uncomfortable truth about production: for every one user who reports a bug, there are a hundred who silently rage-quit. They hit an error, curse under their breath, close the tab, and never come back. You lose the customer and you never even learn what broke.
Most Next.js developers ship to Vercel, see a green checkmark, and call it done. No error boundaries. No Sentry. No structured logs. No Core Web Vitals tracking. When something crashes at 3am on a Tuesday, their only clue is a vague support ticket saying "the button doesn't work."
Next.js 13+ gives you everything you need to build a production observability stack:
error.jsandglobal-error.jsfor route-level recovery,useReportWebVitalsfor performance telemetry, OpenTelemetry for distributed tracing, and first-class Sentry integration across client, server, and edge runtimes.In Lesson 8.4, I break down how to instrument a Next.js app so nothing ever crashes silently again — and what you should absolutely never log (hint: PII will get you fined).
Read the full lesson -> [link]
#NextJS #Observability #Sentry #CoreWebVitals #ErrorTracking #InterviewPrep
What You'll Learn
- How
error.jsandglobal-error.jscreate route-level error boundaries - How to integrate Sentry across client, server, and edge runtimes
- How to track Core Web Vitals with
useReportWebVitals - How to emit structured server logs that are searchable and aggregatable
- How to upload source maps so Sentry stack traces are readable
- How to enable OpenTelemetry tracing for distributed systems
- What you should log — and more importantly, what you must never log (PII, secrets, tokens)
The Smoke Detector Analogy — Why Observability Matters
Imagine you own a house. You install locks on the doors, fire-resistant walls, and a sprinkler system. Your house is "safe" by any reasonable standard. Then, one night, an electrical short starts a small fire behind a wall. No one is awake. No one sees it. By the time you smell smoke, half the house is gone.
A smoke detector would have caught it in 30 seconds. One cheap device, screaming the moment something goes wrong, saves the entire house. It doesn't prevent fires — it detects them immediately so you can act before the damage spreads.
Production monitoring is that smoke detector. Your code will fail. Databases will time out. Third-party APIs will return garbage. Users will click buttons in orders you never imagined. You cannot prevent every failure, but you can detect every failure and react to it before it costs you customers.
Next.js gives you four smoke detectors baked in: error.js for route crashes, Sentry for exception aggregation, useReportWebVitals for performance regressions, and OpenTelemetry for distributed traces. Wire them up once and your app will tell you exactly what's broken, where, and for whom — in real time.
+---------------------------------------------------------------+
| THE OBSERVABILITY STACK (What Each Layer Does) |
+---------------------------------------------------------------+
| |
| LAYER 1: ROUTE BOUNDARIES (error.js, global-error.js) |
| Catches React render errors per route segment. |
| Shows fallback UI. Offers retry. |
| |
| LAYER 2: EXCEPTION AGGREGATION (Sentry) |
| Captures uncaught errors across client/server/edge. |
| Groups by fingerprint. Alerts on spikes. |
| |
| LAYER 3: STRUCTURED LOGS (pino, winston, console.log JSON) |
| Timestamped, tagged, queryable log stream. |
| Shipped to Datadog, CloudWatch, Grafana Loki. |
| |
| LAYER 4: PERFORMANCE METRICS (useReportWebVitals) |
| LCP, INP, CLS, TTFB, FCP sent to analytics. |
| Alerts when p75 regresses. |
| |
| LAYER 5: DISTRIBUTED TRACING (OpenTelemetry) |
| End-to-end spans across services. |
| Find slow dependencies instantly. |
| |
+---------------------------------------------------------------+
Error Boundaries — error.js and global-error.js
Next.js App Router has two special files that turn any route segment into a React error boundary. You don't write <ErrorBoundary> components manually — you drop a file into the directory, and Next.js wires it up automatically.
Route-Level Error Recovery with error.tsx
An error.tsx file catches render-time errors in its sibling page.tsx and any nested layouts below it. Everything above the boundary (parent layouts, the root) keeps rendering normally — only the broken segment is replaced with a fallback.
// app/dashboard/error.tsx
'use client';
// Error boundaries MUST be Client Components — they use React state
// and need to register themselves as boundaries at runtime.
import { useEffect } from 'react';
export default function DashboardError({
error,
reset,
}: {
// The error thrown during rendering of the segment below this file
error: Error & { digest?: string };
// Call reset() to re-render the segment and attempt recovery
reset: () => void;
}) {
useEffect(() => {
// Report the error to your monitoring service
// The digest is a server-generated hash you can cross-reference in logs
console.error('Dashboard route error:', error);
// In real code, forward to Sentry, Datadog, etc.
// Sentry.captureException(error, { tags: { route: 'dashboard' } });
}, [error]);
return (
<div className="error-container">
<h2>Something went wrong loading your dashboard.</h2>
<p>We've been notified and are looking into it.</p>
{/* reset() retries rendering the segment without a full page reload */}
<button onClick={() => reset()}>Try again</button>
</div>
);
}
Key behavior:
- The error boundary only catches errors below it —
app/dashboard/error.tsxcatches errors inapp/dashboard/page.tsxand children, but not inapp/dashboard/layout.tsx. To catch a layout error, you need anerror.tsxone level up. - On the server, Next.js strips the original error message in production and replaces it with a generic "An error occurred" message. The
error.digestproperty is a hash you can match against server logs to find the real error. reset()re-renders the boundary's children. If the cause was transient (a flaky API), the retry may succeed.
Catching Root Layout Errors with global-error.tsx
error.tsx cannot catch errors in the root app/layout.tsx — because if the root layout fails, there is no surviving parent to render a fallback. For that, you need global-error.tsx.
// app/global-error.tsx
'use client';
import { useEffect } from 'react';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// This runs when even the root layout has crashed.
// At this point, your only priority is reporting the failure.
console.error('Global error (root layout crashed):', error);
}, [error]);
// global-error.tsx MUST render its own <html> and <body> tags,
// because the root layout is broken and not rendering them.
return (
<html lang="en">
<body>
<div style={{ padding: 40, fontFamily: 'system-ui' }}>
<h1>Application error</h1>
<p>A critical error has occurred. Please reload the page.</p>
<button onClick={() => reset()}>Reload</button>
</div>
</body>
</html>
);
}
Important: global-error.tsx only runs in production. In development, Next.js shows its own detailed error overlay instead, so you can actually debug what broke.
+---------------------------------------------------------------+
| ERROR BOUNDARY HIERARCHY |
+---------------------------------------------------------------+
| |
| app/ |
| |-- global-error.tsx <-- catches root layout failures |
| |-- layout.tsx (root layout) |
| |-- error.tsx <-- catches app/page.tsx errors |
| |-- page.tsx |
| |-- dashboard/ |
| |-- error.tsx <-- catches dashboard/page errors |
| |-- layout.tsx (dashboard layout) |
| |-- page.tsx |
| |-- settings/ |
| |-- error.tsx <-- catches settings/page errors |
| |-- page.tsx |
| |
| Errors bubble UP to the nearest error.tsx above them. |
| If none exists, they keep bubbling until global-error.tsx. |
| |
+---------------------------------------------------------------+
Sentry Integration — Client, Server, and Edge
Sentry is the de facto standard for error aggregation. It captures exceptions, groups them by fingerprint, tracks release versions, and alerts you when error rates spike. Next.js has an official @sentry/nextjs package that wires up all three runtimes in one shot.
Installation and Configuration Wizard
# Run the official wizard — it creates config files and uploads source maps
npx @sentry/wizard@latest -i nextjs
The wizard generates three config files, one for each runtime environment. You need separate configs because client code, Node.js server code, and edge runtime code each need their own Sentry SDK initialization.
Client Configuration
// sentry.client.config.ts
// Runs in the browser for all client-side code.
import * as Sentry from '@sentry/nextjs';
Sentry.init({
// DSN identifies your Sentry project — safe to expose on the client
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Sample 10% of transactions for performance monitoring in prod
// (100% in dev is fine; 100% in prod is expensive)
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// Record user sessions when errors happen (last 30 seconds before crash)
replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0.0,
// Redact sensitive text and block all media from session replays
integrations: [
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
// Only send errors from production builds
enabled: process.env.NODE_ENV === 'production',
});
Server Configuration
// sentry.server.config.ts
// Runs in the Node.js server runtime (Server Components, Route Handlers,
// Server Actions, API routes).
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
// Scrub request bodies and headers that might contain PII
beforeSend(event) {
if (event.request?.cookies) {
delete event.request.cookies;
}
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['cookie'];
}
return event;
},
});
Edge Configuration
// sentry.edge.config.ts
// Runs in the Edge Runtime (middleware and edge route handlers).
// The Edge Runtime is a limited subset of Node — Sentry uses a lighter SDK.
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
});
Capturing Errors Manually
Uncaught exceptions are captured automatically. For caught errors you still want reported, use captureException:
// app/api/checkout/route.ts
import * as Sentry from '@sentry/nextjs';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
const body = await request.json();
// ... process checkout
return NextResponse.json({ ok: true });
} catch (error) {
// Send to Sentry with extra context so you can debug the failure
Sentry.captureException(error, {
tags: { route: 'checkout', method: 'POST' },
// NEVER put raw request body here — it may contain card numbers
extra: { stage: 'payment-intent-creation' },
});
return NextResponse.json({ error: 'Checkout failed' }, { status: 500 });
}
}
Source Maps — Making Stack Traces Readable
Minified production JavaScript looks like a.b.c(d) in stack traces — useless for debugging. Source maps translate that back into your original TypeScript with line numbers. The Sentry wizard configures automatic source map upload during next build via the withSentryConfig wrapper in next.config.js:
// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');
const nextConfig = {
// ... your Next.js config
};
// Wrap the config so source maps upload to Sentry on every build
module.exports = withSentryConfig(nextConfig, {
org: 'your-org',
project: 'your-project',
// Upload source maps but strip them from the public bundle
// so they are NEVER served to end users (protects your source code)
hideSourceMaps: true,
// Auth token with project:releases scope
authToken: process.env.SENTRY_AUTH_TOKEN,
});
Critical: hideSourceMaps: true prevents your .map files from being served publicly. Without it, anyone can fetch /_next/static/chunks/page.js.map and read your full source code. Sentry still receives the maps during build, so stack traces remain readable in the dashboard — but they're stripped from the shipped bundle.
Core Web Vitals Tracking with useReportWebVitals
Errors tell you what broke. Web Vitals tell you what's slow. Google penalizes slow sites in search rankings, and slow sites have measurably worse conversion rates. Next.js gives you a hook — useReportWebVitals — that fires every time the browser measures a vital.
// app/components/web-vitals.tsx
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
useReportWebVitals((metric) => {
// metric = { id, name, value, rating, delta, navigationType }
// name is one of: FCP, LCP, CLS, INP, TTFB, FID
// rating is 'good' | 'needs-improvement' | 'poor'
// Option 1: Send to your own analytics endpoint
const body = JSON.stringify(metric);
const url = '/api/vitals';
// Use sendBeacon so the request survives page unload
if (navigator.sendBeacon) {
navigator.sendBeacon(url, body);
} else {
fetch(url, { body, method: 'POST', keepalive: true });
}
// Option 2: Send to Google Analytics
// window.gtag?.('event', metric.name, {
// value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
// metric_id: metric.id,
// metric_value: metric.value,
// metric_delta: metric.delta,
// non_interaction: true,
// });
});
// This component renders nothing — it only wires up the hook
return null;
}
Then mount it once in the root layout:
// app/layout.tsx
import { WebVitals } from './components/web-vitals';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<WebVitals />
{children}
</body>
</html>
);
}
What Each Metric Means
+---------------------------------------------------------------+
| CORE WEB VITALS CHEAT SHEET |
+---------------------------------------------------------------+
| |
| LCP (Largest Contentful Paint) |
| When the biggest element appears. Target: < 2.5s |
| Fix by: optimizing images, server response time |
| |
| INP (Interaction to Next Paint) [replaces FID in 2024] |
| Responsiveness to user input. Target: < 200ms |
| Fix by: breaking up long JS tasks, less hydration |
| |
| CLS (Cumulative Layout Shift) |
| Visual stability. Target: < 0.1 |
| Fix by: reserving space for images, fonts, ads |
| |
| FCP (First Contentful Paint) |
| When ANY content appears. Target: < 1.8s |
| |
| TTFB (Time To First Byte) |
| Server response speed. Target: < 800ms |
| |
+---------------------------------------------------------------+
Structured Logging — Searchable Server Logs
console.log('user logged in') is worthless at 3am when you're scrolling through 50,000 lines of CloudWatch trying to find one event. Structured logs solve this: every log line is a JSON object with consistent fields (timestamp, level, event, userId, requestId) that your log aggregator can index and query.
// lib/logger.ts
// Minimal structured logger — in production use pino or winston
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
interface LogContext {
event: string;
userId?: string; // OK: opaque internal ID (not email/name)
requestId?: string; // OK: correlation ID
durationMs?: number; // OK: numeric metric
[key: string]: unknown;
}
function log(level: LogLevel, context: LogContext) {
// One line of JSON per event — easy to parse, grep, and aggregate
console.log(
JSON.stringify({
timestamp: new Date().toISOString(),
level,
...context,
})
);
}
export const logger = {
info: (ctx: LogContext) => log('info', ctx),
warn: (ctx: LogContext) => log('warn', ctx),
error: (ctx: LogContext) => log('error', ctx),
debug: (ctx: LogContext) => log('debug', ctx),
};
Usage in a server action:
// app/actions/create-order.ts
'use server';
import { logger } from '@/lib/logger';
export async function createOrder(formData: FormData) {
const start = Date.now();
const requestId = crypto.randomUUID();
try {
const orderId = await saveOrderToDatabase(formData);
logger.info({
event: 'order.created',
requestId,
orderId,
durationMs: Date.now() - start,
});
return { ok: true, orderId };
} catch (error) {
logger.error({
event: 'order.failed',
requestId,
durationMs: Date.now() - start,
error: error instanceof Error ? error.message : String(error),
// NOTE: no form fields, no user email, no payment details
});
throw error;
}
}
Now a query like level:error event:order.failed returns every failed order across your entire fleet in seconds.
OpenTelemetry Support in Next.js
For distributed systems where one request touches multiple services (Next.js -> API -> database -> third-party), you need distributed tracing: a single trace ID that follows the request end-to-end so you can find the slow span. Next.js has first-class OpenTelemetry support via the @vercel/otel package.
// instrumentation.ts (in project root, next to next.config.js)
// This file runs exactly once when the Next.js server starts.
export async function register() {
// Only register OTel in Node.js runtime — not edge
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { registerOTel } = await import('@vercel/otel');
registerOTel({ serviceName: 'my-next-app' });
}
}
Next.js automatically emits spans for every Server Component render, route handler, fetch call, and metadata resolution. You get a full waterfall of where time was spent, with zero extra instrumentation code, exportable to Honeycomb, Datadog, Jaeger, or any OTLP-compatible backend.
What to Log — and What NEVER to Log
This is the section that will save you from a GDPR fine or a lawsuit. The golden rule: logs are not a secret store. Assume every log line will eventually be read by an intern, leaked in a breach, or indexed by a third-party vendor.
+---------------------------------------------------------------+
| LOGGING ALLOWLIST / DENYLIST |
+---------------------------------------------------------------+
| |
| ALLOW (safe to log): |
| + Timestamps and durations |
| + Opaque internal user IDs (uuid, not email) |
| + Request IDs / trace IDs |
| + HTTP status codes, routes, methods |
| + Error types and stack traces (in Sentry) |
| + Feature flag names and variants |
| + Event names (e.g. 'order.created') |
| |
| DENY (NEVER log): |
| - Passwords, even hashed |
| - API keys, tokens, session cookies |
| - Credit card numbers, CVV, PAN |
| - Full names, emails, phone numbers (PII) |
| - IP addresses in some jurisdictions (GDPR) |
| - Health data (HIPAA), biometric data |
| - Authorization headers |
| - Raw request/response bodies with user data |
| - Database connection strings |
| |
+---------------------------------------------------------------+
If a bug requires looking at user data to reproduce, use a session replay tool (Sentry Replays, LogRocket, FullStory) with DOM masking enabled — not raw logs. Replays are encrypted, retention-limited, and access-controlled. Logs are plain text on disk.
Common Mistakes
1. Treating error.tsx like a try/catch for every error.
error.tsx only catches render-time errors in Client and Server Components. It does not catch errors thrown inside event handlers (onClick) — those need a manual try/catch. It also doesn't catch errors in Server Actions unless they bubble up during re-render. If you expect an error boundary to catch form submission failures, you'll be surprised.
2. Forgetting that error.tsx must be a Client Component.
Error boundaries in React require class components with componentDidCatch, which React handles internally via the 'use client' directive. Server-only error.tsx files silently do nothing. Always start the file with 'use client'.
3. Shipping source maps to the public bundle.
Without hideSourceMaps: true, your .map files are fetchable at /_next/static/.../page.js.map, meaning anyone can reconstruct your original source code — including comments, TODOs, and hardcoded secrets. Always configure Sentry to upload maps during build and strip them from the served output.
4. Logging PII or request bodies "temporarily for debugging".
Temporary logs become permanent. One console.log(req.body) on a signup endpoint and you now have passwords sitting in CloudWatch for 30 days. Use Sentry's beforeSend to scrub sensitive fields, and audit your logger for any raw object dumps before going to production.
5. Capturing 100% of transactions in Sentry performance monitoring.
tracesSampleRate: 1.0 in production on a busy app will blow through your Sentry quota in hours and slow down every request with extra instrumentation overhead. Use 0.1 (10%) for production, 1.0 only in development. Sentry's statistical aggregates remain accurate even at 10% sampling.
Interview Questions
1. "What's the difference between error.js and global-error.js in Next.js App Router? When does each one fire?"
error.js (or error.tsx) is a route-segment error boundary. It catches render-time errors in its sibling page.tsx and any nested layouts or pages beneath it, but not in its sibling layout.tsx — that layout is above the boundary. Errors bubble up to the nearest error.tsx above the failing component. global-error.js is a special file at the root of the app/ directory that catches errors in the root layout itself. It's the last line of defense: if app/layout.tsx crashes, there's no surviving parent to render a fallback, so global-error.js takes over and renders its own <html> and <body> tags. Both files must be Client Components ('use client'), both receive error and reset props, and global-error.js only runs in production builds.
2. "How does Sentry integrate with Next.js across the three different runtimes?"
Next.js code runs in three environments: the browser (Client Components), Node.js (Server Components, Route Handlers, Server Actions), and the Edge Runtime (middleware, edge route handlers). Each needs its own Sentry SDK initialization because they have different APIs and constraints. The @sentry/nextjs package expects three config files: sentry.client.config.ts for the browser, sentry.server.config.ts for Node.js, and sentry.edge.config.ts for the edge runtime (which uses a lighter SDK without Node APIs). You then wrap next.config.js with withSentryConfig to automatically upload source maps during build and inject Sentry into your webpack output. Uncaught exceptions in any runtime are captured automatically; caught exceptions can be manually reported with Sentry.captureException(error, { tags, extra }).
3. "How do you track Core Web Vitals in a Next.js app and what should you do with the data?"
Next.js exposes a useReportWebVitals hook from next/web-vitals that fires a callback every time the browser measures a metric (LCP, INP, CLS, FCP, TTFB). You put it in a Client Component mounted once at the root layout. The callback receives a metric object with name, value, rating, and id. You forward that data to an analytics endpoint — either your own /api/vitals route, Google Analytics via gtag, or a SaaS like Vercel Analytics or Datadog RUM. Use navigator.sendBeacon instead of fetch because it survives page unload, so you don't lose metrics when the user navigates away. Once data is flowing, set alerts on the p75 of each metric — if LCP p75 crosses 2.5 seconds, Google considers your page "needs improvement" and you need to investigate regressions immediately.
4. "Why are source maps important for production error tracking, and why should you hide them from the public bundle?"
Production JavaScript is minified and bundled — variable names become single letters, code is inlined, and whitespace is stripped. A raw production stack trace looks like at a.b (chunk-abc.js:1:45321), which is completely useless for debugging. Source maps are separate files that translate minified code back to your original source lines, function names, and file paths. Sentry uses source maps to show stack traces in your original TypeScript, making bugs actionable. However, if you ship source maps in the public bundle, anyone can download them from /_next/static/.../page.js.map and reconstruct your full source code — including comments, internal logic, and any hardcoded keys. The withSentryConfig wrapper's hideSourceMaps: true option solves this by uploading maps to Sentry during the build (so Sentry can still resolve stack traces on its end) while stripping them from the files served to end users. You get debuggable errors in the dashboard without leaking source code.
5. "What kinds of data should you avoid putting into server logs in a Next.js app, and why?"
Logs are not a secret store. They get written to disk, shipped to third-party aggregators like Datadog or CloudWatch, retained for weeks or months, and are often accessible to a large number of engineers and contractors. So you must treat any log line as if it will eventually leak. That means: never log passwords (even hashed), API keys, session tokens, authorization headers, or full cookies. Never log credit card numbers, CVVs, or any PCI data — it can revoke your payment processing ability. Never log full names, emails, phone numbers, or health data without a legal review, because GDPR and HIPAA impose strict controls on PII. Never dump raw request or response bodies, because they usually contain the above by accident. Instead, log opaque internal IDs (UUIDs), request IDs, timestamps, durations, status codes, and event names. If debugging requires user data, use a session replay tool with masking enabled instead of raw logs, because those systems have built-in access control, retention limits, and encryption.
Quick Reference — Monitoring Cheat Sheet
+---------------------------------------------------------------+
| NEXT.JS MONITORING CHEAT SHEET |
+---------------------------------------------------------------+
| |
| ROUTE ERROR BOUNDARY: |
| app/<segment>/error.tsx ('use client' required) |
| props: { error, reset } |
| catches: render errors in sibling page.tsx and children |
| |
| ROOT CRASH FALLBACK: |
| app/global-error.tsx |
| must render its own <html> and <body> |
| only runs in production |
| |
| SENTRY CONFIG FILES: |
| sentry.client.config.ts (browser) |
| sentry.server.config.ts (Node.js) |
| sentry.edge.config.ts (Edge Runtime) |
| next.config.js -> withSentryConfig(nextConfig, {...}) |
| |
| WEB VITALS: |
| import { useReportWebVitals } from 'next/web-vitals' |
| metrics: LCP, INP, CLS, FCP, TTFB |
| transport: navigator.sendBeacon |
| |
| OPENTELEMETRY: |
| instrumentation.ts at project root |
| registerOTel({ serviceName: '...' }) |
| |
+---------------------------------------------------------------+
+---------------------------------------------------------------+
| KEY RULES |
+---------------------------------------------------------------+
| |
| 1. error.tsx MUST be 'use client' |
| 2. Catch all 3 runtimes with Sentry (client/server/edge) |
| 3. hideSourceMaps: true — never ship .map files publicly |
| 4. tracesSampleRate: 0.1 in production, 1.0 in dev |
| 5. Log opaque IDs, never PII or secrets |
| 6. Use sendBeacon for web vitals transport |
| 7. Structured JSON logs, not free-text console.log |
| 8. Scrub request bodies in Sentry beforeSend |
| |
+---------------------------------------------------------------+
| Tool | What It Catches | Where It Runs |
|---|---|---|
error.tsx | React render errors in a route segment | Client (after hydration) |
global-error.tsx | Root layout crashes | Client (production only) |
| Sentry client | Uncaught browser exceptions, unhandled rejections | Browser |
| Sentry server | Server Component / Route Handler exceptions | Node.js |
| Sentry edge | Middleware / edge handler exceptions | Edge Runtime |
useReportWebVitals | LCP, INP, CLS, FCP, TTFB | Browser |
| Structured logs | Business events, request lifecycle | Node.js |
| OpenTelemetry | Distributed traces across services | Node.js |
Prev: Lesson 8.3 -- Deploying to Vercel and Self-Hosting Next: Lesson 8.5 -- Interview Questions Roundup
This is Lesson 8.4 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.