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/SentryPropagatorbridge 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.
Prerequisites
Section titled “Prerequisites”- Node.js 22+
autotel@sentry/node, to capture the errors.autotel-sentrycalls no Sentry API and declares no dependency on it.
Installation
Section titled “Installation”npm install autotel autotel-sentry @sentry/nodeQuick start
Section titled “Quick start”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 spanSentry.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`)Browser: autotel traces, Sentry errors
Section titled “Browser: autotel traces, Sentry errors”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.idand, while replay records the session,sentry.replay_id, which Sentry’s trace view uses to open the replay.
npm install autotel-web autotel-sentry @sentry/browserimport * 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
```typescriptjoinSentry(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.
What it does not do
Section titled “What it does not do”- It does not trace with Sentry. With
browserTracingIntegrationalso on, two tracers would put two trace headers on each request, andjoinSentryleaves 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/nextjsand 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.
Alternative: let Sentry trace the browser
Section titled “Alternative: let Sentry trace the browser”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.
Migrating from linkSentryErrors
Section titled “Migrating from linkSentryErrors”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.
Examples
Section titled “Examples”example-sentry: traces and logs over OTLP, with captured errors on the active trace.pnpm webruns the browser join: a failed checkout whose Sentry error, browser spans, server span and replay share one trace.
See also
Section titled “See also”- Sentry OTLP docs for the Sentry-side configuration.
- Configuration for the
endpointandheadersoptions oninit().