Skip to content

Sentry

autotel-sentry sends Autotel traces to Sentry over OTLP. It parses a Sentry DSN into the OTLP endpoint and auth headers that Autotel’s init() expects. In the browser, autotel-sentry/browser links the Sentry SDK you already run to autotel-web’s traces: see Browser.

On the server, to put captured errors on the active trace, call Sentry.setOpenTelemetryContextAsyncContextStrategy() after Sentry.init(): @sentry/node then reads the active OpenTelemetry span when it builds an event, so each error carries that span’s trace_id and span_id.

[!NOTE] Earlier versions used a SentrySpanProcessor / SentryPropagator bridge built on deprecated Sentry Hub APIs. That bridge is removed. Sentry SDK v8+ ships its own OTel SDK, so Autotel owns OTel and exports straight to Sentry’s OTLP endpoint. No custom span processor.

  • Node.js 22+
  • autotel
  • @sentry/node, to capture the errors. autotel-sentry calls no Sentry API and declares no dependency on it.
Terminal window
npm install autotel autotel-sentry @sentry/node
import * as Sentry from '@sentry/node';
import { init } from 'autotel';
import { sentryOtlpConfig } from 'autotel-sentry';
const config = sentryOtlpConfig(process.env.SENTRY_DSN!);
// 1. Initialize Sentry. It leaves OpenTelemetry to autotel.
Sentry.init({ dsn: config.dsn });
// Sentry reads trace ids from the active OpenTelemetry span
Sentry.setOpenTelemetryContextAsyncContextStrategy();
// 2. Initialize Autotel. It owns OTel and exports traces to Sentry's OTLP endpoint.
init({ service: 'my-app', endpoint: config.endpoint, headers: config.headers });

@sentry/node 11 leaves OpenTelemetry to autotel by default (enableOpenTelemetrySetup is false). On 8–10, pass skipOpenTelemetrySetup: true instead: those versions register their own OTel SDK otherwise, and you get duplicate span processors and broken traces.

sentryOtlpConfig(dsn: string): SentryOtlpConfig

Section titled “sentryOtlpConfig(dsn: string): SentryOtlpConfig”

Parses a Sentry DSN and returns the three values that wire Autotel to Sentry’s OTLP ingestion endpoint. Throws when the DSN is missing or cannot be parsed.

const config = sentryOtlpConfig(
'https://<key>@o<org>.ingest.sentry.io/<project>',
);
// config.dsn normalized DSN string (pass to Sentry.init)
// config.endpoint OTLP base URL (pass to Autotel init as `endpoint`)
// config.headers auth headers (pass to Autotel init as `headers`)

Keep Sentry for what it already does on your page (errors, replays) and let autotel-web trace. joinSentry links the two both ways:

  • every Sentry error carries the autotel trace it happened in, so the issue opens that trace;
  • every span carries the Sentry user’s user.id and, while replay records the session, sentry.replay_id, which Sentry’s trace view uses to open the replay.
Terminal window
npm install autotel-web autotel-sentry @sentry/browser
import * as Sentry from '@sentry/browser'; // or @sentry/react, @sentry/vue…
import { initFull } from 'autotel-web/full';
import { joinSentry, sentryOtlpConfig } from 'autotel-sentry/browser';
const sentry = sentryOtlpConfig(import.meta.env.VITE_SENTRY_DSN);
Sentry.init({
dsn: sentry.dsn,
// No browserTracingIntegration: autotel traces.
integrations: [Sentry.replayIntegration()],
replaysSessionSampleRate: 0.1,
});
initFull({
service: 'web',
// Straight to Sentry's OTLP endpoint, from the browser.
endpoint: sentry.browserEndpoint,
spanEnrichers: [joinSentry(Sentry)],
});

browserEndpoint is Sentry’s OTLP endpoint with the DSN’s public key as ?sentry_key=. The browser exporter sends no custom headers, and the beacon that flushes on page unload cannot carry any, so the key goes in the URL. It is the same public key the Sentry SDK already puts in every page. Sentry’s OTLP endpoint accepts cross-origin requests, so no collector is needed. To send spans somewhere else instead, point endpoint there and pass traceUrl (see below).

The error links even when it is captured after an await, where the browser has already lost the active span:

await span('checkout.click', async () => {
const response = await fetch('/checkout', { method: 'POST' });
// No active span here any more. The Sentry event still carries the
// checkout.click trace.
if (!response.ok) Sentry.captureException(new Error('Checkout failed'));
});

If two traces are in flight at that moment (two overlapping clicks), the error keeps Sentry’s own trace id rather than risk linking the wrong one. To be explicit, read the ids before the await and pass them; a trace you pass is always kept:

const ids = trace.getActiveSpan()?.spanContext();
await fetch('/checkout', { method: 'POST' });
Sentry.captureException(error, {
contexts: { trace: { trace_id: ids?.traceId, span_id: ids?.spanId } },
});
``` See
[Join any frontend SDK](/autotel/integrations/join/#spans--vendor-currentspancontext)
for how to pass the trace explicitly then.
### Options
```typescript
joinSentry(Sentry, {
// Spans exported somewhere other than Sentry: link the issue to them.
// Written to the event's `autotel` context as `trace_url`.
traceUrl: ({ traceId }) =>
`https://grafana.example.com/explore?trace=${traceId}`,
// Failed spans get `session.replay.url`, so other backends can open the replay.
replayUrl: (id) => `https://my-org.sentry.io/replays/${id}/`,
});

sentry.replay_id and session.replay.url are added only while replay records the whole session (replaysSessionSampleRate). A buffered replay (replaysOnErrorSampleRate) is uploaded only when Sentry captures an error, so a link from a span that failed without one would open nothing. Sentry follows the same rule for its own replay_id.

debug (on in development, off in production) explains in the console, once per reason, why a link did not happen.

  • It does not trace with Sentry. With browserTracingIntegration also on, two tracers would put two trace headers on each request, and joinSentry leaves an error on Sentry’s span whenever Sentry has one. It warns once in development if it finds that integration.
  • It imports no Sentry package. It reads the SDK object you pass in, so @sentry/browser, @sentry/react, @sentry/nextjs and the CDN bundle all work, and your page keeps one copy of Sentry.

joinSentry is built from autotel-web/join, the same building blocks you can use for any other frontend SDK.

A Sentry SDK in the browser or a mobile app generates its own trace ids and does not send W3C traceparent by default, so your frontend and your Autotel backend end up in two unconnected traces. Turn on propagateTraceparent, and enable browser tracing so something actually instruments the requests:

Sentry.init({
dsn: 'YOUR_DSN',
// Plain @sentry/browser does not auto-enable this. Without it nothing
// instruments fetch/XHR and no header is attached, whatever
// propagateTraceparent says. Framework SDKs enable it for you.
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1.0,
propagateTraceparent: true,
});

The header carries the frontend’s sampling decision, and a parentbased_* sampler honours it. parentbased_always_on is the OpenTelemetry default, so your backend stops deciding for itself and a tail-based sampler in your collector never sees the spans it is meant to judge. Set OTEL_TRACES_SAMPLER=always_on on the backend to take that decision back.

If autotel-web is also on the page, only one of them can own the header. Both inject traceparent only when it is absent, so whichever is initialized last wraps the other and wins for fetch. XHR does not follow that rule, because both SDKs inject during send() rather than by wrapping. So when Sentry is meant to own propagation, set both instrumentFetch: false and instrumentXHR: false on autotel-web rather than relying on initialization order; otherwise fetch and XHR can land in different traces.

Removed in 0.7.0. Delete the call and its import, and call Sentry.setOpenTelemetryContextAsyncContextStrategy() once after Sentry.init(). The SentryLinkable type is gone too.

  • example-sentry: traces and logs over OTLP, with captured errors on the active trace. pnpm web runs the browser join: a failed checkout whose Sentry error, browser spans, server span and replay share one trace.