Web Development
Next.jsObservabilitySentryOpenTelemetryLoggingTracingMonitoring

Observability for Next.js App Router in 2026: Sentry, OpenTelemetry, Traces, and Actionable Alerts

AO
Adrijan Omićević
·16 min read

# 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:

  1. 1
    Sentry for error monitoring, release health, user impact, session replay, and performance workflows.
  2. 2
    OpenTelemetry 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 surfaceTypical failuresWhat to capturePrimary tool
Server Actionsslow DB queries, auth failures, validation errorsspans around business logic, error with contextSentry + OpenTelemetry
Route Handlerstimeouts, upstream HTTP errors, parsing issuesrequest span, downstream spans, status codesSentry + OpenTelemetry
RSC renderingslow fetches, serialization errorsrender transactions, fetch spansSentry
Client navigationJS errors, slow routes, broken API callssession replay, web vitals, long tasksSentry
Edge runtimecold starts, upstream latency, limited Node APIsminimal spans, error captureSentry + 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#

RequirementVersionNotes
Next.js14 or 15App Router assumed
Node.js18+Node runtime for full server instrumentation
Sentry accountOne project for frontend plus backend (or combined)
OpenTelemetry collectoroptionalRecommended for production export control
HostingVercel, 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#

ItemRecommendationExample
Service namestable, environment-agnosticweb-app
Environmentproduction, staging, previewpreview
ReleaseCI commit SHA or semver2026.08.10+sha.abc123
Transaction namesroute-based, low cardinalityPOST /api/orders
User/session keynon-PII, stablesid_9f3...

What “good” looks like in production#

Use these initial SLO-ish targets as a starting point:

SignalInitial targetWhy
5xx rate (Route Handlers)less than 0.5%above this users feel it quickly
P95 API latencyless than 800 mskeeps UI responsive under load
P95 Server Action latencyless than 600 msServer Actions are user-facing interactions
Error regression2x baseline within 15 mincatches bad deploys early
Apdex0.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#

Bash
npm i @sentry/nextjs
npx @sentry/wizard@latest -i nextjs

This 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:

JavaScript
// 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.

EnvironmentErrorsTracesReplay
production100% (with grouping)5% to 15%0.5% sessions, 10% on error
staging100%25%2% sessions, 25% on error
preview100%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#

TypeScript
// 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.

TypeScript
// 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.

TypeScript
// 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.

ComponentRuns whereResponsibility
Next.js appNode runtimecreate spans, attach context
OpenTelemetry Collectorsidecar or separate servicebatching, sampling, exporting
Trace backendTempo, Honeycomb, Datadog, New Relicstorage, querying, dashboards

Install basic OpenTelemetry packages#

Keep it minimal to avoid bundle bloat.

Bash
npm i @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
npm i @opentelemetry/exporter-trace-otlp-http

Minimal 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:

TypeScript
// 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.”

TypeScript
// 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=edge so 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#

WorkloadRecommended runtimeWhy
Auth gating, redirects, A/B routingEdgefast, close to user
Payments, heavy DB access, PDF generationNodefull SDK support, longer CPU time
Complex observability with OTelNodeexporter 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:

LevelWhen to useExample fields
infokey business milestonesevent, orderId, durationMs
warnunexpected but recoverableevent, reason, retry
errorfailures and exceptionsevent, 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#

TypeScript
// 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:

TypeScript
// 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)#

TypeScript
// 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:

TypeScript
// 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.

TypeScript
// 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
DashboardWidgetSuggested breakdown
Availability5xx rate, error countby route, by release
PerformanceP50, P95 latencyby route, by runtime
User impactaffected users, sessionsby country, by browser
Regressionerrors introduced in last releaseby release, by endpoint
Dependenciesupstream latency and errorsby 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#

AlertConditionWindowAction
API 5xx spike5xx rate greater than 1%10 minpage on-call
Latency regressionP95 greater than 1.5x baseline15 minnotify dev channel
Error regression on releasenew issue count greater than 2030 min after deployblock rollout
Checkout failurePOST /api/orders failure rate greater than 0.5%10 minpage on-call
Edge anomalyEdge errors greater than 2x baseline15 mininvestigate 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)#

  1. 1
    High-cardinality tags — Avoid tagging with full URLs containing IDs or emails. Normalize routes and use safe identifiers.
  2. 2
    No runtime split — If you don’t tag runtime=edge vs runtime=node, you’ll misdiagnose performance and blame the wrong layer.
  3. 3
    Sampling 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.
  4. 4
    Logging without correlation — Logs that don’t include requestId, sessionId, and trace context are expensive noise during incidents.
  5. 5
    Treating 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 like sentry-trace and baggage, 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

Share
A
Adrijan OmićevićFounder & Senior Developer

Founder & Senior Developer at Samioda. 8+ years building React, Next.js, Flutter and n8n automation solutions for clients across Europe.

Need help with your project?

We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.