All Articles
CategoryFull-Stack
Reading Time
11 min read
Published
2026-10-26
Word Count
2,703words

Grab a coffee — this one is a deep dive!

Node.js Observability: OpenTelemetry + Sentry Setup

Summary

A Node.js OpenTelemetry + Sentry observability setup: the OTel core inside @sentry/node 11, the Next.js instrumentation.ts split, Prisma spans, sampling, and alert strategy.

  • @sentry/node 11.0.0 is now on npm latest — the OpenTelemetry integration is no longer a hidden SDK detail but a deliberate, three-option mode (Sentry-only, enableOpenTelemetrySetup:true, or your own OTel pipeline).
  • In Next.js, instrumentation.ts is the root file; instrumentation-client.ts (browser), sentry.server.config.ts (Node), and sentry.edge.config.ts (edge) each run in a separate runtime.
  • If you run your own OTel pipeline, feed the URL + auth header config that Sentry.getOtlpTracesEndpoint(dsn) returns directly to OTLPTraceExporter; enableOpenTelemetrySetup already defaults to false on @sentry/node, while on Next.js/SvelteKit you have to set it to false explicitly — otherwise every operation produces two separate, never-merging span pipelines.
  • On a Sentry-native setup, Prisma is instrumented out of the box; with your own OTel pipeline, register PrismaInstrumentation from the @prisma/instrumentation package and the span tree descends from prisma:client:operation into prisma:engine:db_query.
Node.js Observability: OpenTelemetry + Sentry Setup

On Node.js, "error tracking" and "observability" are no longer the same thing. To see which Prisma query slowed a request, or which outbound API call timed out, you need the trace/log/metric trio — not just Sentry's classic error-capture mode. This post covers a Node.js OpenTelemetry + Sentry observability setup end to end, with real code: the OpenTelemetry core inside @sentry/node 11.x, the Next.js instrumentation.ts split, Prisma spans, sampling, and alert strategy.

💡 Pro Tip: Before flipping the enableOpenTelemetrySetup flag, check whether the project already has an OTel pipeline — leaving both on at the same time produces two separate spans per operation, and the traces never merge.

Table of Contents

The 2026 Observability Minimum (Trace + Log + Metric)

Being "observable" in a modern Node.js service means collecting three signals together: traces (a request's journey through the system, made of spans), logs (structured event records), and metrics (counters, histograms, time-series data). Sentry historically started with error capture; OpenTelemetry has collected these three signals under a vendor-neutral standard from the start. In 2026 the intersection of these two worlds is essentially mandatory: an error message alone doesn't show why a request was slow — the span tree does.

Why This Changed Now

As of 2026-09-23, the npm latest tag for @sentry/node is 11.0.0, confirmed by the 11.0.0 release page on the getsentry/sentry-javascript GitHub repo. In this major release, OpenTelemetry is no longer a hidden background detail in the SDK — it's a layer you configure directly: the orchestrion-based channel instrumentation that was opt-in in v10 (instead of import-in-the-middle) is now the default, turning on instrumentation at build and run time — why tracing now works on targets like Vercel/Netlify. Span streaming is now default, and the minimum supported Node version rose to 20.19.0 (the engines field of @sentry/node@11.0.0: >=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0). Check your own version with node -v; if the output falls in one of these ranges, you're compatible.

By contrast, @opentelemetry/sdk-node is still on 0.222.0, within the 0.x band — even though OpenTelemetry JS's trace API/SDK spec is considered stable, the package version hasn't crossed 1.0. This matters: while Sentry jumped a major version (10 to 11), OTel packages are still numbered 0.x — don't read that as a mismatch; the two projects simply move on separate cadences.

The OpenTelemetry Core Inside @sentry/node 11

The biggest architectural change in @sentry/node 11.0.0 is that OpenTelemetry is no longer a hidden implementation detail — it's a selectable mode. The SDK supports three scenarios:

Approach
What It Does
When To Use It
Sentry only (default)
OTel never gets involved; Sentry does its own error/performance capture
No other tracing system exists in the project
enableOpenTelemetrySetup: true
Sentry sets up its own OTel-compatible tracer provider; spans opened with @opentelemetry/api turn into native Sentry spans
You'll only use Sentry's dashboard, but want to instrument with the OTel API
enableOpenTelemetrySetup: false + your own pipeline
You set up your own NodeTracerProvider/NodeSDK; Sentry only attaches errors/logs to the active span
You're already sending traces to an OTel collector like Grafana/Datadog/Jaeger

The migration doc clearly separates these three under "Choosing an OpenTelemetry setup" — decide based on the project's existing infrastructure, and don't leave both on at once.

In the simple scenario (using Sentry's own OTel-compatible setup), the install starts like this:

bash
1npm install @sentry/node --save
ts
1// sentry.server.config.ts
2import * as Sentry from "@sentry/node";
3 
4Sentry.init({
5 dsn: process.env.SENTRY_DSN,
6 enableOpenTelemetrySetup: true,
7 tracesSampleRate: 0.2,
8 dataCollection: {
9 // explicitly turn this category off to preserve v10 behavior
10 databaseQueryData: false,
11 },
12});

Watch dataCollection.databaseQueryData. Per the migration doc, sendDefaultPii was removed; dataCollection replaces it, setting each category individually — left unset, it collects broader data than v10: databaseQueryData defaulted to false in v10, true in v11. Since query content (potential PII) is now collected by default, I set the field to false above to preserve the old behavior.

The Split Between instrumentation.ts and instrumentation-client.ts

The most confusing part of setting up Sentry in Next.js is knowing which file runs in which runtime. The official setup tool separates this automatically:

bash
1npx @sentry/wizard@latest -i nextjs

The wizard creates three separate init files and wires them into a single root file:

  • instrumentation-client.ts: Runs in the browser, initializes the client-side Sentry SDK.
  • sentry.server.config.ts: Runs in the Node.js runtime, sets up server-side error/trace capture.
  • sentry.edge.config.ts: Runs in the edge runtime (middleware, edge routes).
  • instrumentation.ts: Next.js's own framework hook — the root file that imports the three files above depending on the runtime.

Per the migration doc, enableOpenTelemetrySetup defaults to false in most server SDKs, and true in @sentry/nextjs and @sentry/sveltekit. Moving to your own OTel pipeline in Next.js requires explicitly setting this flag to false — otherwise Sentry registers its own tracer provider, and you land in the second setup instead of the third.

Wiring Existing OTel Spans to Sentry

If you already run your own OpenTelemetry pipeline (your own NodeTracerProvider), add Sentry not as a parallel tracing system, but as an error/log source attached to that pipeline. The official way: getOtlpTracesEndpoint(), which derives Sentry's OTLP address and auth headers from your DSN and returns the config for OTLPTraceExporter — not a URL string:

bash
1npm install @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http
ts
1// otel-setup.ts
2import * as Sentry from "@sentry/node";
3import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
4import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
5import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
6 
7const dsn = process.env.SENTRY_DSN!;
8const endpoint = Sentry.getOtlpTracesEndpoint(dsn);
9 
10if (!endpoint) {
11 throw new Error("Could not parse the Sentry DSN");
12}
13 
14const provider = new NodeTracerProvider({
15 spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter(endpoint))],
16});
17provider.register();
18 
19Sentry.init({
20 dsn,
21 enableOpenTelemetrySetup: false,
22 integrations: [Sentry.openTelemetryIntegration()],
23});

In this scenario we don't set tracesSampleRate — the sampling decision is now the responsibility of the OTel sampler; Sentry just attaches errors/logs to spans that are already being produced.

Wiring Prisma/Postgres Queries to Spans

Seeing that a request is slow isn't enough — you need to know which Prisma query caused it. On the Sentry-native path (setups one and two), Prisma is instrumented out of the box via prismaIntegration; the setup below only applies to your own OTel pipeline, using Prisma's official OTel integration, @prisma/instrumentation:

bash
1npm install @prisma/instrumentation
ts
1// otel-setup.ts (in addition to the provider setup above)
2import { PrismaInstrumentation, registerInstrumentations } from "@prisma/instrumentation";
3 
4registerInstrumentations({
5 instrumentations: [new PrismaInstrumentation()],
6});

With this active, the span tree produces: prisma:client:operation (a method you call, like prisma.user.findMany()) opens prisma:engine:db_query (the actual SQL query) beneath it (with spans like prisma:engine:query in between). Prisma's docs note this instrumentation had to be enabled as a preview feature via previewFeatures = ["tracing"] for versions 4.2.0–6.1.0, and works directly in later versions — check the project's Prisma version in package.json and set the schema flag accordingly.

Sampling and Cost Control

Sentry has three separate sampling concepts, and mixing them up either sends too much data and inflates cost, or leaves you seeing no traces at all:

Setting
What It Samples
Default
Note
sampleRate
Error events
1 (all errors)
Works independently of traces
tracesSampleRate
Performance traces
Off if unset
No trace data is sent at all if unset
tracesSampler
Performance traces (conditional)
—
Dynamic decision based on operation name/context

With your own OTel pipeline (the enableOpenTelemetrySetup: false setup above), don't set tracesSampleRate — sampling happens at the OTel Sampler layer, before data reaches the OTLP exporter. Mixing up the two mechanisms and applying limits on both sides makes it untraceable where data actually gets cut. Keep it simple: Sentry-native path → Sentry's tracesSampleRate is the sole authority; own OTel pipeline → the OTel sampler is.

On the Sentry-native path, for a conditional decision instead of a fixed rate, use tracesSampler. A common pattern excludes noisy endpoints like health checks entirely:

ts
1import * as Sentry from "@sentry/node";
2 
3Sentry.init({
4 dsn: process.env.SENTRY_DSN,
5 enableOpenTelemetrySetup: true,
6 tracesSampler: ({ attributes, inheritOrSampleWith }) =>
7 inheritOrSampleWith(attributes?.["url.path"] === "/health" ? 0 : 0.2),
8});

The trap is specific to v11: since tracesSampler runs before the span starts, Express hasn't matched the route yet — the http.server span's name is just GET, not GET /health. As the migration doc warns, name-based rules fail silently: no error, no warning, just unexpected quota usage. Check the url.path attribute, not the name.

Setting Up Alerts: What to Alarm On, What Never To

After collecting traces and spans, the next step is turning that data into a real alert. Sentry's alert model has three parts: a trigger (when the condition is checked), a filter (which data it applies to), and an action (what happens when met — email, Slack, webhook). Official example scenarios include downtime detection, traffic spikes, and error-threshold breaches.

The practical rule: alarm on thresholds that affect users, not every span. An instant alert for one slow Prisma query is noise; instead, trigger on a crossed p95 latency threshold or a sustained error rate. Don't alarm on one-off, self-correcting anomalies — like a brief cold-start delay during a deploy. Add a time window (say, 5 minutes) and a minimum occurrence count to filter that noise out.

Setting It Up in a Self-Hosted Scenario

Running Sentry on your own server (not SaaS)? The one thing to check before using @sentry/node 11.0.0 is your self-hosted Sentry version: the migration doc states this SDK requires self-hosted Sentry 26.4.2 or later. Pairing an older self-hosted version with the 11.x SDK can cause span/data schema mismatches.

Setup steps don't differ from SaaS — the only difference is that dsn points to your own server's ingest endpoint, and getOtlpTracesEndpoint() uses this same custom DSN:

ts
1const dsn = "https://<public_key>@sentry.your-own-server.com/<project_id>";
2 
3Sentry.init({
4 dsn,
5 enableOpenTelemetrySetup: true,
6 tracesSampleRate: 0.1,
7});

On self-host, verifying the Sentry server version before upgrading is the cheapest check to prevent broken data flow after you update the SDK:

bash
1docker compose exec web sentry --version

If the output is below 26.4.2, update your self-hosted Sentry setup first — otherwise the data schema @sentry/node 11.x sends may not be fully processed server-side.

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?

Reader Reward

We put together a short checklist you can review before taking this setup to production. Check off each item one by one to make sure your setup is free of the three critical mistakes: double pipeline, sampling confusion, and PII leakage.

FAQ

How do you use OpenTelemetry together with Sentry in Node.js?

Sentry v11 offers three approaches: Sentry alone (default, OTel never involved), Sentry setting up its own lightweight OTel-compatible tracer provider with enableOpenTelemetrySetup: true, or your own OTel pipeline (NodeTracerProvider/NodeSDK) with errors/logs attached via Sentry.openTelemetryIntegration(). Which one depends on whether the project already has an OTel collector.

How do I send my existing OTel traces to Sentry?

Add a BatchSpanProcessor to your own NodeTracerProvider; give its OTLPTraceExporter the URL + auth header config returned by Sentry.getOtlpTracesEndpoint(dsn), call provider.register(), then run Sentry.init({ dsn, enableOpenTelemetrySetup: false, integrations: [Sentry.openTelemetryIntegration()] }). Don't set tracesSampleRate — the sampling decision stays with the OTel sampler.

How do you set up the instrumentation file in Next.js?

Run npx @sentry/wizard@latest -i nextjs; the wizard creates instrumentation-client.ts (browser), sentry.server.config.ts (Node.js), and sentry.edge.config.ts (edge runtime) files, and the root file that wires them into Next.js is instrumentation.ts. In the Next.js SDK, enableOpenTelemetrySetup defaults to true.

Sentry or an OTel collector — which do you need?

They aren't rivals — they're complementary. Sentry gives you error/log/cron tracking plus a ready-made dashboard; OpenTelemetry is a vendor-neutral standard for producing traces/metrics/logs. Sentry-only projects don't need OTel (the default mode). Projects already sending data to an OTel collector (Grafana, Datadog, Jaeger) should add Sentry as an error/log source attached to the existing trace via openTelemetryIntegration(), not as a parallel pipeline — leaving both on in tracing mode produces two spans per operation that never merge.

Which Node.js version is required?

The minimum supported version for @sentry/node 11.0.0 is 20.19.0. The package's engines field specifies the range >=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0 — Node versions outside this range may produce warnings during setup or behave unexpectedly.

Conclusion

Setting up observability in Node.js is no longer just calling init on one SDK; your chosen integration path brings three different architectural decisions with it. On Next.js, Next.js 16.3 Removes runtime='edge': The Return to Node helps you understand which runtimes instrumentation.ts runs in; for caching and navigation, see Next.js 16.3 Instant Navigations and Cache Components Guide.

Backend framework choice affects this too; in a serverless setup, Hono.js: Serverless Web Framework Production Guide 2026 is a good start for instrumentation points fitting your architecture. For the error/logging contract in API design, see REST API Design Principles: Resource, Error, Pagination. For the same discipline on mobile, iOS Performance Monitoring & Observability Guide and iOS Crash Reporting and Analytics: Production Quality Guide show the mobile counterpart.

Once you finish the setup, trace a single request end to end and confirm you see the expected span count in the trace view — that's the most reliable proof the setup actually works.

Sources

Tags

#Node.js#OpenTelemetry#Sentry#Observability#Prisma#Next.js#Backend#DevOps
Muhittin Camdali

Muhittin Çamdalı

Senior Mobile Engineer

Senior Mobile Engineer with 12+ years of experience. I build mobile products with iOS, Flutter and React Native, working full-stack and AI-native.

iOS Development News

Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.

Your subscription starts when you open the link in the confirmation email and press “Confirm my subscription”. The newsletter keeps open/click statistics; you can unsubscribe anytime with one click. Privacy

Share