Next.js
Send OpenTelemetry traces from Next.js applications, server and browser, to KubeSense.
Next.js code runs in up to three places. Each is instrumented differently:
| Where it runs | What runs there | How to instrument it |
|---|---|---|
| Node.js server | Server-side rendering, React Server Components, route handlers (app/api/...), Server Actions, API routes (pages/api/...), getServerSideProps | OpenTelemetry for Node.js: auto-instrumentation or the manual SDK |
| Edge runtime | Middleware and routes with export const runtime = 'edge' | Only the manual SDK with @vercel/otel |
| Browser | Client components, client-side navigation, fetch calls from the page | KubeSense Browser RUM, or the OpenTelemetry web SDK |
Start with the server. Every Next.js application has a server unless it is built with output: 'export' (a fully static site). Server spans show where time is spent: rendering, data fetching, database calls and calls to your other services. Then add browser instrumentation, which links each user action to the server trace it causes.
Browser (RUM SDK or OTel web SDK)
│ fetch / navigation ── traceparent header ──┐
▼ ▼
Next.js server (OTel for Node.js) ── DB, Redis, downstream HTTP calls
│ OTLP/HTTP
▼
KubeSense trace ingest (kubesense-kubecol:33443) → APM → Traces, Services, Service MapBefore you start, enable trace ingestion on KubeSense and choose an endpoint. See Instrumentation.
Server: auto-instrumentation (no code changes)
The Next.js server is a Node.js process, so the standard Node.js auto-instrumentation works with no code changes. It creates spans for incoming requests, outgoing fetch and HTTP calls, and database and cache clients (PostgreSQL, MySQL, MongoDB, Redis and more). Next.js also reports its own spans, for example for route rendering and fetch inside Server Components, when an OpenTelemetry tracer is registered in the process.
Auto-instrumentation covers the Node.js runtime only. Middleware and edge routes are not traced. If you need them, use @vercel/otel.
Option 1: Kubernetes with the OpenTelemetry Operator
If you have set up the OpenTelemetry Operator, add the Node.js annotation to the pod template and restart the workload:
spec:
template:
metadata:
annotations:
instrumentation.opentelemetry.io/inject-nodejs: "kubesense/otel-instrumentation"
spec:
containers:
- name: web
image: <YOUR_REGISTRY>/storefront:1.0.0
env:
- name: OTEL_SERVICE_NAME
value: storefrontkubectl rollout restart deployment/storefront -n <namespace>The operator injects the agent through NODE_OPTIONS. It works with both next start and the standalone node server.js, and needs no package installation or image change.
Don't combine with instrumentation.ts: If the application also registers OpenTelemetry in instrumentation.ts (see Manual instrumentation), remove that code or skip the annotation. Two SDKs in one process produce duplicate or missing spans.
Option 2: Auto-instrumentation on VMs or containers
Use this on VMs, or on Kubernetes clusters without the operator.
Step 1: Install the packages
npm install --save @opentelemetry/api @opentelemetry/auto-instrumentations-nodeStep 2: Set the environment variables and start Next.js
export OTEL_SERVICE_NAME=storefront
export OTEL_EXPORTER_OTLP_ENDPOINT=http://<KUBESENSE_INGEST_HOST>:33443
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_RESOURCE_ATTRIBUTES=kubesense.cluster=<CLUSTER_NAME>,kubesense.env_type=legacy
export OTEL_NODE_RESOURCE_DETECTORS=env,host,os,process
export OTEL_NODE_DISABLED_INSTRUMENTATIONS=fs,dns,net
export OTEL_METRICS_EXPORTER=none
export OTEL_LOGS_EXPORTER=none
export NODE_OPTIONS="--require @opentelemetry/auto-instrumentations-node/register"
npx next startThe variables are described on the Node.js page. On Kubernetes, use http://kubesense-kubecol.kubesense.svc.cluster.local:33443 and kubesense.env_type=k8s. OTEL_NODE_DISABLED_INSTRUMENTATIONS=fs,dns,net is recommended for Next.js, because Next.js reads many files at runtime and the fs spans hide the useful ones.
Step 3: Verify. Load a few pages, then open APM → Traces and filter by your service name.
Standalone output (output: 'standalone'): A standalone build copies only the packages that your application imports into .next/standalone/node_modules. @opentelemetry/auto-instrumentations-node is loaded through NODE_OPTIONS, not imported, so it is missing from the image and Node.js fails to start with Cannot find module. For standalone images, use the OpenTelemetry Operator, which brings its own copy of the agent, or the manual SDK, which is part of the build.
Database spans missing?: Auto-instrumentation patches a library when it is loaded with require(). If Next.js bundles a library into its server build, that library is never patched. Next.js keeps common database clients such as pg, mysql2, mongoose and @prisma/client out of the bundle by default. For other libraries, add them to serverExternalPackages in next.config.js (Next.js 15), or to experimental.serverComponentsExternalPackages (Next.js 13 and 14).
Server: manual instrumentation (SDK)
With manual instrumentation, the OpenTelemetry setup is part of the application, in the instrumentation.ts file that Next.js loads once when the server starts. Use this instead of auto-instrumentation when:
- you build standalone images,
- you need traces from middleware or edge routes,
- you want to control the setup in code, or add custom spans.
Requirements
-
Next.js 13.2 or later. In Next.js 15 and later,
instrumentation.tsis stable. In Next.js 13.2 to 14, enable it innext.config.js:// next.config.js (Next.js 13.2 to 14 only) module.exports = { experimental: { instrumentationHook: true, }, }; -
Put
instrumentation.tsin the project root, or insrc/if your project uses asrcfolder. Not insideapp/orpages/.
Option A: @vercel/otel (recommended)
@vercel/otel is the OpenTelemetry package that the Next.js documentation recommends. It works in both the Node.js and the edge runtime, and it registers Next.js's own spans. Despite the name, it doesn't need Vercel. It works on any host.
Step 1: Install the packages
npm install --save @vercel/otel @opentelemetry/apiStep 2: Create instrumentation.ts
// instrumentation.ts (project root, or src/)
import { registerOTel, OTLPHttpProtoTraceExporter } from '@vercel/otel';
export function register() {
registerOTel({
serviceName: process.env.OTEL_SERVICE_NAME ?? 'storefront',
attributes: {
'kubesense.cluster': '<CLUSTER_NAME>',
'kubesense.env_type': 'k8s', // 'legacy' on VMs
},
traceExporter: new OTLPHttpProtoTraceExporter({
// Full URL including /v1/traces
url: process.env.KUBESENSE_TRACES_URL ??
'http://kubesense-kubecol.kubesense.svc.cluster.local:33443/v1/traces',
}),
});
}Step 3: Set the environment variables and start the application
export OTEL_SERVICE_NAME=storefront
export KUBESENSE_TRACES_URL=http://kubesense-kubecol.kubesense.svc.cluster.local:33443/v1/traces
npm run build && npm startKUBESENSE_TRACES_URL is read only by the code above. Outside the KubeSense cluster, use http://<KUBESENSE_INGEST_HOST>:33443/v1/traces, or send traces to your OpenTelemetry Collector at http://<COLLECTOR_HOST>:4318/v1/traces. See Manual SDK: where to send traces.
note: @vercel/otel exports the traces itself, using the exporter in the code, so you don't need to set NODE_OPTIONS or the OTEL_EXPORTER_OTLP_* variables. Don't set NODE_OPTIONS=--require @opentelemetry/auto-instrumentations-node/register together with this setup, because that starts a second SDK.
@vercel/otel traces incoming requests and fetch calls. To trace database clients and other libraries as well, add their instrumentations:
npm install --save @opentelemetry/instrumentation-pg @opentelemetry/instrumentation-ioredisimport { registerOTel, OTLPHttpProtoTraceExporter } from '@vercel/otel';
import { PgInstrumentation } from '@opentelemetry/instrumentation-pg';
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
export function register() {
registerOTel({
serviceName: process.env.OTEL_SERVICE_NAME ?? 'storefront',
attributes: { 'kubesense.cluster': '<CLUSTER_NAME>', 'kubesense.env_type': 'k8s' },
traceExporter: new OTLPHttpProtoTraceExporter({ url: process.env.KUBESENSE_TRACES_URL }),
// 'auto' keeps the default fetch and HTTP instrumentation
instrumentations: ['auto', new PgInstrumentation(), new IORedisInstrumentation()],
});
}Library instrumentations work only in the Node.js runtime, and only for libraries that are not bundled. See Database spans missing? above.
Option B: OpenTelemetry Node.js SDK
Use the full Node.js SDK when you want every library that auto-instrumentations-node supports, configured from the standard OTEL_* variables. It runs in the Node.js runtime only, so middleware and edge routes are not traced.
Step 1: Install the packages
npm install --save @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-nodeStep 2: Create the setup files
The SDK uses Node.js APIs that don't exist in the edge runtime, so load it only in the Node.js runtime:
// instrumentation.ts (project root, or src/)
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./instrumentation.node');
}
}// instrumentation.node.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
// The exporter, endpoint, service name and resource attributes
// are configured from the OTEL_* environment variables.
const sdk = new NodeSDK({
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false },
}),
],
});
sdk.start();Keep the SDK out of the Next.js server bundle, so that its instrumentations can patch libraries when they load:
// next.config.js (Next.js 15; in 13.2 to 14, use experimental.serverComponentsExternalPackages)
module.exports = {
serverExternalPackages: ['@opentelemetry/sdk-node', '@opentelemetry/auto-instrumentations-node'],
};Step 3: Configure the endpoints
Set the variables from Manual SDK: where to send traces, for example:
OTEL_SERVICE_NAME=storefront
OTEL_EXPORTER_OTLP_ENDPOINT=http://kubesense-kubecol.kubesense.svc.cluster.local:33443
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_RESOURCE_ATTRIBUTES=kubesense.cluster=<CLUSTER_NAME>,kubesense.env_type=k8s
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=none
OTEL_LOGS_EXPORTER=noneMigrating from Datadog dd-trace
Applications that start dd-trace in instrumentation.ts move to Option B with the same structure: the NEXT_RUNTIME === 'nodejs' check stays, and the dd-trace import becomes import('./instrumentation.node').
dd-trace setting | OpenTelemetry equivalent |
|---|---|
hostname, port (DD_AGENT_HOST, DD_TRACE_AGENT_PORT) | OTEL_EXPORTER_OTLP_ENDPOINT |
service (DD_SERVICE) | OTEL_SERVICE_NAME, or service.name in the resource |
env | Resource attribute deployment.environment.name |
version | Resource attribute service.version |
tags | Resource attributes, in OTEL_RESOURCE_ATTRIBUTES or resourceFromAttributes({...}) in code |
tracePropagationStyle: ['datadog', 'tracecontext'] | Nothing to set. OpenTelemetry uses W3C tracecontext and baggage by default |
runtimeMetrics | Not needed. KubeSense collects pod CPU and memory from the cluster |
Also:
- Replace
dd-tracewith the OpenTelemetry packages inserverExternalPackagesand in any webpackexternalsinnext.config.js, and removedeclare module 'dd-trace'type declarations. - Search for other
dd-traceusers, such as logging libraries with add-traceintegration that addsdd.trace_idto log lines. Replace them before removing the package. The OpenTelemetry instrumentations for pino, winston and bunyan, included inauto-instrumentations-node, addtrace_idandspan_idto log records. - Rewrite custom spans made with
tracer.trace()ortracer.wrap()with the OpenTelemetry API (see below). - Remove the
DD_*environment variables.
Custom spans
With either option, create spans around your own code with the OpenTelemetry API:
// app/checkout/actions.ts
'use server';
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('storefront');
export async function placeOrder(cartId: string) {
return tracer.startActiveSpan('placeOrder', async (span) => {
try {
span.setAttribute('cart.id', cartId);
return await submitOrder(cartId);
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end();
}
});
}Next.js tracing options
| Variable | Effect |
|---|---|
NEXT_OTEL_VERBOSE=1 | Next.js reports more of its internal spans. Useful while investigating; noisy in production |
NEXT_OTEL_FETCH_DISABLED=1 | Next.js stops creating spans for fetch calls. Use this when another instrumentation already traces fetch |
Container image
With the manual SDK, the setup is compiled into the build, so a standalone image needs only the endpoint variables:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # next.config.js sets output: 'standalone'
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]Set OTEL_SERVICE_NAME and the endpoint (KUBESENSE_TRACES_URL for Option A, the OTEL_* variables for Option B) in the Kubernetes manifest, so the same image works in every environment.
Browser instrumentation
Server instrumentation doesn't see what happens in the user's browser: page load time, Web Vitals, JavaScript errors, or slow fetch calls that the page makes. There are two ways to add the browser side:
| KubeSense Browser RUM (recommended) | OpenTelemetry web SDK | |
|---|---|---|
| Page views, sessions, Web Vitals | ✅ | — |
| JavaScript errors, long tasks, user actions | ✅ | User interactions only |
| Session Replay | ✅ (full SDK) | — |
Browser fetch / XHR linked to server traces | ✅ allowedTracingUrls | ✅ |
| Where data appears | RUM | APM → Traces |
| Endpoint | KubeSense RUM endpoint (CORS supported) | Needs a proxy or a collector with CORS |
Use one of them. Both add a traceparent header to fetch calls, and using both creates conflicting trace contexts.
KubeSense Browser RUM
The Browser RUM SDK collects page views, sessions, Web Vitals, resources, errors and, optionally, Session Replay. With allowedTracingUrls, it also adds a W3C traceparent header to requests to your own server, so the server spans from the previous sections join the same trace.
Step 1: Install the SDK
npm install @kubesense/kubesense-browser-rumStep 2: Initialize it in the browser
Next.js 15.3 and later: create instrumentation-client.ts in the project root (or src/). Next.js runs it in the browser before the application starts:
// instrumentation-client.ts
import { kubesenseSdk } from '@kubesense/kubesense-browser-rum';
kubesenseSdk.init({
applicationId: process.env.NEXT_PUBLIC_KUBESENSE_RUM_APP_ID!,
clientToken: process.env.NEXT_PUBLIC_KUBESENSE_RUM_CLIENT_TOKEN!,
kubesenseRumEndpoint: '<YOUR_KUBESENSE_RUM_ENDPOINT>',
service: 'storefront-web',
env: 'production',
version: process.env.NEXT_PUBLIC_APP_VERSION,
sessionSampleRate: 100,
trackUserInteractions: true,
trackResources: true,
trackLongTasks: true,
// Link browser requests to server traces. Include your own API domains.
allowedTracingUrls: [
{
match: (url: string) => url.startsWith(window.location.origin),
propagatorTypes: ['tracecontext'],
},
{ match: /https:\/\/api\.example\.com/, propagatorTypes: ['tracecontext'] },
],
traceSampleRate: 100,
});Next.js 13 and 14 (App Router): create a client component and render it in the root layout:
// app/kubesense-rum.tsx
'use client';
import { kubesenseSdk } from '@kubesense/kubesense-browser-rum';
if (typeof window !== 'undefined') {
kubesenseSdk.init({
// same options as above
});
}
export function KubesenseRum() {
return null;
}// app/layout.tsx
import { KubesenseRum } from './kubesense-rum';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<KubesenseRum />
{children}
</body>
</html>
);
}Pages Router: import the same kubesense-rum.tsx module at the top of pages/_app.tsx.
Browser variables: Only variables prefixed with NEXT_PUBLIC_ reach the browser, and they are fixed at build time, not read when the server starts. The RUM clientToken is designed to be public. Never put a server secret in a NEXT_PUBLIC_ variable.
Step 3: Verify. Open the application in a browser, then check RUM for the session. Open a request from the session's resources and follow it to the server trace in APM → Traces.
What gets linked: Requests made with fetch or XHR are linked to server traces: API calls, Server Actions, and the RSC requests that Next.js makes on client-side navigation. The first HTML document request of a page load is made by the browser before any JavaScript runs, so it can't carry a traceparent header and is not linked.
Already using the Datadog RUM SDK
If the application already uses @datadog/browser-rum, you can keep it and send its data to KubeSense instead of Datadog. The SDK's proxy option sends RUM events, Session Replay and browser logs to the KubeSense RUM endpoint. The application code stays the same, including setUser, setGlobalContextProperty and startSessionReplayRecording.
Step 1: Create a RUM application in KubeSense. Go to Settings → RUM Application Management and copy the application's applicationId and clientToken. If you set allowed origins on the application, include every domain the site runs on. KubeSense rejects requests from other origins with 403.
Step 2: Point the SDK at KubeSense. Replace the Datadog application ID and client token with the KubeSense values, and add proxy:
// pages/_app.tsx (Pages Router), or instrumentation-client.ts (Next.js 15.3+)
import { datadogRum } from '@datadog/browser-rum';
datadogRum.init({
applicationId: process.env.NEXT_PUBLIC_RUM_APPLICATION_ID!, // KubeSense application ID
clientToken: process.env.NEXT_PUBLIC_RUM_CLIENT_TOKEN!, // KubeSense client token
site: 'datadoghq.com', // required by the SDK; not used with proxy
proxy: 'https://<KUBESENSE_RUM_HOST>/api/v2/proxy',
service: 'storefront-web',
env: 'production',
version: process.env.NEXT_PUBLIC_APP_VERSION,
sessionSampleRate: 100,
sessionReplaySampleRate: 20,
trackUserInteractions: true,
trackResources: true,
trackLongTasks: true,
// Link browser requests to the OpenTelemetry server traces
allowedTracingUrls: [
{ match: (url: string) => url.startsWith(window.location.origin), propagatorTypes: ['tracecontext'] },
{ match: /https:\/\/api\.example\.com/, propagatorTypes: ['tracecontext'] },
],
});If the application also uses @datadog/browser-logs, give datadogLogs.init the same proxy and client token. See Datadog Browser Logs.
Step 3: Rebuild and deploy. NEXT_PUBLIC_ variables are fixed at build time, so restarting the server is not enough.
note: With proxy set, the SDK sends data only to KubeSense. It can't send to Datadog at the same time.
Use tracecontext in propagatorTypes. OpenTelemetry services read the W3C traceparent header and ignore the datadog headers. Don't match every URL (match: () => true): third-party APIs then receive trace headers, and some reject them in their CORS preflight.
Alternative: OpenTelemetry web SDK (traces only)
The OpenTelemetry web SDK creates spans in the browser for fetch and XHR calls and for user interactions (clicks, submits). Spans appear in APM → Traces, in the same trace as the server spans. It doesn't collect Web Vitals, sessions or errors. Use it when you want only browser-to-server traces, and not RUM.
OpenTelemetry for the browser is experimental: The OpenTelemetry project marks its browser instrumentation as experimental. It adds about 60 to 100 KB (minified) to your bundle and exports spans from every page view unless you sample.
Step 1: Install the packages
npm install --save @opentelemetry/api @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base \
@opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/instrumentation \
@opentelemetry/instrumentation-fetch @opentelemetry/instrumentation-xml-http-request \
@opentelemetry/instrumentation-user-interaction @opentelemetry/context-zoneStep 2: Forward browser spans through your Next.js server
The KubeSense trace endpoint (:33443) doesn't send CORS headers, so browsers can't post to it directly. It is also an internal endpoint that shouldn't be exposed to the internet. Add a route handler that receives spans from the browser and forwards them from the server:
// app/api/otel/v1/traces/route.ts
const TARGET =
process.env.KUBESENSE_TRACES_URL ??
'http://kubesense-kubecol.kubesense.svc.cluster.local:33443/v1/traces';
const MAX_BYTES = 1_000_000;
export async function POST(req: Request) {
const body = await req.arrayBuffer();
if (body.byteLength > MAX_BYTES) {
return new Response(null, { status: 413 });
}
const res = await fetch(TARGET, {
method: 'POST',
headers: { 'Content-Type': req.headers.get('content-type') ?? 'application/json' },
body,
cache: 'no-store',
});
return new Response(null, { status: res.status });
}The browser now posts to its own origin, so no CORS configuration is needed, and ad blockers that block telemetry domains don't drop the spans. If trace authentication is enabled on your KubeSense, add the Authorization header here on the server, never in browser code. Because this route is public, protect it like any other public endpoint, for example with your ingress rate limits.
Using a collector instead: If you already expose an OpenTelemetry Collector publicly over HTTPS, the browser can post to it directly. Allow your site's origin on the collector's OTLP HTTP receiver:receivers: otlp: protocols: http: endpoint: 0.0.0.0:4318 cors: allowed_origins: - https://www.example.com allowed_headers: ['*']
Step 3: Create the browser setup
// instrumentation-client.ts (Next.js 15.3+), or a 'use client' module as shown for RUM
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor, TraceIdRatioBasedSampler, ParentBasedSampler } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';
import { ZoneContextManager } from '@opentelemetry/context-zone';
const provider = new WebTracerProvider({
resource: resourceFromAttributes({
'service.name': 'storefront-web',
'kubesense.cluster': '<CLUSTER_NAME>',
'kubesense.env_type': 'k8s',
}),
// Keep 10% of page traces; adjust to your traffic
sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(0.1) }),
spanProcessors: [
new BatchSpanProcessor(
new OTLPTraceExporter({ url: `${window.location.origin}/api/otel/v1/traces` }),
),
],
});
provider.register({ contextManager: new ZoneContextManager() });
// Requests that should carry the traceparent header: your own origin and your APIs.
// Don't use /.*/: third-party APIs reject the extra header in their CORS preflight.
const propagateTraceHeaderCorsUrls = [/https:\/\/api\.example\.com/];
const ignoreUrls = [/\/api\/otel\//];
registerInstrumentations({
instrumentations: [
new FetchInstrumentation({ propagateTraceHeaderCorsUrls, ignoreUrls }),
new XMLHttpRequestInstrumentation({ propagateTraceHeaderCorsUrls, ignoreUrls }),
new UserInteractionInstrumentation({ eventNames: ['click', 'submit'] }),
],
});Same-origin requests, such as route handlers and Server Actions, always get the traceparent header. List other domains in propagateTraceHeaderCorsUrls, and allow the traceparent header in those APIs' CORS configuration.
Step 4: Verify. Click through the application, then open APM → Traces and filter by storefront-web. A click span has the browser fetch span as its child, and the Next.js server spans below that.
Troubleshooting
| Symptom | Check |
|---|---|
| No server spans at all | instrumentation.ts must be in the project root or src/, not in app/ or pages/. On Next.js 13.2 to 14, experimental.instrumentationHook: true must be set |
Cannot find module '@opentelemetry/auto-instrumentations-node/register' | Standalone build with NODE_OPTIONS. Use the operator or the manual SDK. See Standalone output |
| HTTP spans appear, database spans don't | The library is bundled into the Next.js server build. Add it to serverExternalPackages |
| No spans from middleware | Middleware runs in the edge runtime. Only @vercel/otel traces it |
Edge routes fail with Module not found for fs or net | The Node.js SDK is loaded in the edge runtime. Load it only when process.env.NEXT_RUNTIME === 'nodejs' |
| Duplicate spans | Two SDKs run in one process, for example NODE_OPTIONS or the operator together with instrumentation.ts |
| Browser and server spans are in separate traces | The request URL isn't in allowedTracingUrls (RUM) or propagateTraceHeaderCorsUrls (OTel web SDK), or the propagator isn't tracecontext |
| Browser console shows CORS errors on API calls | The API rejects the traceparent header in its CORS preflight. Allow the header in that API's CORS configuration, or remove the domain from the tracing list |
| CORS error on the trace export request | The browser posts directly to :33443. Send it to the route handler instead |
ECONNREFUSED to port 33443 | Trace ingestion is not enabled on KubeSense, or the port is not reachable. See Instrumentation |
For more export detail, set OTEL_LOG_LEVEL=debug (Node.js SDK) or NEXT_OTEL_VERBOSE=1 temporarily.