# Link errors across tiers with traces

> Turn on tracePropagation so browser errors, server errors and job runs from one request show up together under “Same trace”.

One click in the browser can end in an error three tiers away: a server function, then a Worker, then a job. With **trace propagation** on, every error and job run caused by that click carries the same **trace id**, and Belvador shows them together under **Same trace** on each issue and on the **Trace** page.

This is linking, not performance tracing: the Belvador SDKs create no spans and no timings per tier — only which errors and runs share a trace. For spans and timings, send [OpenTelemetry traces](https://belvador.dev/en/docs/otel/overview); the option below makes your errors line up with them.

## Turn it on

Available from `@belvador/*` 0.3.0. It is **off** by default, because adding headers to requests can trigger CORS checks on APIs you do not control.

### Browser and React

```ts
import { init } from "@belvador/browser"; // or @belvador/react

init({
  dsn: "https://<public-key>@ingest.belvador.dev/<project-id>",
  // Only your own APIs. Every other URL is left alone.
  tracePropagation: { targets: ["/api/", "https://api.example.com/"] },
});
```

### TanStack Start

```ts title="src/router.tsx"
import { init } from "@belvador/tanstack-start/client";

if (typeof window !== "undefined") {
  init({
    dsn: "https://<public-key>@ingest.belvador.dev/<project-id>",
    // Same-origin server functions and routes; the server SDK continues the trace.
    tracePropagation: { targets: ["/_serverFn/", "/api/"] },
  });
}
```

`targets` lists the URLs that get trace headers: a string matches any URL containing it (and, in the browser, the path of same-origin requests); a RegExp is tested against the URL. Only list your own APIs. An empty list turns propagation off.

The server packages (`@belvador/tanstack-start/server`, `@belvador/cloudflare`) continue an incoming trace automatically: an error or `withMonitor` run during that request carries the caller's trace id. Workers also forward headers on their own `fetch` calls when you set `tracePropagation` in the `withBelvador` options.

## With OpenTelemetry

If the app already runs OpenTelemetry, let OpenTelemetry own the trace and give Belvador its API. An error captured while a span is active then carries that span's trace id and span id, so the issue opens on the right span of the [trace waterfall](https://belvador.dev/en/docs/otel/overview):

```ts title="src/index.ts"
import * as api from "@opentelemetry/api";
import { withBelvador } from "@belvador/cloudflare"; // same option in every @belvador/* init

export default withBelvador(
  (env: { BELVADOR_DSN?: string }) => ({
    dsn: env.BELVADOR_DSN ?? "https://<public-key>@ingest.belvador.dev/<project-id>",
    // The same @opentelemetry/api copy your OpenTelemetry setup uses.
    otel: { api },
  }),
  handler,
);
```

- Pass the module itself (`import * as api from "@opentelemetry/api"`) — the copy your OpenTelemetry SDK registered its provider with. Belvador never imports it, never creates spans and never registers a provider.
- The ids come from the span that is active when the error is captured: `captureException` inside your span, or an error the framework reports before the span ends. An error that escapes the span and is reported later by the outer handler keeps the request's trace id without that span.
- With `otel` set, `tracePropagation` is ignored (OpenTelemetry already sends `traceparent`), and the SDK reports the diagnostic `BLV_TRACE_OWNER_OTEL` until you remove it.
- Available from `@belvador/*` 0.4.0.

## Headers sent

| Header | Sent | Content |
|---|---|---|
| `traceparent` | Always (W3C Trace Context) | `00-<trace id>-<span id>-00` |
| `sentry-trace` | Always | `<trace id>-<span id>` |
| `baggage` | Only with `baggage: true` | Only the SDK's own `sentry-*` keys |

Requests to Belvador's own ingest are never given trace headers.

## CORS

If the API is on another origin, its CORS preflight must allow the new headers, or the browser blocks the request:

```text
Access-Control-Allow-Headers: traceparent, sentry-trace
# only when you also turn on baggage:
Access-Control-Allow-Headers: traceparent, sentry-trace, baggage
```

Check this before turning propagation on in production: a missing header in `Access-Control-Allow-Headers` makes the **request** fail, not only the trace.

## Reading a trace

- On an issue, **Same trace** lists up to 200 errors and runs with the same trace id within ±24 hours of that event, across the projects of your workspace you can see.
- **Open trace** shows them in time order. You can paste a trace id or a whole `traceparent` header into ⌘K to open it.
- A trace only joins data inside one workspace. A request into another workspace's app shows up there as a separate piece.

An event without a trace id says so on the issue; that is normal for errors that did not come from a propagated request.
