Skip to main content
Browser monitoring is a beta AppSignal 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.
A product page reports a failed GET /api/boom/ request: HTTP 500. You know the request failed, but not why. Distributed tracing follows that request from the browser into the back end that handled it and raised a RuntimeError. In this example, that is a Django app. Any back end that supports distributed tracing works the same way. The service map shows both sides of that trace:
Trace service map with two boxes, Browser and django, both marked with an error, connected by an 81 ms edge.

The service map for this trace: the back-end error connected to the browser error that started it.

To connect the browser error to work in the back end, 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 back end: Instrument the service that handles those requests. The instrumentation reads traceparent and reports 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 back end 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.
Distributed tracing is a beta AppSignal Labs feature. The browser SDK needs no collector. The receiving service needs an AppSignal hosted collector for its spans.

Before you start

Your front end needs only the Browser SDK. The rest of the setup is in the back end: the service that handles your front end’s requests. In the example above, that is the Django service. It can be an API on its own host, or the same app that serves your pages. Set it up first, so its spans appear in the trace when a request fails:
  • Report through a hosted collector. On the Hosted Collectors page in your organization settings, open a collector and copy its Collector URL. Send the service’s data to that URL. If the list is empty, create a collector there first. Distributed tracing only works through a hosted collector. In Python and Ruby, set the URL as the collector_endpoint option. Ruby also needs the appsignal-opentelemetry gem.
  • Set a service name. Give the service its own name. The trace uses it to tell the service apart from the browser and other services. Where you set it depends on the back end’s language:
  • Instrument its HTTP server. Use an integration for its framework that reads traceparent from incoming requests. That is how the service joins the browser’s trace instead of starting its own. In Go, also register a propagator with otel.SetTextMapPropagator.
  • Allow traceparent across origins. A back end can be on another origin, such as api.example.com called from www.example.com. The browser may then send a CORS preflight before the request. Allow traceparent in the back end’s response to the preflight. Otherwise the browser may block the request:
Node.js and Elixir are not supported yet. With a Node.js back end, such as Express or Next.js API routes, failed requests are still reported as browser errors. Their traces show no back-end spans.

Enable it

Add a pattern for each back-end host and path to tracePropagationTargets. The default empty list leaves tracing off:
Patterns match the host and path of each fetch and XMLHttpRequest call. Because the SDK hooks into those browser APIs, setup is the same in React, Vue, or plain JavaScript. That includes libraries built on them, such as axios. Patterns ignore the scheme and query string. A relative URL resolves against the page’s address, so a call to /api/orders from www.example.com matches www.example.com/api/**. Use * within one path segment or ** across segments. Include both a host and a path: 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 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 back end’s host, then instrument the receiving service. 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. 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.

What is reported

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 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.
  • 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, unless they match privacy.networkBlocklist or breadcrumb collection is disabled. A breadcrumb for a request that propagated context includes its trace ID. Its Open trace button opens that trace’s timeline. A failed request goes through the same pipeline as every other error. errors.enabled, errors.sampleRate, the 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.

Reading it in AppSignal

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. Open an issue to see its message, breadcrumbs, and tags in the side panel. Its Sample tab also shows the trace: the Trace service map, Trace timeline, and related log lines. Select Fullscreen in the Trace timeline card to open the view below. If the preview omits spans, the button reads All spans. Open full trace page, the icon next to the issue number, opens the full issue page. In the fullscreen timeline, Browser labels the browser span’s service. Select that span to see the request and error details. In the product page example, GET /api/boom/ returned HTTP 500. In the 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.
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.

The failed browser request and related Django spans in one trace.

Joining a trace your code started

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 in fullscreen to inspect the back-end work.