← Back to Blog Home

Next.js already traces your requests. Here's how to export them with OpenTelemetry.

Next.js already traces your requests. Here's how to export them with OpenTelemetry.

Traces are a goldmine of information that can help you, or your AI, find slow pages and fix them.

Next.js comes out of the box with support for tracing. Incoming requests, fetch() calls, middleware, and server-side rendering are all wired up and ready to send traces to any OpenTelemetry-compatible backend.

The catch is, unless you configure an exporter, you’ll never see those traces.

With a few lines of code and some help from the @vercel/otel library, your app can export those traces to any platform that accepts OpenTelemetry data, including Sentry.

(And if you’re trying to decide between OTLP and the Sentry SDK, we’ll cover that too.)

Why Next.js traces matter

When a page in your Next.js app is slow, the hard part is figuring out which part of the request is responsible. It might be middleware, server-side rendering, an API route, a database query, an upstream fetch() call, or a custom function you wrote. Without a trace of how the request actually executed, you end up working backwards from symptoms: reproducing locally, adding logs, guessing at likely bottlenecks, and hoping production behaves the same way your machine does.

With tracing, each API call, page load, database query, and more, is recorded as a span in an execution timeline. A trace groups all of the spans from a single request into a waterfall, so you can see exactly where time was spent.

Sentry trace waterfall showing a Next.js GET /api/auth/[...nextauth] request broken into resolve page components, execute API route, and start response spans

In this trace, the top-level GET /api/auth/[...nextauth] span represents the incoming request. Under it, Next.js creates additional spans for every step in that request; resolving page components, executing the API route, and starting the response. With this full hierarchy, we can see exactly where time was spent during that request.

In the Node runtime, you’ll begin to enrich this timeline with your own custom spans that add more app-specific context to each trace (we’ll walk through some examples down below).

Edge runtime traces can still include automatic spans from Next.js through @vercel/otel, but @vercel/otel does not support custom spans on Edge.

OpenTelemetry is an open standard for recording and transporting a variety of telemetry data, like traces, to any backend that will support it. In order to get this data out of your app, you just need to tell OpenTelemetry where to send it.

Configuring OpenTelemetry for Next.js

Next.js already has server-side tracing enabled, OpenTelemetry just needs to know where to send it.

Vercel offers a library, @vercel/otel, that configures the OpenTelemetry SDK for you and handles the differences between the Node and Edge runtimes automatically. We’ll use it to configure the SDK and export the traces to Sentry.

This is particularly useful if you have an existing OpenTelemetry stack, and are self-hosting Next.js. If you are hosting on Vercel, there are other options available.

Sentry provides a direct OTLP endpoint that you can use to send OpenTelemetry data to, but you can use any OTLP-compatible backend.

// instrumentation.ts
import { registerOTel } from "@vercel/otel";

export function register() {
  registerOTel({ serviceName: "next-app" });
}

Next.js automatically loads instrumentation.ts from the project root at startup. The register function runs once before any request is handled, providing a good hook to configure the OpenTelemetry SDK.

That’s essentially all you need to add to your app. Exporting the traces to Sentry only requires two environment variables, which we’ll cover in a second.

You can read the full setup guide for @vercel/otel in the Vercel docs.

Adding your own spans

Even though Next.js comes pre-instrumented with tracing, the spans it emits are generic and don’t include any unique attributes about your specific business logic. It’s a good and useful starting point, but to really get the most out of tracing, you’ll want to add your own spans around critical parts of your application.

Add your own spans around important business operations that you would name during an incident: generating an invoice, charging a card, resizing an uploaded image, calling an AI model, scoring recommendations, syncing a customer to a third-party API, etc.

Don’t wrap every helper function. Start with high-level operations that impact user experience and business metrics.

For span names, use consistent action-oriented names like invoice.generate-pdf or ai.summarize-ticket. Dotted notation for naming is a common convention. It’s a useful way to group related spans by domain, and you can use them to scope your search when aggregating later. For example, you might start by investigating all invoice spans, and then narrow down to a specific operation like invoice.generate-pdf.

For attributes, try to add context that will help you filter and group traces when investigating performance; feature flags, plan tiers, batch sizes, item counts, payload sizes, generated file sizes, etc. Avoid raw user IDs, emails, secrets, prompts, or anything with very high cardinality unless your backend explicitly supports that use case.

Let’s look at some spans we might add to an invoice generator, for example:

import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("next-app");

export async function generateInvoicePdf(invoice: Invoice) {
  return tracer.startActiveSpan("invoice.generate-pdf", async (span) => {
    try {
      span.setAttribute("invoice.line_items_count", invoice.lineItems.length);
      span.setAttribute("invoice.locale", invoice.locale);
      span.setAttribute("invoice.template", invoice.template);
      span.setAttribute("customer.plan", invoice.customer.plan);

      const pdf = await renderInvoicePdf(invoice);

      span.setAttribute("invoice.pdf_size_bytes", pdf.byteLength);

      return pdf;
    } finally {
      span.end();
    }
  });
}

Now, if invoice generation gets slow, you can search for invoice.generate-pdf, group those spans by template, locale, or customer plan, and quickly see whether large invoices, a specific PDF template, or one customer segment is causing the slowdown.

An important caveat of custom spans for @vercel/otel is that they are only supported in the Node runtime. The @vercel/otel library does instrument automatic spans in the Edge runtime, but you cannot add your own. To add custom spans in the Edge runtime, consider using the @sentry/nextjs SDK instead.

Sending traces to an OTLP backend

OTLP is the standard protocol for transporting OpenTelemetry traces. An OTLP backend is the destination that receives those traces, stores them, and gives you a UI for searching, filtering, and viewing trace waterfalls. That could be an observability platform like Sentry, or another OpenTelemetry-compatible service.

@vercel/otel expects and honors the standard OTEL_EXPORTER_OTLP_* environment variables for configuring the exporter. So, to export to Sentry’s direct OTLP endpoint, you’ll need to set two environment variables: an endpoint URL and an authentication header.

You’ll find your endpoint URL and public key under Project Settings → Client Keys (DSN).

Set two environment variables:

export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="___OTLP_TRACES_URL___"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="x-sentry-auth=sentry sentry_key=___PUBLIC_KEY___"

Set those wherever you deploy your app so they exist at runtime. Once set, you should start immediately seeing traces populating your Sentry Trace View. No Sentry SDK, no collector, minimal code change.

Choosing between OTLP and the Sentry SDK

OpenTelemetry is vendor-neutral and excellent when you are already deep in the ecosystem. But, if you’re configuring tracing for the first time, my recommendation, especially for Next.js, is to use the Sentry SDK.

@vercel/otel is easy to configure, but it doesn’t capture the whole picture. It can export useful server-side spans from the Node and Edge runtimes, but it won’t give you the browser side of the trace, Sentry issues, or the rest of the debugging context that comes from the SDK.

Feature@vercel/otel direct to Sentry OTLP@sentry/nextjs SDK
Node server tracingYesYes
Edge runtime tracingAutomatic server spans onlyYes
Custom spansNode runtime onlyNode, Edge, and browser
Browser tracingNoYes
Error monitoringNoYes
Source-mapped stack tracesNoYes
Session ReplayNoYes
LogsNoYes

The practical choice is simpler than the number of integrations makes it look. If your team relies on Sentry to find, group, and debug production issues, use the SDK. If you already have an OpenTelemetry pipeline, want to migrate in stages, or only need standards-based server traces in Sentry, direct OTLP is the leaner option.

One compatibility note: don’t initialize @vercel/otel and the Sentry SDK’s OpenTelemetry setup in the same app. Both register a tracer provider. If you already manage your own NodeTracerProvider and want to add Sentry alongside it, set skipOpenTelemetrySetup: true in Sentry.init() and follow the custom setup guide.

If your app runs on Vercel and you want platform-level forwarding instead of app-level exporter configuration, Vercel Drains can send logs and traces to Sentry too. Just remember that drains and direct OTLP still do not replace SDK-based error monitoring.

Next steps

Once your Next.js traces are flowing into Sentry, turn them into something your team can act on. Use Monitors to watch the routes and operations that matter most: auth flows, checkout, dashboards, API routes, and any page users complain about being slow. Monitors can evaluate thresholds on spans and other application signals, then turn those conditions into issues when they cross the line.

Pair those monitors with Alerts so the right people hear about new or worsening issues in Slack, email, PagerDuty, webhooks, or whatever workflow your team uses. That gives you a path from trace data, to a triageable issue, to the action your team needs to take.

Then build dashboards around the questions you want answered every week: which routes are slowest, which custom spans are regressing, and whether performance changed after a deploy.

Traces are most useful when they become part of your operational loop: monitor the important paths, alert when they drift, use dashboards to see whether the fixes actually helped, and try Seer Agent to automatically inspect traces and explain what’s causing a slow or failing request.

FAQs

Do I need the OpenTelemetry Collector to send traces to Sentry?

No. Sentry now accepts OTLP traces directly at an ingestion endpoint, so for the simple case you point @vercel/otel straight at Sentry with two environment variables. Add a collector back into the pipeline only when you need to sample, transform, or route traces to multiple destinations before ingestion.

Why use @vercel/otel instead of the raw OpenTelemetry SDK?

Because of the Edge runtime. The manual setup relies on @opentelemetry/sdk-node, which doesn't run on Edge. That means middleware and Edge route handlers produce no automatic spans, and you have to guard imports with process.env.NEXT_RUNTIME === 'nodejs'. @vercel/otel works in both the Node and Edge runtimes for Next.js's automatic server spans. Custom spans are still Node-runtime only.

Does @vercel/otel trace the browser?

No. @vercel/otel runs in the Node and Edge runtimes only. It instruments your server, not the client. End-to-end traces that start in the browser and continue into your server need separate client-side instrumentation. The @sentry/nextjs SDK covers the browser through its instrumentation-client.ts entry point.

Can I use @vercel/otel and the Sentry SDK at the same time?

Not directly — both register an OTel provider, so they conflict. If you want to move from @vercel/otel to the Sentry SDK, replace registerOTel() with Sentry.init(). If you have a separate custom NodeTracerProvider setup (not @vercel/otel), you can add the Sentry SDK alongside it using skipOpenTelemetrySetup: true.

Why don't I see all the spans I expected?

Next.js emits a useful subset of its default spans. To see everything, including lower-level internals, set NEXT_OTEL_VERBOSE=1 in your environment. If you're running your own fetch instrumentation and want to suppress Next.js's automatic fetch spans, set NEXT_OTEL_FETCH_DISABLED=1.

What are the limitations of direct OTLP ingestion?

It's in open beta. Span events are dropped entirely, while span links and array attributes are ingested and displayed in the Trace View but can't be searched, filtered, or aggregated. If you need full fidelity and tight error/log correlation, the @sentry/nextjs SDK is the richer option.

Syntax.fm logo

Listen to the Syntax Podcast

Of course we sponsor a developer podcast. Check it out on your favorite listening platform.

Listen To Syntax