# Sentry SDKs for errors while OpenTelemetry owns tracing

> One tracing owner and the bridge that puts trace ids on errors: Sentry 10 and 11 for Node.js, OTLPIntegration for Python, sentry-go and sentry-java.

Many services already use a Sentry SDK for errors. Keep it — Belvador receives those errors as they are — and let OpenTelemetry own tracing. Two things are separate and both matter:

1. **One tracing owner.** Only OpenTelemetry creates spans. The Sentry SDK has tracing off: no `tracesSampleRate`, `traces_sample_rate` or `TracesSampleRate`, no Sentry performance integration.
2. **Errors carry the active span's ids.** Turning Sentry tracing off does not by itself make errors read OpenTelemetry's context. Each SDK has a small bridge for that; without it errors and traces are both stored but do not line up.

Keep your existing OpenTelemetry exporter pointed at Belvador. Sentry's examples that build an OTLP endpoint from the DSN send spans to Sentry; skip those parts.

## Belvador SDKs

`@belvador/*` packages send errors only and never create spans. Pass the OpenTelemetry API with the `otel` option and errors carry the active span's ids — tested in Node.js and Cloudflare Workers: [Link requests](https://belvador.dev/en/docs/sdk/tracing#with-opentelemetry).

## Node.js (`@sentry/node`)

**Sentry 11** (released September 2026) has a dedicated integration: it links events to the active OpenTelemetry context, sends no spans and leaves propagation to OpenTelemetry. Start your OpenTelemetry SDK first, then:

```js
import * as Sentry from "@sentry/node";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  enableOpenTelemetrySetup: false,
  integrations: [Sentry.openTelemetryIntegration()],
  // no tracesSampleRate
});
```

**Sentry 10** has no such integration. Its errors-only custom setup uses `skipOpenTelemetrySetup: true` and installs Sentry's context manager, sampler and propagator into your OpenTelemetry SDK, which changes how your process samples and propagates. Prefer upgrading to 11, or use the Belvador SDK's `otel` option.

## Python (`sentry-sdk`)

`OTLPIntegration` (sentry-sdk 2.45.0 or later) makes errors use the active OpenTelemetry trace and span. By default it also installs an exporter to Sentry and replaces the global propagator; switch both off so your setup stays the owner:

```python
import sentry_sdk
from sentry_sdk.integrations.otlp import OTLPIntegration

sentry_sdk.init(
    dsn=os.environ["SENTRY_DSN"],
    integrations=[
        OTLPIntegration(
            setup_otlp_traces_exporter=False,
            setup_propagator=False,
        ),
    ],
    # no traces_sample_rate
)
```

Install it with `pip install "sentry-sdk[opentelemetry-otlp]"`. The older `instrumenter="otel"` with `SentrySpanProcessor` and `OpenTelemetryIntegration` are deprecated.

## Go (`sentry-go`)

`sentryotel.NewOtelIntegration()` (sentry-go 0.45.0 or later) resolves trace ids from a Go `context.Context` — Go has no implicit active span, so the context must reach the capture call:

```go
sentry.Init(sentry.ClientOptions{
    Dsn: os.Getenv("SENTRY_DSN"),
    Integrations: func(in []sentry.Integration) []sentry.Integration {
        return append(in, sentryotel.NewOtelIntegration())
    },
})

// ctx carries the request's span (from otelhttp)
hub := sentry.GetHubFromContext(ctx)
if hub == nil {
    hub = sentry.CurrentHub()
}
hub.Client().CaptureException(err, &sentry.EventHint{Context: ctx}, hub.Scope())
```

A bare `sentry.CaptureException(err)` does not see the span. `NewSentrySpanProcessor` was deprecated in 0.45.0 and removed in 0.47.0.

## Java (`sentry-java`)

With the plain OpenTelemetry Java agent owning spans, add the `sentry-opentelemetry-otlp` module (8.34.0 or later) and its event processor:

```java
Sentry.init(options -> {
    options.setDsn(System.getenv("SENTRY_DSN"));
    options.addEventProcessor(new OpenTelemetryOtlpEventProcessor());
    // no tracesSampleRate
});
```

Do not also attach `sentry-opentelemetry-agent`: it is a second agent with its own instrumentation. `SENTRY_AUTO_INIT=false` only stops that agent from initialising Sentry; it is not an errors-only switch.

## What Belvador has run

The Belvador SDK's `otel` bridge is tested, and so is each Sentry setup above. In every case a small HTTP service with OpenTelemetry owning spans keeps the Sentry SDK for errors exactly as shown, sends one error per request to Belvador, and the error sits on the request's span; without the integration (or event processor) the same error lands outside the trace:

| SDK | Tested with | Result |
|---|---|---|
| Node.js (`@sentry/node`) | node 22.23.4, @opentelemetry/sdk-node 0.300.0, @opentelemetry/auto-instrumentations-node 0.82.0, pino 10.4.0, @sentry/node 11.8.0 | Error on the request's span (2026-10-25) |
| Python (`sentry-sdk`) | python 3.12.15, opentelemetry-distro 0.66b1, opentelemetry-sdk 1.45.1, flask 3.1.3, sentry-sdk 2.72.0 | Error on the request's span (2026-10-25) |
| Go (`sentry-go`) | go 1.27.3, go.opentelemetry.io/otel v1.47.0, otelhttp v0.72.0, otelslog v0.21.0, sentry-go v0.50.0, sentry-go/otel v0.50.0 | Error on the request's span (2026-10-25) |
| Java (`sentry-java`) | java 21.0.12.1, opentelemetry-javaagent 2.32.0, sentry-java 8.61.0, sentry-opentelemetry-otlp 8.61.0, opentelemetry-api 1.66.0 | Error on the request's span (2026-10-25) |

The [capability table](https://belvador.dev/en/docs/reference/capabilities) lists them under **Trace headers**. With Maven or Gradle the Java module brings `opentelemetry-api` and its dependencies; on a hand-built classpath, `opentelemetry-context` also needs `opentelemetry-common`. Check one error on its trace after setting up: the issue's **Trace** link should open the request's waterfall.
