Correlation with Data
Correlate what a real user did in the browser with the exact backend work your services performed, and the log line(s) those services emitted using one continuous context. This page walks you through a RUM session → the backend trace → the attached logs, explaining what each panel means and how to read it confidently.
What you get
- A single request “thread” across browser, services, and logs (via
<trace_id>/<span_id>). - Visual timing (flame graph) to spot where time is actually spent.
- Request/response and headers to validate behaviour and caching.
- Log details to confirm status, payload sizes, and errors.
Prerequisites#
- RUM SDK initialised with tracing enabled, so network events carry trace context. What that takes differs by platform — see Platform support below.
- Backend services instrumented for tracing and configured to propagate W3C Trace Context (
traceparent/tracestate) through gateways/queues. - Logging is configured to enrich log events with
<trace_id>and<span_id>(so the Logs tab can stitch them automatically).
Platform support#
Correlation itself is platform‑agnostic: Middleware joins a RUM event to a backend trace whenever they share a <trace_id>. What differs is how much setup each SDK needs before its requests carry that ID.
| Platform | HTTP events | Trace propagation | What you have to do |
|---|---|---|---|
| Browser | Automatic | Every URL by default | Nothing |
| iOS | Automatic (URLSession) | Every URL by default | Nothing |
| React Native | Automatic (fetch/XHR) | Every URL by default | Nothing |
| Android | Manual | Every URL, once instrumented | Route all traffic through createRumOkHttpCallFactory(...) |
Android is the one platform that needs work before correlation happens, and it fails silently: sessions look healthy, but no HTTP events are recorded and no backend trace ever resolves. The SDK logs a warning at startup if no client was wrapped. Follow the Android link above for the fix.
On every platform you can narrow which hosts receive trace headers, so your trace IDs stay off third‑party APIs. See the platform page for the option name.
Short Answer: it’s close, but we can make the “what” and the “how” completely unmistakable by adding two small, explicit sections at the top. Below are drop-in blocks you can paste into your page. They keep your placeholders and reference your screenshots.
What is “correlation” in RUM?#
Correlation means stitching a single user action in the browser (a RUM event such as an XHR/Fetch) to the exact backend trace that handled it and to the log entries those services emitted so that you can follow one request end-to-end.
Middleware achieves this by propagating and storing shared identifiers, primarily <trace_id> and <span_id>, across RUM, tracing, and logging. When these IDs match, you are looking at the same request across all three data types.
How Middleware stitches RUM → Traces → Logs#
- Client (RUM SDK) — browser, iOS, Android, or React Native
- Instruments the app's HTTP client and starts/joins a distributed trace. See Platform support for which SDKs do this automatically.
- Attaches W3C Trace Context headers (
traceparent,tracestate) carrying<trace_id>/<span_id>on the network request. - Captures browser timings and attributes (e.g.,
resource.duration,http.url,browser.name) as well as the session ID (session.id). - Sends the RUM event with these fields to Middleware.
- Edge / Service (Tracing)
- Your tracing library extracts the incoming context from
traceparent. - It continues the same trace and creates server spans (e.g.,
app-api-v2span), preserving<trace_id>and generating child<span_id>values. - Spans are sent to the Middleware’s trace store.
- Your tracing library extracts the incoming context from
- Service Logging (Logs)
- Your logger is enriched to include
<trace_id>/<span_id>on every relevant log event (request logs, errors, DB warnings, etc.). - Logs are shipped to Middleware and indexed by these IDs.
- Your logger is enriched to include
- Index & Link
- Middleware indexes RUM events, traces, and logs on
<trace_id>(and also onsession.id/time). - From a RUM event, the UI can unambiguously fetch the trace and the logs that share the duplicate IDs.
- Middleware indexes RUM events, traces, and logs on
Start from the RUM Session (front end)#
Open RUM → Sessions and select a session that interests you (e.g., a user reporting slowness). In the left pane, choose Events. Filter for Type: xhr and click the network event (e.g., GET /api/v1/account/init).
You’re selecting the exact browser request that the user’s page fired. From here, Middleware pivots you to the backend view for this very request.

What you see on the right:
- Flame Graph (top): stacked spans across services that handled this request.
- Services panel / Exec time % (right): Which service took the most time.
- Tabs (bottom): Trace Attributes, Events, Logs, Headers, Request, Response.
Why it matters:
- This is already filtered to the same request your browser made. No manual search across products; you’re looking at one stitched timeline.
Read the Trace (How to Interpret the Panels)#
1. The Flame Graph#
- Each bar is a span; its width is duration.
- Bars stack to show parent/child work (e.g., frontend gateway →
app-api-v2→ DB). - Zoom and hover to see where the latency sits. Long, wide bars are hot spots.
How to use it
- If the top bar is the HTTP handler and a child DB call dominates, you’ve found the bottleneck.
- If most of the time is spent before any server span, it’s likely due to network/TTFB/CDN issues.
2. Service Breakdown (Exec time %)#
- A quick at-a-glance ranking of which service is responsible for most of the end-to-end time.
3. Trace Attributes (context you’ll rely on)#
Open the Trace Attributes tab (it’s usually selected). You’ll see a merged view of browser and server attributes. Typical high-value fields:
| Attribute | Meaning | Example |
|---|---|---|
event.type | Browser event type | xhr |
ttp.method, http.url, http.status_code | Core request data | GET https://<host>/api/v1/account/init → 200 |
traceId, spanId | Correlation keys across RUM, trace, and logs | <trace_id>, <span_id> |
browser.trace | Browser tracing enabled | true |
resource.duration | Browser-observed total resource time | 369.3 ms |
resource.first_byte.duration | Time to first byte from server/CDN | 366.5 ms |
resource.download.duration | Time to read the response | 2.8 ms |
resource.size / resource.transfer_size | Uncompressed vs on-wire size | 14708 B / 5716 B |
http.response_content_length | Server-reported length | 5416 B |
project.name, mw.agent_id | Project/agent identity | <project_name>, <agent_id> |
browser.name/version, os | Client environment | Chrome 138, Linux |
cf-*, email, name | (Optional) geo/user fields | <city>, <country>, <email>, <user_name> |
Anchor the correlation on IDs: Write down <trace_id> and <span_id> from here; you’ll confirm them again in the Logs view. If they match, you’re unquestionably looking at the same request from browser → services → logs.
Jump to Logs (same request, same IDs)#
Click the Logs tab in the same right-hand inspector. You’ll see log entries scoped to this trace/span, no extra filters needed.

Open a log row to inspect its structured fields.

Key fields you’ll read here:
| Field | Why it matters | Example |
|---|---|---|
service.name | Which service emitted the log | app-api-v2 |
method, path, response_status | Confirms HTTP behaviour | GET, /api/v1/account/init, 200 |
duration | Service-reported time | 24 ms |
traceId / spanId | Must match the Trace view | <trace_id> / <span_id> |
response-body-size | What the service sent | 0 B / 14 K |
ip, user_id | Origin context (if logged) | <ip>, <user_id> |
project.name, mw_agent, telemetry.sdk.language | Runtime & agent metadata | <project_name>, true, go |
Sanity check: If the traceId here doesn’t match the Trace Attributes, your gateway/worker likely started a new trace. Ensure trace context headers are forwarded end-to-end.
Validate Payloads and Headers#
Use the Request, Response, and Headers tabs to confirm:
- Auth headers present/valid; cache headers as expected.
- Response status/text and content length match what the browser saw.
- Mismatch examples to look for:
- Server
durationis small butresource.first_byte.durationis large → network/CDN or queueing before your app. - Response length differs between server and browser fields → compression or proxy mutation (expected with HTTP/2 + gzip/brotli).
- Server
Field Glossary (front-end timings)#
These fields are reported by the browser and help you understand “time outside your code”:
resource.connect.duration: TCP connect time.resource.ssl.duration: TLS handshake time.resource.first_byte.duration: Time until first byte (server + network + intermediaries).resource.download.duration: Time to read response from socket.resource.duration: Sum of the above (roughly connect → download).
Tip: Compare resource.first_byte.duration with the service log duration. If first_byte ≫ service duration, most latency happened before your application code returned (CDN, WAF, upstream, or network).
Link a RUM Session to Backend API Calls#
Trace correlation joins one request at a time through its <trace_id>. To see every backend call made during a user's session, send the RUM session ID with your API requests and record it on your backend spans and logs. You can then:
- Find all backend traces and logs for a session a user reported.
- Go from a backend error or slow trace straight to the session replay.
The Browser RUM SDK keeps the session ID in a first-party cookie that your own JavaScript can read, so this needs no SDK changes.
How the session ID is stored#
| Property | Value |
|---|---|
| Cookie name | Starts with _mwRumSessionId_. The suffix is the SDK's cookie format version, so match on the prefix. |
| Cookie value | URL-encoded JSON: {"id":"<session_id>","startTime":<epoch_ms>} |
id | 32-character hex string, the same value as session.id on RUM events |
| Scope | path=/ on the host that loads the SDK; readable from JavaScript (not HttpOnly) |
| Lifetime | Extended by 15 minutes on activity, up to 4 hours. A new session gets a new ID. |
The cookie is created when the SDK initialises and changes when the session rotates, so read it each time you send a request instead of caching it at page load.
Step 1: Read the session ID in the browser#
1function getRumSessionId() {
2 const entry = document.cookie
3 .split(";")
4 .map((c) => c.trim())
5 .find((c) => c.startsWith("_mwRumSessionId_"));
6 if (!entry) return undefined;
7 try {
8 const value = JSON.parse(decodeURIComponent(entry.slice(entry.indexOf("=") + 1)));
9 return value.id || undefined;
10 } catch {
11 return undefined;
12 }
13}Step 2: Send it with your API requests#
Add the ID as a request header, for example sessionId, in the one place your app sends API requests from:
1async function apiFetch(url, options = {}) {
2 const headers = new Headers(options.headers);
3 const sessionId = getRumSessionId();
4 if (sessionId) headers.set("sessionId", sessionId);
5 return fetch(url, { ...options, headers });
6}1const api = axios.create({ baseURL: "https://api.example.com" });
2
3api.interceptors.request.use((config) => {
4 const sessionId = getRumSessionId();
5 if (sessionId) config.headers["sessionId"] = sessionId;
6 return config;
7});Notes:
- Send it only to your own APIs. Don't attach it to third-party requests.
- Cross-origin APIs: a custom header triggers a CORS preflight. Add
sessionIdto your API'sAccess-Control-Allow-Headersresponse header, or the browser blocks the request. - EventSource and WebSocket: these can't set request headers. Pass the ID as a query parameter instead, for example
?sessionId=<session_id>. - API on the same host as your page: the browser already sends the
_mwRumSessionId_cookie with each request, so your backend can parse the cookie (Step 1's format) instead of reading a header.
Step 3: Record it on your backend#
Read the header and set it as the session.id attribute on the active server span, so backend spans carry the same key as the RUM events. The code must run after your tracing middleware has started the server span.
1const { trace } = require("@opentelemetry/api");
2
3app.use((req, res, next) => {
4 const sessionId = req.get("sessionId");
5 if (sessionId) trace.getActiveSpan()?.setAttribute("session.id", sessionId);
6 next();
7});1import (
2 "net/http"
3
4 "go.opentelemetry.io/otel/attribute"
5 "go.opentelemetry.io/otel/trace"
6)
7
8func withRumSession(next http.Handler) http.Handler {
9 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
10 if id := r.Header.Get("sessionId"); id != "" {
11 trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("session.id", id))
12 }
13 next.ServeHTTP(w, r)
14 })
15}
16
17// Wrap inside the tracing handler so the server span already exists:
18// otelhttp.NewHandler(withRumSession(mux), "server")1from flask import request
2from opentelemetry import trace
3
4@app.before_request
5def tag_rum_session():
6 session_id = request.headers.get("sessionId")
7 if session_id:
8 trace.get_current_span().set_attribute("session.id", session_id)To filter logs by session as well, add session.id as a field on your request logs.
The session ID is a debugging label, not a credential. Never use it for authentication or authorisation, and treat the incoming value as untrusted input.
Step 4: Find a session's backend calls#
- From a session to the backend: copy the session ID from RUM → Sessions, then filter APM traces (or logs) by
session.id. - From the backend to the session: open a backend span or log that carries
session.id, then look up that ID in RUM → Sessions to watch the replay.
Troubleshooting Correlation#
- No
session.idon backend spans- Check the browser's Network tab for the
sessionIdheader. The first requests on a page load may go out before the SDK has created the cookie. - For cross-origin APIs, make sure the preflight allows the
sessionIdheader. - Make sure the attribute is set after the tracing middleware has started the server span.
- Check the browser's Network tab for the
- No logs under the trace
- Ensure your logger attaches
<trace_id>/<span_id>and that log ingestion is enabled for the service. - Check sampling and log levels; the event might be filtered out.
- Ensure your logger attaches
- IDs don’t match between trace and logs
- A proxy/queue/async worker can start a new trace. Forward
traceparentheaders and bind server spans to incoming context.
- A proxy/queue/async worker can start a new trace. Forward
- The browser shows a long TTFB, but the service time is small
- Investigate CDN/WAF, upstreams, or network segments. Use headers (age, via, server-timing) to attribute the gap.
- No
browser.trace- Verify RUM init options; ensure CSP/ad-blockers aren’t disabling XHR/fetch instrumentation.
Need assistance or want to learn more about Middleware? Get in touch with us via our Contact Us or join our Slack channel.