Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
enabled: process.env.NODE_ENV === 'production',
});
}
```typescript
// lib/telemetry/server.ts
import * as Sentry from '@sentry/nextjs';
export function initializeServerTelemetry() {
Sentry.init({
dsn: process.env.OBSERVABILITY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1,
enabled: process.env.NODE_ENV === 'production',
});
}
// lib/telemetry/edge.ts
import * as Sentry from '@sentry/nextjs';
export function initializeEdgeTelemetry() {
Sentry.init({
dsn: process.env.OBSERVABILITY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1,
});
}
Architecture Rationale: Separating configurations prevents the client SDK from leaking into server bundles and vice versa. The enabled flag ensures development runs remain uncluttered while production captures telemetry. Replay integration is client-only and configured to mask sensitive DOM content by default.
2. Runtime Registration Hook
Next.js provides instrumentation.ts as the official entry point for runtime initialization. This file must reside at the project root and conditionally load the appropriate telemetry module based on the active runtime.
// instrumentation.ts
export async function register() {
const runtime = process.env.NEXT_RUNTIME;
if (runtime === 'nodejs') {
await import('./lib/telemetry/server');
} else if (runtime === 'edge') {
await import('./lib/telemetry/edge');
}
}
Why this works: The register function executes before request handling begins. Dynamic imports ensure tree-shaking removes unused SDK code from client bundles. The runtime check guarantees edge functions receive the lightweight edge-compatible SDK variant.
3. Error Boundary Instrumentation
The App Router provides two distinct error boundaries. error.tsx catches failures within route segments, while global-error.tsx handles root layout crashes. Both must forward exceptions to the telemetry layer.
// app/error.tsx
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
interface RouteErrorProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function RouteErrorBoundary({ error, reset }: RouteErrorProps) {
useEffect(() => {
Sentry.captureException(error, {
tags: { boundary: 'route-segment', digest: error.digest },
});
}, [error]);
return (
<section aria-live="polite">
<h2>Segment Load Failure</h2>
<p>Reference ID: {error.digest ?? 'unknown'}</p>
<button onClick={reset} type="button">
Retry Segment
</button>
</section>
);
}
// app/global-error.tsx
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
interface GlobalErrorProps {
error: Error;
reset: () => void;
}
export default function GlobalErrorBoundary({ error, reset }: GlobalErrorProps) {
useEffect(() => {
Sentry.captureException(error, { tags: { boundary: 'root-layout' } });
}, [error]);
return (
<html lang="en">
<body>
<main>
<h1>Application Initialization Failed</h1>
<button onClick={reset} type="button">
Reload Application
</button>
</main>
</body>
</html>
);
}
Key Insight: The error.digest property is a Next.js-generated hash that correlates client-side boundary failures with server-side logs. Attaching it as a tag enables cross-runtime trace linking in the observability dashboard.
4. Server Action Instrumentation
Server actions bypass automatic error capture. Explicit wrapping ensures mutations, database calls, and external API requests are traced.
// actions/document.ts
'use server';
import * as Sentry from '@sentry/nextjs';
import { db } from '@/lib/database';
export const submitDocument = Sentry.withServerActionInstrumentation(
'document.submit',
{ recordResponse: true, recordInput: true },
async (payload: FormData) => {
const title = payload.get('title') as string;
const content = payload.get('content') as string;
const result = await db.documents.create({
data: { title, content, status: 'pending' },
});
return { id: result.id, success: true };
}
);
Why explicit wrapping: Server actions execute in isolated server contexts. Without instrumentation, exceptions bubble up as generic 500 responses with no stack trace. The wrapper creates a dedicated span, captures input/output safely, and attaches the action name to traces for filtering.
5. Session Context Enrichment
Telemetry becomes actionable when tied to user identity. Context should be attached after authentication and cleared on session termination.
// lib/auth/session.ts
import * as Sentry from '@sentry/nextjs';
export function attachUserContext(user: { id: string; email: string; tier: string }) {
Sentry.setUser({ id: user.id, email: user.email });
Sentry.setTag('account_tier', user.tier);
Sentry.setContext('workspace', { seats: 12, region: 'us-east-1' });
}
export function clearUserContext() {
Sentry.setUser(null);
}
Production Note: Always clear context on logout. Failing to do so causes cross-session data leakage, violates privacy regulations, and corrupts user-level filtering in dashboards.
6. Build-Time Source Map Preservation
Minified production builds render stack traces unreadable. Source maps bridge the gap between deployed code and original source files.
# .env.local
OBSERVABILITY_AUTH_TOKEN=your-token
OBSERVABILITY_ORG=your-org-slug
OBSERVABILITY_PROJECT=your-project-slug
The withSentryConfig wrapper in next.config.ts automatically uploads source maps during next build. Ensure the auth token has project:releases and org:read scopes. Without this step, every production error resolves to at n (chunk-xyz.js:1:4523), forcing manual reproduction.
7. Dynamic Trace Sampling
Fixed sampling rates waste quota on low-value routes or miss critical failures. Implement context-aware sampling to balance visibility and cost.
// lib/telemetry/sampler.ts
import type { SamplingContext } from '@sentry/nextjs';
export function dynamicTraceSampler(ctx: SamplingContext): number {
if (ctx.parentSampled) return 1.0;
const url = ctx.location?.href ?? '';
const route = url.split('?')[0];
if (route.includes('/checkout')) return 0.6;
if (route.includes('/api/webhooks')) return 1.0;
if (route.includes('/health')) return 0.0;
return 0.05;
}
Pass this function to tracesSampler in your server/client configs. This approach guarantees full tracing for parent spans, prioritizes revenue-critical paths, ignores health checks, and defaults to a low baseline for general traffic.
Pitfall Guide
1. Unconditional SDK Initialization
Explanation: Calling Sentry.init() without environment checks causes development noise and can crash edge runtimes that lack Node.js APIs.
Fix: Always guard initialization with process.env.NODE_ENV === 'production' and verify runtime compatibility before importing.
2. Missing Root Layout Boundary
Explanation: error.tsx only catches route segment failures. Root layout crashes (e.g., provider initialization, global middleware errors) bypass it entirely.
Fix: Implement global-error.tsx with explicit <html> and <body> tags. It must be a client component and cannot use server-side features.
3. Source Map Omission in CI/CD
Explanation: Local builds upload maps, but CI/CD pipelines often skip environment variables, leaving production with minified traces.
Fix: Inject OBSERVABILITY_AUTH_TOKEN, OBSERVABILITY_ORG, and OBSERVABILITY_PROJECT into your deployment pipeline. Verify upload logs during next build.
4. Over-Sampling High-Volume Routes
Explanation: Setting tracesSampleRate: 1.0 globally exhausts quota quickly on API-heavy applications, triggering rate limits and dropped events.
Fix: Use tracesSampler with route-based logic. Exclude health checks, static assets, and internal pings. Cap production sampling at 5β10% for general traffic.
5. Stale User Context After Logout
Explanation: Failing to call Sentry.setUser(null) attaches the previous user's identity to subsequent anonymous requests, corrupting analytics and violating privacy standards.
Fix: Clear context immediately after session termination. Implement a middleware or auth hook that guarantees cleanup on every logout flow.
6. Incorrect Server Action Wrapper Placement
Explanation: Wrapping a server action outside the 'use server' directive or placing the wrapper in a client file causes build errors or silent failures.
Fix: Keep 'use server' at the top of the file. Apply Sentry.withServerActionInstrumentation directly to the exported async function. Never wrap the entire module.
7. Ignoring error.digest Correlation
Explanation: Client boundaries report errors without server context. Without the digest tag, tracing a client crash back to a server mutation requires manual log searching.
Fix: Always attach error.digest as a tag in error.tsx. Configure your observability dashboard to filter by this tag for instant cross-runtime trace linking.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small SaaS (<10k MAU) | Fixed 10% sampling, full source maps, basic user context | Low traffic allows generous sampling without quota exhaustion | Minimal; fits within free/low-tier plans |
| High-Traffic API (>100k req/min) | Dynamic sampling, exclude health/static routes, edge-only telemetry | Prevents rate limiting, focuses quota on business-critical mutations | Moderate; requires quota monitoring and alerting |
| Enterprise Compliance (GDPR/HIPAA) | Mask all replay text/media, clear context on logout, PII scrubbing rules | Ensures regulatory compliance while maintaining debuggability | Higher; may require dedicated Sentry tier and data retention policies |
| Multi-Tenant Platform | Attach tenant ID as context, route-based sampling per workspace | Enables per-tenant error isolation and billing correlation | Moderate; increases context payload size but improves support efficiency |
Configuration Template
// next.config.ts
import { withSentryConfig } from '@sentry/nextjs';
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactStrictMode: true,
experimental: {
serverActions: {
bodySizeLimit: '2mb',
},
},
};
export default withSentryConfig(nextConfig, {
org: process.env.OBSERVABILITY_ORG,
project: process.env.OBSERVABILITY_PROJECT,
authToken: process.env.OBSERVABILITY_AUTH_TOKEN,
silent: false,
widenClientFileUpload: true,
hideSourceMaps: true,
disableLogger: true,
});
# .env.production
OBSERVABILITY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
OBSERVABILITY_AUTH_TOKEN=your-token-with-project-releases-scope
OBSERVABILITY_ORG=your-org-slug
OBSERVABILITY_PROJECT=your-project-slug
NODE_ENV=production
Quick Start Guide
- Initialize the SDK: Execute
npx @sentry/wizard@latest -i nextjs and follow the prompts to generate config files and update next.config.ts.
- Configure Environment Variables: Add
OBSERVABILITY_DSN, OBSERVABILITY_AUTH_TOKEN, OBSERVABILITY_ORG, and OBSERVABILITY_PROJECT to your deployment environment.
- Verify Runtime Registration: Ensure
instrumentation.ts exists at the project root and dynamically imports server/edge modules based on process.env.NEXT_RUNTIME.
- Deploy and Validate: Push to production, trigger a test error, and confirm source maps resolve correctly in the observability dashboard. Check that
error.digest tags appear in client boundary events.