Back to Blog Home

Contents

Share

Share on Twitter
Share on Bluesky
Share on HackerNews
Share on LinkedIn

Send your existing OpenTelemetry traces to Sentry

James W. image

James W. -

Send your existing OpenTelemetry traces to Sentry

Send your existing OpenTelemetry traces to Sentry

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:

  1. Click Settings in the left sidebar.

  2. Under the Organization section in the Settings sidebar, click Projects.

  3. Find your project in the list and click on it to open the project settings.

  4. In the project settings sidebar, click Client Keys (DSN) under the SDK Setup section.

  5. 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 sentry sentry_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:

Click to Copy
git clone https://github.com/getsentry/otlp-tracing-sentry.git
cd otlp-tracing-sentry
npm install

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

Click to Copy
cp .env.example .env

Now edit .env and add your Sentry OTLP credentials from the previous step:

Click to Copy
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=3000

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

Click to Copy
npm start

You should see the following:

Click to Copy
OpenTelemetry tracing initialized
Service: book-recommendation-service
Book Recommendation Service running on http://localhost:3000

Generate a trace

In a new terminal window, send a request to create a book recommendation:

Click to Copy
curl -X POST http://localhost:3000/recommend \
  -H "Content-Type: application/json" \
  -d '{"userId": "user123"}'

You'll get a JSON response with book recommendations:

Click to Copy
{
  "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:

  1. Go to your Sentry project.

  2. 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: SELECT

  • category: db

  • db.operation: SELECT

  • db.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:

Click to Copy
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:

Custom spans

The index.js file imports instrument.js first, then creates custom spans for business operations:

Click to Copy
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:

Click to Copy
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:

Click to Copy
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:

Click to Copy
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:

Click to Copy
// 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:

Click to Copy
// instrument.js
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
});

Creating spans

OTLP:

Click to Copy
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:

Click to Copy
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.

Click to Copy
// 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 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.

Click to Copy
// 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 traces FAQs

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
© 2026 • Sentry is a registered Trademark of Functional Software, Inc.