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.

PlatformHTTP eventsTrace propagationWhat you have to do
BrowserAutomaticEvery URL by defaultNothing
iOSAutomatic (URLSession)Every URL by defaultNothing
React NativeAutomatic (fetch/XHR)Every URL by defaultNothing
AndroidManualEvery URL, once instrumentedRoute 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-v2 span), preserving <trace_id> and generating child <span_id> values.
    • Spans are sent to the Middleware’s trace store.
  • 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.
  • Index & Link
    • Middleware indexes RUM events, traces, and logs on <trace_id> (and also on session.id/time).
    • From a RUM event, the UI can unambiguously fetch the trace and the logs that share the duplicate IDs.

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.

RUM flame graph showing stacked spans across services for a user 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:

AttributeMeaningExample
event.typeBrowser event typexhr
ttp.method, http.url, http.status_codeCore request dataGET https://<host>/api/v1/account/init → 200
traceId, spanIdCorrelation keys across RUM, trace, and logs<trace_id>, <span_id>
browser.traceBrowser tracing enabledtrue
resource.durationBrowser-observed total resource time369.3 ms
resource.first_byte.durationTime to first byte from server/CDN366.5 ms
resource.download.durationTime to read the response2.8 ms
resource.size / resource.transfer_sizeUncompressed vs on-wire size14708 B / 5716 B
http.response_content_lengthServer-reported length5416 B
project.name, mw.agent_idProject/agent identity<project_name>, <agent_id>
browser.name/version, osClient environmentChrome 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.

Button/interface to jump from RUM session trace to corresponding backend logs

Open a log row to inspect its structured fields.

Detailed log properties panel showing service, trace ID, span ID, method, path, duration, and response info

Key fields you’ll read here:

FieldWhy it mattersExample
service.nameWhich service emitted the logapp-api-v2
method, path, response_statusConfirms HTTP behaviourGET, /api/v1/account/init, 200
durationService-reported time24 ms
traceId / spanIdMust match the Trace view<trace_id> / <span_id>
response-body-sizeWhat the service sent0 B / 14 K
ip, user_idOrigin context (if logged)<ip>, <user_id>
project.name, mw_agent, telemetry.sdk.languageRuntime & 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 duration is small but resource.first_byte.duration is 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).

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).

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#

PropertyValue
Cookie nameStarts with _mwRumSessionId_. The suffix is the SDK's cookie format version, so match on the prefix.
Cookie valueURL-encoded JSON: {"id":"<session_id>","startTime":<epoch_ms>}
id32-character hex string, the same value as session.id on RUM events
Scopepath=/ on the host that loads the SDK; readable from JavaScript (not HttpOnly)
LifetimeExtended 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 sessionId to your API's Access-Control-Allow-Headers response 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.id on backend spans
    • Check the browser's Network tab for the sessionId header. 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 sessionId header.
    • Make sure the attribute is set after the tracing middleware has started the server span.
  • 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.
  • IDs don’t match between trace and logs
    • A proxy/queue/async worker can start a new trace. Forward traceparent headers and bind server spans to incoming context.
  • 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.