# What You’ll Build (and Why It Matters)#
This guide gives you an end-to-end observability setup for the Next.js App Router: errors, logs, and traces across Server Actions, Route Handlers, and the Edge versus Node.js runtime split. You’ll end with dashboards and alert thresholds that catch real incidents instead of spamming your team.
If you already have “some logging,” you still likely miss answers to the questions that matter during incidents: which deploy caused the spike, which route is slow, which user segment is impacted, and what exact downstream call is failing. The goal is to reduce mean time to detect and resolve by making issues actionable.
For broader observability concepts, read our baseline overview: Web app observability guide: logging, metrics, tracing.
# Observability Architecture for Next.js App Router#
A practical setup for “Next.js logging monitoring Sentry OpenTelemetry” uses two complementary layers:
- 1Sentry for error monitoring, release health, user impact, session replay, and performance workflows.
- 2OpenTelemetry for vendor-neutral distributed tracing and exporting traces to your backend of choice.
You can run Sentry only and be productive. Adding OpenTelemetry pays off once you have multiple services, queues, or you need consistent traces across infrastructure.
What to instrument in App Router#
| Next.js surface | Typical failures | What to capture | Primary tool |
|---|---|---|---|
| Server Actions | slow DB queries, auth failures, validation errors | spans around business logic, error with context | Sentry + OpenTelemetry |
| Route Handlers | timeouts, upstream HTTP errors, parsing issues | request span, downstream spans, status codes | Sentry + OpenTelemetry |
| RSC rendering | slow fetches, serialization errors | render transactions, fetch spans | Sentry |
| Client navigation | JS errors, slow routes, broken API calls | session replay, web vitals, long tasks | Sentry |
| Edge runtime | cold starts, upstream latency, limited Node APIs | minimal spans, error capture | Sentry + selective OTel |
For runtime tradeoffs and limitations, see: Next.js Edge runtime vs Node.js runtime on Vercel and Cloudflare.
ℹ️ Note: Treat logs as evidence, traces as the timeline, and errors as the symptom. Incidents get resolved fastest when you can jump from an alert to a trace, then to the exact error and its user impact.
# Prerequisites#
| Requirement | Version | Notes |
|---|---|---|
| Next.js | 14 or 15 | App Router assumed |
| Node.js | 18+ | Node runtime for full server instrumentation |
| Sentry account | — | One project for frontend plus backend (or combined) |
| OpenTelemetry collector | optional | Recommended for production export control |
| Hosting | Vercel, AWS, etc. | Edge and Node differ |
# Step 1: Define Your Observability Standards#
Before installing SDKs, set standards so data stays consistent across teams and services.
Naming conventions that keep data queryable#
| Item | Recommendation | Example |
|---|---|---|
| Service name | stable, environment-agnostic | web-app |
| Environment | production, staging, preview | preview |
| Release | CI commit SHA or semver | 2026.08.10+sha.abc123 |
| Transaction names | route-based, low cardinality | POST /api/orders |
| User/session key | non-PII, stable | sid_9f3... |
What “good” looks like in production#
Use these initial SLO-ish targets as a starting point:
| Signal | Initial target | Why |
|---|---|---|
| 5xx rate (Route Handlers) | less than 0.5% | above this users feel it quickly |
| P95 API latency | less than 800 ms | keeps UI responsive under load |
| P95 Server Action latency | less than 600 ms | Server Actions are user-facing interactions |
| Error regression | 2x baseline within 15 min | catches bad deploys early |
| Apdex | 0.85+ | quick “is it slow” health check |
Tune thresholds after 2 to 4 weeks of baseline data.
# Step 2: Install and Configure Sentry for Next.js App Router#
Sentry’s Next.js SDK covers client and server, and supports App Router surfaces well when configured correctly.
Install dependencies#
npm i @sentry/nextjs
npx @sentry/wizard@latest -i nextjsThis typically creates:
sentry.client.config.*sentry.server.config.*sentry.edge.config.*- updates to
next.config.*
Core Sentry settings that matter#
Ensure your Sentry config includes:
- Releases and environments
- Traces sampling for performance
- Profiles sampling if you use it (server profiling depends on runtime support)
Example minimal server config:
// sentry.server.config.js
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.VERCEL_ENV || process.env.NODE_ENV,
release: process.env.SENTRY_RELEASE,
tracesSampleRate: 0.1,
});Client config should also set a sampling rate and enable session replay only when needed to control cost.
Recommended sampling strategy#
| Environment | Errors | Traces | Replay |
|---|---|---|---|
| production | 100% (with grouping) | 5% to 15% | 0.5% sessions, 10% on error |
| staging | 100% | 25% | 2% sessions, 25% on error |
| preview | 100% | 10% | off or 0.1% |
Adjust based on traffic. For example, at 1000 requests per minute, 10% traces yields 100 traces per minute, usually enough for performance triage without overwhelming ingestion.
💡 Tip: Start traces at 10% in production, then add dynamic sampling rules: keep 100% for slow transactions (P95+), 5xx responses, and key user flows like checkout.
# Step 3: Capture Errors and Context in Server Actions#
Server Actions often fail silently if you only rely on console output. You want:
- the exception
- the action name
- user/session context
- input shape (sanitized)
- a trace that includes downstream calls
Example Server Action with Sentry and structured context#
// app/actions/createOrder.ts
"use server";
import * as Sentry from "@sentry/nextjs";
export async function createOrderAction(input: { sku: string; qty: number }) {
return await Sentry.startSpan(
{ name: "server_action:createOrder", op: "function" },
async () => {
try {
// Your business logic here
if (input.qty <= 0) throw new Error("Invalid quantity");
return { ok: true };
} catch (err) {
Sentry.captureException(err, {
tags: { surface: "server_action" },
extra: { sku: input.sku, qty: input.qty },
});
throw err;
}
}
);
}This creates a clear span and attaches the right metadata for grouping and filtering.
What not to log in Server Actions#
Avoid logging:
- emails, phone numbers, addresses
- raw access tokens
- full request headers
- payment data
Instead log stable identifiers: userId, tenantId, orderId, sessionId.
# Step 4: Instrument Route Handlers (API) with Traces and Request IDs#
Route Handlers are where you’ll want correlation across:
- inbound request
- outbound fetch calls
- database calls
- queue publishing
- response status and latency
Add a stable request ID and propagate trace headers#
In Next.js, you can implement a middleware that attaches a request ID header. Keep it low-cardinality and unique per request.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
const requestId = crypto.randomUUID();
const res = NextResponse.next();
res.headers.set("x-request-id", requestId);
return res;
}Then, in your Route Handler, log and tag with x-request-id, and forward trace headers when calling downstream services.
// app/api/orders/route.ts
import * as Sentry from "@sentry/nextjs";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const requestId = req.headers.get("x-request-id") || "missing";
return await Sentry.startSpan(
{ name: "POST /api/orders", op: "http.server" },
async () => {
try {
const body = await req.json();
const traceHeaders = {
"sentry-trace": req.headers.get("sentry-trace") || "",
baggage: req.headers.get("baggage") || "",
"x-request-id": requestId,
};
const upstream = await fetch(process.env.ORDERS_SERVICE_URL!, {
method: "POST",
headers: { "content-type": "application/json", ...traceHeaders },
body: JSON.stringify(body),
});
if (!upstream.ok) {
throw new Error(`Upstream failed with status ${upstream.status}`);
}
return NextResponse.json({ ok: true }, { headers: { "x-request-id": requestId } });
} catch (err) {
Sentry.captureException(err, {
tags: { surface: "route_handler" },
extra: { requestId },
});
return NextResponse.json({ ok: false }, { status: 500, headers: { "x-request-id": requestId } });
}
}
);
}This creates an end-to-end chain: browser event to Route Handler trace to upstream service, with a request ID for logs.
# Step 5: Add OpenTelemetry Tracing (Vendor-Neutral) for Server Runtime#
Sentry gives you an excellent workflow, but OpenTelemetry adds portability and consistency across infrastructure. The most robust Next.js OpenTelemetry story is still on Node runtime, because exporters and auto-instrumentation typically rely on Node APIs.
Recommended production topology#
| Component | Runs where | Responsibility |
|---|---|---|
| Next.js app | Node runtime | create spans, attach context |
| OpenTelemetry Collector | sidecar or separate service | batching, sampling, exporting |
| Trace backend | Tempo, Honeycomb, Datadog, New Relic | storage, querying, dashboards |
Install basic OpenTelemetry packages#
Keep it minimal to avoid bundle bloat.
npm i @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
npm i @opentelemetry/exporter-trace-otlp-httpMinimal OTel init for Node runtime#
Create a server-only file and load it early. The exact loading strategy depends on your deployment, but the core configuration looks like this:
// otel-node.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: { "x-otlp-api-key": process.env.OTEL_API_KEY || "" },
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();Keep OTel exporting server-side only. Do not attempt to run this in Edge.
⚠️ Warning: Auto-instrumentation can create high-cardinality spans if you capture full URLs with IDs. Normalize transaction names and avoid putting user identifiers into span names. Put identifiers into attributes only when necessary, and only after privacy review.
Add manual spans where auto-instrumentation won’t help#
Auto-instrumentation often misses business logic boundaries like “checkout validation” or “credit check.”
// app/lib/pricing.ts
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("web-app");
export async function calculatePrice(sku: string) {
return await tracer.startActiveSpan("pricing:calculate", async (span) => {
try {
span.setAttribute("sku", sku);
// ... do work
return 1999;
} finally {
span.end();
}
});
}Use manual spans sparingly around top-level operations that you actually query during incidents.
# Step 6: Handle Edge Runtime Observability Without Breaking Builds#
Edge runtime is great for latency, but it limits Node APIs and some SDKs. The safe approach is:
- keep Edge instrumentation lightweight
- do not rely on Node-only OTel SDK in Edge
- tag events with
runtime=edgeso dashboards split cleanly
A common pattern is to rely on Sentry Edge config for errors and basic tracing, and reserve OpenTelemetry for Node runtime paths.
To decide what should run on Edge, revisit: Next.js Edge runtime vs Node.js runtime on Vercel and Cloudflare.
Practical rule for teams#
| Workload | Recommended runtime | Why |
|---|---|---|
| Auth gating, redirects, A/B routing | Edge | fast, close to user |
| Payments, heavy DB access, PDF generation | Node | full SDK support, longer CPU time |
| Complex observability with OTel | Node | exporter and instrumentation compatibility |
# Step 7: Logging Strategy That Works in Serverless#
In serverless, logs are often your fastest “first glance,” but only if they’re structured and correlated.
What to log (and how)#
Log events at three levels:
| Level | When to use | Example fields |
|---|---|---|
| info | key business milestones | event, orderId, durationMs |
| warn | unexpected but recoverable | event, reason, retry |
| error | failures and exceptions | event, requestId, traceId |
Use JSON logs so your platform and log backend can parse fields. In Node runtime you can use any logger, but keep it simple and avoid giant dependencies in Edge.
Minimal JSON logger with correlation fields#
// app/lib/log.ts
export function log(level: "info" | "warn" | "error", message: string, data: Record<string, unknown> = {}) {
const payload = {
level,
message,
ts: new Date().toISOString(),
...data,
};
console.log(JSON.stringify(payload));
}Then in a Route Handler:
// app/api/health/route.ts
import { NextRequest, NextResponse } from "next/server";
import { log } from "@/app/lib/log";
export async function GET(req: NextRequest) {
const requestId = req.headers.get("x-request-id") || "missing";
log("info", "health_check", { requestId, surface: "route_handler" });
return NextResponse.json({ ok: true });
}If your tracing backend exposes a trace ID, include it in logs as traceId. For Sentry, you can also include sentryTrace from inbound headers.
# Step 8: Correlate User Sessions to Backend Traces#
Correlation is what turns observability from charts into root cause analysis.
Session identifier strategy#
Use a stable, non-PII session identifier:
- generated client-side
- stored in a first-party cookie
- forwarded as a header to API routes
Example: set a cookie sid and send it as x-session-id on fetch calls.
Client-side session ID creation (lightweight)#
// app/lib/session.ts
export function getOrCreateSessionId() {
const key = "sid";
const existing = window.localStorage.getItem(key);
if (existing) return existing;
const sid = `sid_${crypto.randomUUID()}`;
window.localStorage.setItem(key, sid);
return sid;
}Then attach it to requests:
// app/lib/http.ts
import { getOrCreateSessionId } from "@/app/lib/session";
export async function apiFetch(path: string, init: RequestInit = {}) {
const sid = getOrCreateSessionId();
const headers = new Headers(init.headers);
headers.set("x-session-id", sid);
return fetch(path, { ...init, headers });
}Capture the same session ID in Sentry#
In client code, set the user context to include the session ID. Keep it separate from user.id if you also have authenticated users.
// sentry-session.ts
import * as Sentry from "@sentry/nextjs";
import { getOrCreateSessionId } from "@/app/lib/session";
export function attachSentrySession() {
const sid = getOrCreateSessionId();
Sentry.setUser({ id: sid });
Sentry.setTag("session_id", sid);
}Call it once at app start in a client component.
Use the session ID in backend spans and logs#
In Route Handlers and Server Actions, read x-session-id from headers when available, and attach it:
- as a Sentry tag
- as an OTel attribute
- as a log field
This enables queries like “show all errors for session X” and “open the trace for the slow checkout session.”
🎯 Key Takeaway: Correlation requires the same identifier to exist in three places: browser telemetry, backend telemetry, and logs. Without that, you can’t reliably move from “user report” to “trace” in minutes.
# Step 9: Dashboards That Teams Actually Use#
Dashboards should answer operational questions in under 30 seconds. Create separate dashboards for:
- availability and errors
- latency and regressions
- runtime split: Edge vs Node
- critical flows: login, checkout, search
Recommended dashboard widgets#
| Dashboard | Widget | Suggested breakdown |
|---|---|---|
| Availability | 5xx rate, error count | by route, by release |
| Performance | P50, P95 latency | by route, by runtime |
| User impact | affected users, sessions | by country, by browser |
| Regression | errors introduced in last release | by release, by endpoint |
| Dependencies | upstream latency and errors | by hostname |
In Sentry specifically, configure:
- Releases and commit tracking
- Performance views with transaction breakdown
- Alerts tied to release version
If you run background jobs, treat them as a separate service with their own dashboard and alerts. Next.js projects often hide critical work in cron or queues, so instrument those too: Next.js background jobs, queues, and cron on Vercel.
# Step 10: Actionable Alerts and Thresholds (Without Noise)#
Most alert fatigue comes from two issues: alerting on raw counts instead of rates, and not separating “symptom” from “impact.”
Practical alert thresholds to start with#
| Alert | Condition | Window | Action |
|---|---|---|---|
| API 5xx spike | 5xx rate greater than 1% | 10 min | page on-call |
| Latency regression | P95 greater than 1.5x baseline | 15 min | notify dev channel |
| Error regression on release | new issue count greater than 20 | 30 min after deploy | block rollout |
| Checkout failure | POST /api/orders failure rate greater than 0.5% | 10 min | page on-call |
| Edge anomaly | Edge errors greater than 2x baseline | 15 min | investigate runtime config |
Use different channels for severity. Paging should be reserved for conditions that impact revenue or core functionality.
Make alerts actionable with context#
Every alert should include:
- environment
- release
- top 3 transactions affected
- link to a sample trace
- affected users or sessions
In Sentry, tie alerts to:
- Issue alerts for error spikes and new regressions
- Metric alerts for latency and throughput
# Common Pitfalls (and How to Avoid Them)#
- 1High-cardinality tags — Avoid tagging with full URLs containing IDs or emails. Normalize routes and use safe identifiers.
- 2No runtime split — If you don’t tag
runtime=edgevsruntime=node, you’ll misdiagnose performance and blame the wrong layer. - 3Sampling without strategy — Sampling everything at a flat 1% often misses the exact slow traces you care about. Keep higher sampling for slow and failing requests.
- 4Logging without correlation — Logs that don’t include
requestId,sessionId, and trace context are expensive noise during incidents. - 5Treating background jobs as “secondary” — failed cron runs often create user-facing incidents hours later. Instrument jobs as first-class services.
# Key Takeaways#
- Instrument Next.js App Router end-to-end by covering Server Actions, Route Handlers, and client navigation, then split dashboards by Edge vs Node runtime.
- Use Sentry for fast error triage and user impact, and add OpenTelemetry on Node runtime for vendor-neutral distributed traces.
- Standardize correlation fields:
x-request-id,x-session-id, plus trace headers likesentry-traceandbaggage, then put them into logs, spans, and error events. - Build dashboards that answer operational questions fast: 5xx rate, P95 latency, regressions by release, and dependency health by hostname.
- Start with alert thresholds based on rates and baselines (not raw counts), and route alerts by severity to avoid alert fatigue.
# Conclusion#
A solid “Next.js logging monitoring Sentry OpenTelemetry” setup is not about collecting more data—it’s about making failures diagnosable within minutes. Instrument Sentry across client, server, and edge, add OpenTelemetry on Node runtime for portable tracing, standardize correlation IDs, and ship dashboards and alerts that map to user impact and business risk.
If you want Samioda to implement this end-to-end in your Next.js codebase, including runtime split strategy, dashboards, and alert tuning, contact us and we’ll ship an observability setup your team can actually operate in production.
FAQ
Founder & Senior Developer at Samioda. 8+ years building React, Next.js, Flutter and n8n automation solutions for clients across Europe.
More in Web Development
All →Next.js App Router UX Patterns: Error Boundaries, Loading UI, and Streaming Done Right
A practical guide to resilient UX in Next.js App Router: route segment structure, error.tsx, loading.tsx, not-found.tsx, and Suspense streaming patterns for partial rendering, safer data fetching, and fewer layout shifts.
Building a Design System in Next.js with Radix UI, Tailwind, and Storybook: End-to-End Guide for 2026
A practical, production-ready approach to building and maintaining a Next.js design system using Radix UI for accessibility, Tailwind for styling, and Storybook for documentation, testing, and versioned releases.
The React Code Review Checklist We Use: Performance, Accessibility, and Maintainability
A practical React code review checklist focused on performance, accessibility, and maintainability, with examples, automation tips, and a copy-paste template.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
Web Application Observability: A Practical Guide to Logging, Metrics, and Tracing for React and Next.js
An end-to-end, production-ready observability setup for React and Next.js: error tracking, performance monitoring, structured logs, tracing, dashboards, and alerts that catch real issues.
Next.js App Router UX Patterns: Error Boundaries, Loading UI, and Streaming Done Right
A practical guide to resilient UX in Next.js App Router: route segment structure, error.tsx, loading.tsx, not-found.tsx, and Suspense streaming patterns for partial rendering, safer data fetching, and fewer layout shifts.
Flutter Observability in Production: Crash Reporting, Logging, and Performance Monitoring
A practical 2026 guide to Flutter production observability: instrument crashes, non-fatal errors, API latency, app start, and frame timings. Includes Crashlytics vs Sentry comparison, integration steps, and a release checklist.