> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appsignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Distributed tracing for Browser

> See how a failed request in the browser connects to the back-end trace it started, so the error and the server-side work behind it read as one trace.

export const Compatibility = ({versions = [], label = "Available in"}) => {
  if (!Array.isArray(versions) || versions.length === 0) {
    return null;
  }
  const defaultPillStyle = {
    borderColor: "#d4d4d8",
    background: "#f4f4f5",
    color: "#3f3f46"
  };
  const pillStyles = {
    "AppSignal for Elixir": {
      background: "#f3e8ff",
      borderColor: "#d8b4fe",
      color: "#6b21a8"
    },
    "AppSignal for Front-end": {
      background: "#fef9c3",
      borderColor: "#fde047",
      color: "#854d0e"
    },
    "AppSignal for Go": {
      background: "#ccfbf1",
      borderColor: "#5eead4",
      color: "#115e59"
    },
    "AppSignal for JavaScript": {
      background: "#fef9c3",
      borderColor: "#fde047",
      color: "#854d0e"
    },
    "AppSignal for Node.js": {
      background: "#dcfce7",
      borderColor: "#86efac",
      color: "#166534"
    },
    "AppSignal for Python": {
      background: "#dbeafe",
      borderColor: "#93c5fd",
      color: "#1e40af"
    },
    "AppSignal for Ruby": {
      background: "#fee2e2",
      borderColor: "#fca5a5",
      color: "#991b1b"
    },
    "AppSignal for Rust": {
      background: "#ffedd5",
      borderColor: "#fdba74",
      color: "#9a3412"
    }
  };
  const getPillStyle = name => ({
    ...defaultPillStyle,
    ...pillStyles[name] || ({})
  });
  return <div className="not-prose my-4 rounded-lg border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm dark:border-white/10 dark:bg-white/5">
      <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
        <span className="font-semibold text-zinc-700 dark:text-zinc-200">
          {label}:
        </span>
        {versions.map((v, i) => <span key={`${v.name}-${v.version}-${i}`} className="inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium" style={getPillStyle(v.name)}>
            <span>{v.name}</span>
            <span className="opacity-70">
              {v.version}
              {v.exact ? "" : "+"}
            </span>
          </span>)}
      </div>
    </div>;
};

<Compatibility versions={[{ name: "AppSignal for Browser", version: "1.0.0-beta.7" }]} />

<Note>
  Browser monitoring is a beta [AppSignal Labs](/labs) feature. The
  `@appsignal/browser` package is published under the `beta` tag on npm, and its
  configuration and API may still change between beta releases. Share feedback
  in our [Discord community](https://discord.gg/EjF6ykYx63).
</Note>

A checkout page reports `POST https://api.example.com/orders: HTTP 503`. You know the order request failed, but not why. The order service may have spent four seconds waiting on a database lock before returning 503. [Distributed tracing](/distributed-tracing) lets you follow that request from the browser into the order service.

To connect the browser error to work in the order service, set up both sides:

1. **Browser:** Add your API URL patterns to `tracePropagationTargets`. Matching requests carry a `traceparent` header. A 5xx response or qualifying timeout becomes a browser error.
2. **Receiving API service:** Instrument its HTTP server to read `traceparent` and report spans through an AppSignal hosted collector. Use a supported integration or OpenTelemetry instrumentation for its framework.

Step 1 on its own only reports the browser error. Its timeline stays empty until the order service is instrumented in step 2, because the server spans come from that instrumentation, not from the browser SDK. Both steps are needed to see the request as one trace.

<Note>
  Distributed tracing is a beta [AppSignal Labs](/labs) feature. The browser SDK needs no collector. The receiving service needs an AppSignal [hosted collector](/collector/hosted-vs-self-hosted#hosted-collector) for its spans.
</Note>

<h2 id="before-you-start">
  Before you start
</h2>

Choose the setup for the service that receives the browser request. Its HTTP server integration must read `traceparent`, and the service must send spans through an AppSignal [hosted collector](/collector/hosted-vs-self-hosted#getting-started):

* **FastAPI:** Install [`opentelemetry-instrumentation-fastapi`](/python/instrumentations/fastapi) and use [Python collector mode](/python/configuration/collector).
* **Rails:** Use the [`appsignal` and `appsignal-opentelemetry` gems](/ruby/configuration/collector#installing-the-opentelemetry-gems) in collector mode. The [Rails integration](/ruby/integrations/rails) reads incoming traces.
* **Go or custom OpenTelemetry:** Instrument your HTTP server and configure trace-context propagation. See [availability and setup](/distributed-tracing#availability-and-setup).

<h2 id="enable-it">
  Enable it
</h2>

Add a pattern for each API host and path to `tracePropagationTargets`. The default empty list leaves tracing off:

<CodeGroup>
  ```js appsignal.js theme={null}
  import { init } from "@appsignal/browser";

  init({
    key: "<YOUR_FRONTEND_API_KEY>",
    endpoint: "https://appsignal-endpoint.net",
    appVersion: "<YOUR_APP_REVISION>",
    // Your API hosts, matched against host + pathname
    tracePropagationTargets: ["api.example.com/**"],
    // How this front end is named in a trace. Defaults to "Browser"
    serviceName: "Checkout UI",
  });
  ```
</CodeGroup>

Patterns match the host and path of each `fetch` and `XMLHttpRequest` call. They ignore the scheme and query string. Use `*` within one path segment or `**` across segments. Include both a host and a path:

| Pattern | Matches |
| - | - |
| `api.example.com/**` | Every request to `api.example.com` on the default port. |
| `api.example.com:8443/**` | Every request to `api.example.com` on port 8443. Include the port when it is not the default. |
| `*.example.com/**` | Every request to a subdomain of `example.com`. |
| `api.example.com/v1/**` | Requests under `/v1` on `api.example.com`. |
| `api.example.com` | Nothing. It has no path, so add `/**`. |
| `/api/**` | Nothing. It has no host. |
| `**/api/**` | That path on any host, including third parties you never meant to trace. Avoid it. |

In the trace timeline, `serviceName` labels the browser error span. For example, `Checkout UI` identifies errors from your checkout front end. The default is `Browser`. Use a different name for each front end you monitor. The SDK sends the name with every error report.

The [Browser SDK install flow](https://appsignal.com/redirect-to/organization?to=sites/new/browser) offers **Distributed tracing** as an option. It is off by default. Selecting it adds `tracePropagationTargets: ["api.example.com/**"]` to the browser snippet. Replace the example host with your API host, then [instrument the receiving service](#before-you-start). The wizard does not install or configure that service.

To verify propagation, load a page that calls your API and inspect the request in your browser's Network panel. A matching request should carry a `traceparent` header. Then inspect a failed request in AppSignal to check that back-end spans appear in its trace.

For a cross-origin API, such as `api.example.com` called from `www.example.com`, the browser may send a [CORS preflight](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) before the request. Allow `traceparent` in your API's response to the preflight. Otherwise the browser may block the request:

<CodeGroup>
  ```http HTTP theme={null}
  Access-Control-Allow-Headers: content-type, traceparent
  ```
</CodeGroup>

The SDK adds no header to a request made in `no-cors` mode, because the browser would strip it. Those requests are never reported as failed requests.

<h2 id="what-is-reported">
  What is reported
</h2>

The SDK reports these outcomes for a matching request that sent a `traceparent` header:

* **A 5xx response** is reported as an `HTTPError`.
* **A timeout** is reported as a `TimeoutError`. That is a `fetch` that rejects with a `TimeoutError`, as one bounded by `AbortSignal.timeout()` does. It is also an `XMLHttpRequest` whose `timeout` fires. The server may have received and traced a timed-out request.

The message names the request and its outcome, for example `POST https://api.example.com/orders: HTTP 503` or `GET https://api.example.com/orders: timed out`. Besides everything an [ordinary browser error](/browser/error-tracking#what-is-reported) carries, the report includes:

* **The request**, as `params.request`: the `url`, the `method`, the `status` when a response arrived, and the `duration_ms`. Query parameters are removed from the URL as set by your [allowlist](/browser/privacy#urls-and-query-parameters).
* **The span**: the trace ID and span ID from the request's `traceparent`, with its start time and duration. AppSignal builds the browser's span from these, so it covers the request as the browser saw it. After a timeout, the back end's spans can run longer than the browser's span.
* **The service name** you set, or `Browser`.

Nothing is reported for:

* A successful request. The header is sent, but the browser stays silent, so the back-end trace stands on its own.
* A 4xx response. The SDK does not classify it as a failed request for tracing.
* A request the browser could not deliver: refused, failed DNS, or blocked by an extension. No server handled it, so there is no back-end trace to join.
* A timeout while the device is offline, which says nothing about the back end.
* A request your code canceled.

These requests can still leave a [network breadcrumb](/browser/breadcrumbs#reading-network-breadcrumbs), unless they match `privacy.networkBlocklist` or breadcrumb collection is disabled. A breadcrumb for a request that propagated context includes its trace ID.

A failed request goes through the same pipeline as every other error. `errors.enabled`, `errors.sampleRate`, the [rate limits](/browser/error-tracking#rate-limits), and the deduplication of repeats all apply. `beforeError` receives it as an `HTTPError` or `TimeoutError`, with the request under `context.request`. You can drop or redact it there. A request matching `privacy.networkBlocklist` is never reported.

The SDK avoids reporting the same `fetch` rejection twice when it also reaches an unhandled rejection handler or `captureError()`. If your code throws a new error after a 500 response, that error has its own trace. The failed request remains a separate report.

<h2 id="reading-it-in-appsignal">
  Reading it in AppSignal
</h2>

Failed requests appear in the Errors view of the Browser section as `HTTPError` and `TimeoutError` issues. They are grouped by the route the user was on, like every other front-end error. Read more in [front-end errors in AppSignal](/browser-monitoring/errors).

Open an issue to see its message, breadcrumbs, and tags. On the full issue page, select **Traces**. Then select **Fullscreen** in the **Trace timeline** card to open the view below. If the preview omits spans, the button reads **All spans**. From the Browser Errors side panel, **Open full trace page** takes you to the issue page.

In the fullscreen timeline, `Browser` labels the browser span's service. Select that span to see the request and error details.

This example shows a product page where `GET /api/boom/` returned HTTP 500. In the [trace timeline](/trace-page#trace-timeline), you can follow the failed request from the browser into the Django service:

* `Browser` shows an `HTTPError` on `/product/chocolate-stroopwafel` (85 ms). The selected span's details show the request URL and HTTP 500 response.
* Beneath it, `django` shows `GET api/boom/` with a `RuntimeError` (81 ms).
* The Django request contains two `SELECT` spans and a `POST` span, with their durations.

<Frame caption="The failed browser request and related Django spans in one trace.">
  <img src="https://mintcdn.com/appsignal-715f5a51/Dd6j9BLrZGGvnmnD/assets/images/screenshots/browser/distributed-tracing-trace-timeline.png?fit=max&auto=format&n=Dd6j9BLrZGGvnmnD&q=85&s=7bee604479952a0a8d48422852810a59" alt="Trace timeline linking an 85 ms browser HTTPError for GET /api/boom/ (HTTP 500) to an 81 ms Django RuntimeError, with SELECT and POST child spans." width="1395" height="874" data-path="assets/images/screenshots/browser/distributed-tracing-trace-timeline.png" />
</Frame>

The [service map](/trace-page#service-map) shows the whole trace end to end: the error in the Django service connected to the failed request in the browser that started it.

<Frame caption="The service map for the same trace: the back-end error connected to the browser error that started it.">
  <img src="https://mintcdn.com/appsignal-715f5a51/Dd6j9BLrZGGvnmnD/assets/images/screenshots/browser/distributed-tracing-service-map.png?fit=max&auto=format&n=Dd6j9BLrZGGvnmnD&q=85&s=34693ed41ff4344c9a80827fbd60f3fd" alt="Trace service map with two boxes, Browser and django, both marked with an error, connected by an 81 ms edge." width="1049" height="575" data-path="assets/images/screenshots/browser/distributed-tracing-service-map.png" />
</Frame>

<h2 id="joining-a-trace-your-code-started">
  Joining a trace your code started
</h2>

Your application may already set a valid `traceparent` on a request, for example through an OpenTelemetry browser SDK. AppSignal for Browser then leaves that header alone, and a failed request joins the trace your code started. This holds for `fetch` and `XMLHttpRequest` alike.

On the issue's **Traces** tab, open the [trace timeline](/trace-page#trace-timeline) in fullscreen to inspect the back-end work.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.