Skip to main content
Sentry’s JavaScript SDK v8+ is built on OpenTelemetry, the industry-standard observability framework. This enables seamless integration with OpenTelemetry tooling and standardized instrumentation.

Why OpenTelemetry?

OpenTelemetry (OTel) provides:
  • Standardized instrumentation across languages and frameworks
  • Vendor-neutral data collection
  • Automatic instrumentation for common libraries
  • Distributed tracing across services
  • Rich ecosystem of integrations

Automatic OpenTelemetry Integration

In SDK v8+, OpenTelemetry is used automatically:
import * as Sentry from '@sentry/node';

// Sentry initialization automatically sets up OpenTelemetry
Sentry.init({
  dsn: '__DSN__',
  tracesSampleRate: 1.0,
  // OpenTelemetry instrumentation happens automatically
});

import express from 'express';
const app = express();

// Express routes are automatically instrumented via OpenTelemetry
app.get('/users', async (req, res) => {
  // This is automatically traced
  const users = await db.query('SELECT * FROM users');
  res.json(users);
});
In Node.js SDK v8+, you MUST initialize Sentry before any other imports for OpenTelemetry auto-instrumentation to work.

Available Auto-Instrumentations

The SDK automatically includes OpenTelemetry instrumentations for:

HTTP & Networking

  • httpIntegration - HTTP/HTTPS requests
  • nativeNodeFetchIntegration - Native fetch API

Frameworks

  • expressIntegration - Express.js
  • fastifyIntegration - Fastify
  • hapiIntegration - Hapi
  • nestIntegration - Nest.js

Databases

  • prismaIntegration - Prisma ORM
  • mongoIntegration - MongoDB
  • mongooseIntegration - Mongoose
  • mysqlIntegration - MySQL
  • mysql2Integration - MySQL2
  • postgresIntegration - PostgreSQL
  • redisIntegration - Redis (ioredis)

GraphQL

  • graphqlIntegration - GraphQL operations

Custom OpenTelemetry Instrumentations

Add custom OpenTelemetry instrumentations:
import * as Sentry from '@sentry/node';
import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-generic-pool';

Sentry.init({
  dsn: '__DSN__',
  openTelemetryInstrumentations: [
    new GenericPoolInstrumentation(),
  ],
});

Common Third-Party Instrumentations

import { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs';
import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino';
import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston';

Sentry.init({
  dsn: '__DSN__',
  openTelemetryInstrumentations: [
    new KafkaJsInstrumentation(),
    new PinoInstrumentation(),
    new WinstonInstrumentation(),
  ],
});

Working with OpenTelemetry API

Using OpenTelemetry Spans

You can use both Sentry and OpenTelemetry APIs:
import { trace } from '@opentelemetry/api';
import * as Sentry from '@sentry/node';

// Sentry API (recommended)
Sentry.startSpan({ name: 'my-operation' }, () => {
  // Work here
});

// OpenTelemetry API (also works)
const tracer = trace.getTracer('my-app');
tracer.startActiveSpan('my-operation', (span) => {
  // Work here
  span.end();
});

Getting Active Span

import { getActiveSpan } from '@sentry/node';
import { trace } from '@opentelemetry/api';

// Sentry API
const sentrySpan = getActiveSpan();

// OpenTelemetry API
const otelSpan = trace.getActiveSpan();

Adding Span Attributes

import { startSpan } from '@sentry/node';

startSpan({ name: 'process-order' }, (span) => {
  // Sentry-style
  span.setAttribute('order.id', orderId);
  span.setAttribute('order.total', total);
  
  // OpenTelemetry semantic conventions
  span.setAttribute('db.system', 'postgresql');
  span.setAttribute('db.operation', 'SELECT');
});

Semantic Conventions

Use OpenTelemetry semantic conventions for consistency:

HTTP Spans

startSpan(
  {
    name: 'GET /api/users',
    op: 'http.server',
    attributes: {
      'http.method': 'GET',
      'http.route': '/api/users',
      'http.scheme': 'https',
      'http.target': '/api/users?page=1',
      'http.status_code': 200,
    },
  },
  () => {
    // Handle request
  }
);

Database Spans

startSpan(
  {
    name: 'SELECT users',
    op: 'db.query',
    attributes: {
      'db.system': 'postgresql',
      'db.operation': 'SELECT',
      'db.statement': 'SELECT * FROM users WHERE id = $1',
      'db.name': 'myapp_db',
    },
  },
  async () => {
    return await db.query('SELECT * FROM users WHERE id = $1', [userId]);
  }
);

RPC/GraphQL Spans

startSpan(
  {
    name: 'getUser',
    op: 'graphql.query',
    attributes: {
      'graphql.operation.name': 'getUser',
      'graphql.operation.type': 'query',
    },
  },
  () => {
    // Execute query
  }
);

Custom Context Propagation

Control how trace context is propagated:
import { continueTrace } from '@sentry/node';

// Extract trace context from headers
const sentryTrace = req.headers['sentry-trace'];
const baggage = req.headers['baggage'];

continueTrace({ sentryTrace, baggage }, () => {
  // This continues the trace
  Sentry.startSpan({ name: 'handle-request' }, () => {
    // Work here
  });
});

W3C Trace Context

Sentry supports W3C Trace Context:
import { continueTrace } from '@sentry/node';

// W3C traceparent header
const traceparent = req.headers['traceparent'];
const tracestate = req.headers['tracestate'];

continueTrace({
  sentryTrace: traceparent,
  baggage: tracestate,
}, () => {
  // Continue trace
});

Distributed Tracing

Connect traces across services:

Service A (Node.js)

import * as Sentry from '@sentry/node';
import fetch from 'node-fetch';

Sentry.init({
  dsn: '__DSN__',
  tracePropagationTargets: ['https://service-b.example.com'],
});

app.get('/api/data', async (req, res) => {
  // Automatically propagates trace to service-b
  const response = await fetch('https://service-b.example.com/data');
  const data = await response.json();
  res.json(data);
});

Service B (Node.js)

import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: '__DSN__',
  // Automatically continues traces from service-a
});

app.get('/data', (req, res) => {
  // This span is part of the distributed trace
  res.json({ message: 'data' });
});

OpenTelemetry Exporters

Export to multiple backends:
import * as Sentry from '@sentry/node';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';

// Custom OpenTelemetry setup
const provider = new NodeTracerProvider();

// Add Jaeger exporter
provider.addSpanProcessor(
  new BatchSpanProcessor(new JaegerExporter())
);

provider.register();

// Initialize Sentry (uses the registered provider)
Sentry.init({
  dsn: '__DSN__',
  skipOpenTelemetrySetup: true, // Skip Sentry's OTel setup
});
When skipOpenTelemetrySetup: true, you must configure OpenTelemetry yourself. Automatic instrumentations won’t work.

Filtering OpenTelemetry Spans

Control which spans are created:
import { httpIntegration } from '@sentry/node';

Sentry.init({
  dsn: '__DSN__',
  integrations: [
    httpIntegration({
      tracing: {
        // Don't create spans for certain URLs
        shouldCreateSpanForRequest: (url) => {
          return !url.includes('/health') && !url.includes('/metrics');
        },
      },
    }),
  ],
});

Performance Optimization

Disable Unnecessary Instrumentations

import { httpIntegration } from '@sentry/node';

Sentry.init({
  dsn: '__DSN__',
  integrations: [
    // Disable HTTP spans if not needed
    httpIntegration({ spans: false }),
  ],
});

Sampling

Control sampling at the OpenTelemetry level:
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';

Sentry.init({
  dsn: '__DSN__',
  tracesSampleRate: 0.1, // Sentry-level sampling
  
  // Or use custom OTel sampler
  // (requires skipOpenTelemetrySetup: true)
});

Accessing OpenTelemetry Context

import { context, trace } from '@opentelemetry/api';
import { getActiveSpan } from '@sentry/node';

// Get active span
const span = getActiveSpan();

// Get span context
if (span) {
  const spanContext = span.spanContext();
  console.log('Trace ID:', spanContext.traceId);
  console.log('Span ID:', spanContext.spanId);
  console.log('Trace flags:', spanContext.traceFlags);
}

// Access OpenTelemetry context
const ctx = context.active();
const otelSpan = trace.getSpan(ctx);

Best Practices

Sentry APIs (startSpan, getActiveSpan) are easier and integrate better:
// ✅ Recommended
Sentry.startSpan({ name: 'operation' }, () => {});

// ❌ Less ideal
const tracer = trace.getTracer('app');
tracer.startActiveSpan('operation', (span) => {
  span.end();
});
Use OpenTelemetry semantic conventions for consistency:
span.setAttribute('http.method', 'GET');
span.setAttribute('http.status_code', 200);
span.setAttribute('db.system', 'postgresql');
See OpenTelemetry Semantic Conventions
Always initialize Sentry before other imports:
// ✅ Correct
import * as Sentry from '@sentry/node';
Sentry.init({ dsn: '__DSN__' });
import express from 'express';

// ❌ Wrong
import express from 'express';
import * as Sentry from '@sentry/node';
Sentry.init({ dsn: '__DSN__' });
Either use Sentry’s auto-setup OR custom OpenTelemetry setup, not both:
// ✅ Auto-setup (recommended)
Sentry.init({ dsn: '__DSN__' });

// ✅ Custom setup
// Set up OpenTelemetry manually
Sentry.init({
  dsn: '__DSN__',
  skipOpenTelemetrySetup: true,
});

// ❌ Don't mix
// Set up OpenTelemetry manually
Sentry.init({ dsn: '__DSN__' }); // Also tries to setup OTel

Troubleshooting

Symptoms: No performance data in SentrySolutions:
  1. Ensure tracesSampleRate is set
  2. Initialize Sentry before all imports
  3. Check console for errors
  4. Verify instrumentations are loaded
Symptoms: Same operation shows multiple spansSolution: You may be using both Sentry and OTel APIs. Pick one:
// Use Sentry API
Sentry.startSpan({ name: 'op' }, () => {});

// OR use OTel API (not both)
trace.getTracer('app').startActiveSpan('op', (s) => s.end());
Symptoms: Distributed traces disconnectedSolutions:
  1. Set tracePropagationTargets
  2. Ensure services use same trace headers
  3. Check CORS allows trace headers
Solutions:
  1. Lower tracesSampleRate
  2. Disable unused instrumentations
  3. Use shouldCreateSpanForRequest to filter

Complete Example

// server.ts
import * as Sentry from '@sentry/node';
import { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs';

// Initialize Sentry FIRST
Sentry.init({
  dsn: '__DSN__',
  environment: process.env.NODE_ENV,
  tracesSampleRate: 0.1,
  
  // Add custom OpenTelemetry instrumentations
  openTelemetryInstrumentations: [
    new KafkaJsInstrumentation(),
  ],
  
  // Configure integrations
  integrations: [
    Sentry.httpIntegration({
      tracing: {
        shouldCreateSpanForRequest: (url) => {
          return !url.includes('/health');
        },
      },
    }),
    Sentry.prismaIntegration(),
    Sentry.graphqlIntegration(),
  ],
  
  // Propagate to other services
  tracePropagationTargets: [
    'localhost',
    /^https:\/\/api\.example\.com/,
  ],
});

// Now import other modules
import express from 'express';
import { PrismaClient } from '@prisma/client';

const app = express();
const prisma = new PrismaClient();

app.get('/users/:id', async (req, res) => {
  // Automatically traced by Express integration
  const userId = req.params.id;
  
  // Custom span
  const user = await Sentry.startSpan(
    {
      name: 'fetch-user-with-posts',
      op: 'db.query',
      attributes: {
        'user.id': userId,
      },
    },
    async (span) => {
      // Prisma query - automatically instrumented
      const user = await prisma.user.findUnique({
        where: { id: userId },
        include: { posts: true },
      });
      
      span.setAttribute('posts.count', user.posts.length);
      
      return user;
    }
  );
  
  res.json(user);
});

app.listen(3000);

Migration from OpenTelemetry

If you’re already using OpenTelemetry:
// Before (pure OpenTelemetry)
import { trace } from '@opentelemetry/api';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';

const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(new JaegerExporter()));
provider.register();

const tracer = trace.getTracer('my-app');

// After (with Sentry)
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: '__DSN__',
  // Sentry handles OpenTelemetry setup
});

// Use Sentry APIs (simpler)
Sentry.startSpan({ name: 'operation' }, () => {});

// Or continue using OTel APIs (still works)
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('my-app');

Resources

Next Steps

Custom Integrations

Build custom integrations

Performance Monitoring

Best practices for performance

Build docs developers (and LLMs) love