Skip to content

Correlate RUM traces with OpenTelemetry backends

Connect a request from your web app to the backend trace behind it, so you can follow a slow call in a session through to the service that made it slow.

The SDK already adds W3C context propagation headers to fetch and XHR requests made to the same origin, so those are stitched together with no setup.

To reach a backend on a different origin, configure the SDK to send the headers, then allow those headers in your backend’s CORS policy. If you do the first without the second, the browser blocks every call to that origin.

Name the cross-origin targets in propagateTraceHeaderCorsUrls. Each entry is a substring or a regular expression, and it covers both fetch and XMLHttpRequest. To propagate trace context to https://api.example.com:

KloudMateRum.init({
  endpoint: 'https://otel.kloudmate.com:4318',
  rumAccessToken: 'YOUR_PUBLIC_API_KEY',
  applicationName: 'my-app',
  version: '1',
  deploymentEnvironment: 'prod',
  sessionRecorder: {
    enabled: true,
  },
  propagateTraceHeaderCorsUrls: [/https:\/\/api\.example\.com/],
});

This adds context propagation headers to those requests. The backend can then create its spans from that context.

Allow the trace headers in your backend’s CORS policy

Section titled “Allow the trace headers in your backend’s CORS policy”

traceparent isn’t a CORS-safelisted request header, so adding it turns every cross-origin call into a preflighted one. Your backend must name it in the Access-Control-Allow-Headers response header. If it doesn’t, the browser blocks the request before it’s sent:

Access to fetch at 'https://api.example.com/orders' from origin 'https://app.example.com'
has been blocked by CORS policy: Request header field traceparent is not allowed by
Access-Control-Allow-Headers in preflight response.

Add traceparent to the allowed headers, along with tracestate and baggage if your setup propagates them. With Express and the cors middleware:

app.use(cors({
  origin: 'https://app.example.com',
  allowedHeaders: ['content-type', 'traceparent', 'tracestate', 'baggage']
}));

Other stacks set the same header under a different name: CORS_ALLOW_HEADERS in django-cors-headers, allowed_headers in rack-cors, Access-Control-Allow-Headers in an nginx add_header directive.

Match what you’re seeing in the browser to the fix:

SymptomCauseFix
Requests succeed, but the browser session and the backend show separate tracesThe SDK never attached traceparent, so the backend started its own traceAdd the origin to propagateTraceHeaderCorsUrls
Requests fail with Request header field traceparent is not allowed by Access-Control-Allow-HeadersThe SDK attached traceparent, but the backend’s preflight response rejects itAdd traceparent to Access-Control-Allow-Headers

To confirm the header is arriving, log req.headers.traceparent on the backend. If it has a value, the trace is stitched, and the ID it carries is the same one on the browser span.

After that, View full backend trace on a request row in What happened opens the distributed trace behind that call.

With RUM and the backend both instrumented, a request row in a session opens the backend trace behind it:

image

The same trace is available from APM:

image