Send your existing OpenTelemetry traces to Sentry

James W. -

You spent months instrumenting your app with OpenTelemetry. The idea of ripping it out to adopt a new observability backend is not an option. Sentry's OTLP endpoint means you don't have to, two environment variables is all you need and your existing traces start showing up in Sentry's trace explorer.
Sentry's OTLP support is currently in open beta. This means you can start using it today, but there are some known limitations we'll cover later.
Why OTLP: keep your instrumentation, just change the destination
The main advantage of using OpenTelemetry is that your instrumentation stays vendor-neutral. Your instrumentation code uses OpenTelemetry's standard APIs, and OTLP (the protocol) sends that data to any compatible backend. This means you can switch observability backends anytime by changing a few configuration lines. This is particularly useful if you:
Are already heavily invested in the OpenTelemetry ecosystem
Want to keep your instrumentation flexible or already use OpenTelemetry in other parts of your stack
If you're starting from scratch and only need Sentry, the native Sentry SDK provides full support for all Sentry features (including span events, session replay, and profiling), while OTLP is still in beta and has some limitations. We'll compare both approaches later in this guide.
Prerequisites
Before we start, you need:
A Sentry account (the free tier works fine)
Node.js 18 or later installed
Basic familiarity with Express.js
If you don't have a Sentry project yet, create one now. Select Express as the platform when prompted. You can skip the DSN setup instructions because you'll use the OTLP endpoint instead.
Get your Sentry OTLP credentials
Sentry provides dedicated OTLP endpoints for each project. You can find them as follows:
Click Settings in the left sidebar.
Under the Organization section in the Settings sidebar, click Projects.
Find your project in the list and click on it to open the project settings.
In the project settings sidebar, click Client Keys (DSN) under the SDK Setup section.
Select the OpenTelemetry tab. Click the Expand button to see all OTLP endpoint values.
Sentry UI showing Settings > Client Keys (DSN) > OpenTelemetry tab with OTLP endpoints visible
Keep this tab open. We'll use the following values in the next step:
OTLP Traces Endpoint: The URL where Sentry receives traces (which looks like
https://o{ORG_ID}.ingest.us.sentry.io/api/{PROJECT_ID}/integration/otlp/v1/traces)OTLP Traces Endpoint Headers: The authentication header value. Copy only the value after
x-sentry-auth=(which looks like sentrysentry_key={YOUR_PUBLIC_KEY})
Connect your OpenTelemetry app to Sentry
We'll use a sample book recommendation service that already has OpenTelemetry tracing instrumentation. You don't need to change your instrumentation code. Just point it at Sentry's OTLP endpoint.
Clone the starter app
Run the following commands to clone the book recommendation app:
git clone https://github.com/getsentry/otlp-tracing-sentry.git
cd otlp-tracing-sentry
npm installThis app includes:
OpenTelemetry SDK (already configured)
Custom tracing spans throughout the code
Multi-level trace instrumentation (database queries, API calls, and parallel operations)
Configure Sentry as the OTLP destination
Create a .env file in the project root:
cp .env.example .envNow edit .env and add your Sentry OTLP credentials from the previous step:
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://o{YOUR_ORG_ID}.ingest.us.sentry.io/api/{YOUR_PROJECT_ID}/integration/otlp/v1/traces
OTEL_EXPORTER_OTLP_TRACES_HEADERS=sentry
sentry_key={YOUR_PUBLIC_KEY}
OTEL_SERVICE_NAME=book-recommendation-service
PORT=3000Replace the placeholder values with your actual Sentry credentials. The OTEL_SERVICE_NAME will help you filter traces later in Sentry.
That's it. You've just connected OpenTelemetry to Sentry with two lines of configuration.
Generate a trace and watch it appear in Sentry
Start the application:
npm startYou should see the following:
OpenTelemetry tracing initialized
Service: book-recommendation-service
Book Recommendation Service running on http://localhost:3000Generate a trace
In a new terminal window, send a request to create a book recommendation:
curl -X POST http://localhost:3000/recommend \
-H "Content-Type: application/json" \
-d '{"userId": "user123"}'You'll get a JSON response with book recommendations:
{
"userId": "user123",
"userName": "Alice Johnson",
"recommendations": [
{
"bookId": 201,
"title": "Project Hail Mary",
"score": 0.95,
"availability": 6
}
]
}View the trace in Sentry
Now let's see what this looks like in the Sentry Traces view:
Go to your Sentry project.
Navigate to Explore in the left sidebar, then click Traces.
Sentry Traces page showing span samples
The page displays a list of span samples from your traces. Each row represents a span with its duration and description. Click on the Trace Samples tab to switch to viewing complete traces.
Trace Samples tab showing the expanded trace with all spans
Click on a trace to open the waterfall view. You'll see a multi-level trace showing the complete request flow, including nested operations, parallel operations, and how long each one takes.
Explore span attributes
Click on any span in the waterfall to see its attributes.
Trace waterfall view with span attributes panel
The waterfall view makes it easier to see where your app spends its time, instead of guessing which async call wandered off on its own.
For example, the getUserProfile span includes attributes like:
action:SELECTcategory:dbdb.operation:SELECTdb.system:postgresql
These attributes make your traces searchable. You can filter traces by user ID, database operations, or any custom attribute you add.
How the OpenTelemetry instrumentation works
Let's look at how the app creates these traces. Here's what's happening behind the scenes so you can reuse the same patterns in your own app.
OpenTelemetry SDK initialization
The instrument.js file sets up the OpenTelemetry SDK and configures the OTLP exporter:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
// Configure the OTLP trace exporter
const traceExporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
headers: {
'x-sentry-auth': process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS || '',
},
});
// Create SDK instance
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'book-recommendation-service',
}),
traceExporter,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();These are the key parts:
OTLPTraceExportersends traces to Sentry's OTLP endpoint.NodeSDKinitializes OpenTelemetry with automatic instrumentation.getNodeAutoInstrumentations()automatically traces HTTP requests, database calls, and other operations.
Custom spans
The index.js file imports instrument.js first, then creates custom spans for business operations:
import './instrument.js';
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('book-recommendation-service', '1.0.0');Here's how we create a span for the database query:
async function getUserProfile(userId) {
return tracer.startActiveSpan('getUserProfile', async (span) => {
span.setAttribute('db.system', 'postgresql');
span.setAttribute('db.operation', 'SELECT');
span.setAttribute('user.id', userId);
await delay(50);
const profile = {
userId,
name: 'Alice Johnson',
preferences: ['fiction', 'mystery', 'sci-fi']
};
span.end();
return profile;
});
}The startActiveSpan method creates a new span and makes it the "active" span. Any child spans created inside this function automatically become children of this span.
Nested spans
We can create nested operations by starting new spans within a parent span:
async function getReadingHistory(userId) {
return tracer.startActiveSpan('getReadingHistory', async (span) => {
span.setAttribute('db.system', 'postgresql');
span.setAttribute('user.id', userId);
await delay(60);
const history = [
{ bookId: 101, title: 'The Great Gatsby', rating: 5 },
{ bookId: 102, title: '1984', rating: 4 },
{ bookId: 103, title: 'To Kill a Mockingbird', rating: 5 }
]; // Get data
// Nested operation
const filtered = await tracer.startActiveSpan('filterRecentBooks', async (childSpan) => {
childSpan.setAttribute('books.count', history.length);
await delay(20);
const recent = history.slice(0, 2);
childSpan.end();
return recent;
});
span.end();
return filtered;
});
}This creates the nested structure we saw in the Sentry waterfall view.
Parallel operations
For operations that run concurrently, use Promise.all:
async function checkBookAvailability(bookIds) {
return tracer.startActiveSpan('checkBookAvailability', async (span) => {
const checks = await Promise.all([
tracer.startActiveSpan('checkWarehouse1', async (s) => {
s.setAttribute('warehouse.id', 'US-EAST-1');
await delay(40);
s.end();
return { warehouse: 'US-EAST-1', available: 2 };
}),
tracer.startActiveSpan('checkWarehouse2', async (s) => {
s.setAttribute('warehouse.id', 'US-WEST-1');
await delay(45);
s.end();
return { warehouse: 'US-WEST-1', available: 3 };
})
]);
span.end();
return checks;
});
}Sentry shows these spans running in parallel on the waterfall view, making it clear which operations we can optimize.
OTLP vs native Sentry SDK
If you're already on OpenTelemetry, stay there. If you're starting fresh and only using Sentry, use the native SDK — you'll get more features and less config. Here's how they differ in implementation.
Setup and configuration
OTLP:
// instrument.js
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
const traceExporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
headers: {
'x-sentry-auth': process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS
},
});
const sdk = new NodeSDK({
traceExporter,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();Native Sentry SDK:
// instrument.js
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
});Creating spans
OTLP:
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('my-service', '1.0.0');
async function getUserProfile(userId) {
return tracer.startActiveSpan('getUserProfile', async (span) => {
span.setAttribute('user.id', userId);
// Your code here
span.end();
return result;
});
}Native Sentry SDK:
import * as Sentry from '@sentry/node';
async function getUserProfile(userId) {
return Sentry.startSpan(
{
op: 'db.query',
name: 'getUserProfile',
attributes: { 'user.id': userId },
},
async () => {
// Your code here
return result;
}
);
}With OTLP, you must manually call span.end(). The native Sentry SDK automatically ends the span when the callback completes.
When to use OTLP
Use OpenTelemetry with OTLP if you:
Already have OpenTelemetry instrumentation in your codebase
Send traces to multiple observability backends
Need vendor-neutral instrumentation
Work with AI or LLM frameworks that use OpenTelemetry by default
Use the OpenTelemetry Collector for processing traces
When to use native Sentry
Use the native Sentry SDK if you:
Are starting from scratch without existing instrumentation
Use Sentry as your only observability backend
Need features that are currently limited in the OTLP beta (such as span events, full span link support, and searchable array attributes)
Want automatic integration with Sentry error tracking and session replay
Known limitations (open beta)
Sentry's OTLP support is in open beta, so a few things don't work yet. Here's what to watch out for.
Span events are dropped
OpenTelemetry span events are not supported. If your instrumentation adds events to spans, they will be dropped during ingestion.
// This event will be dropped
span.addEvent('cache-miss', { key: 'user:123' });If you need to track events, use span attributes or create separate spans.
Span links have limited support
Span links are ingested and displayed in the trace view, but you cannot search, filter, or aggregate by them. You can see the links when viewing a trace, but they won't appear in trace queries.
Array attributes have limited support
Array attributes work the same way as span links. Sentry ingests and displays them, but you cannot use them in search queries or aggregations.
// This array attribute will display but won't be searchable
span.setAttribute('book.genres', ['fiction', 'mystery', 'sci-fi']);If you need searchable arrays, consider using separate attributes or joining the array into a string.
Further reading
OpenTelemetry: Semantic conventions for standardized attribute names




