subtext.session.url, opens that person’s real session, and walks the decisive moments with screenshots, the component tree, network, and console. OpenTelemetry detects; Subtext diagnoses.
This guide assumes the Subtext capture snippet is installed and capturing sessions, the OpenTelemetry JS web SDK is running in your browser app (
@opentelemetry/sdk-trace-web plus fetch/XHR instrumentation), and you have an OTLP-compatible tracing backend. The examples use Grafana Cloud / Tempo, but any OTLP backend works.Attach the session URL
Session replay is your front-end telemetry; OpenTelemetry is your back-end telemetry, and wiring the seam links them both directions. Put the Subtext session id in Baggage so it propagates to the backend, stamp the clickable session URL on the root span, and — so the link runs the other way too — stamp the active trace id back onto the session. Elsewhere in these guides the key issubtext_url; on OpenTelemetry spans it follows OTel attribute-naming conventions as subtext.session.url / subtext.session.id. Keep the browser footprint minimal; you are not replacing session replay, only linking to the trace.
Set up browser tracing
Register aWebTracerProvider with the W3C trace-context and Baggage propagators, and scope trace-header injection to your own API origins.
Put the session id in Baggage
Once capture has started, putsubtext.session.id in Baggage so it propagates to the backend. The format: 'id' option returns the bare session id — see Fullstory’s Get Session Details docs. Wrap the code that should carry the session id forward — context.with() only threads the baggage into the callback you pass it.
FetchInstrumentation request hook) so every outbound request carries subtext.session.id, not just those rooted in the initial render.
Set the session URL on the root span
Stamp the clickablesubtext.session.url on the root span with a SpanProcessor, which keeps it out of the propagated header. Never put the URL in Baggage.
traceparent and a baggage: subtext.session.id=... header.
Promote Baggage onto backend spans
So every server span carries the session id, add the Baggage span processor (OTel contrib) in each backend service.BaggageSpanProcessor only has baggage to read if the Node tracer extracts the incoming baggage header. The OpenTelemetry JS NodeSDK does this by default (OTEL_PROPAGATORS defaults to tracecontext,baggage); if you’ve narrowed the propagators, add the W3C Baggage propagator back or subtext.session.id never enters server context.
Stamp the trace id back onto the session
Stamp the active trace id onto the Fullstory session so a session action deep-links to the backend trace. This is your app’s instrumentation, not automatic capture-snippet behavior — call it wherever your code can see the trace you want linked. It stamps oneotel_trace event per call.
FS('getSession', { format: 'id' }), FS('observe'), and FS('trackEvent') are Fullstory Browser API operations not covered in Subtext’s API pages — see the Fullstory developer docs and verify them against your installed snippet version. Likewise verify the @opentelemetry/* import paths and the @opentelemetry/baggage-span-processor exports (BaggageSpanProcessor, ALLOW_ALL_BAGGAGE_KEYS) against your installed versions; the JS and contrib API surfaces shift between releases.Interesting traces (errors, high latency, session-flagged) must survive sampling. Use tail-based sampling at the OpenTelemetry Collector — keep 100% of error / high-latency traces and sample the rest — rather than dropping traces head-side in the browser.
From signal to session
However you first cross the seam — an alert, a dashboard panel, or a session someone already has open — the path is the same: get to thesubtext.session.url, then hand it to your agent for a Detect-vs-Diagnose read. The trace tells you a span crossed a latency threshold or errored; the session tells you whether a real person was actually blocked, what they saw, and the precise sequence behind it. Because subtext.session.id rides in Baggage and lands on every backend span for a request, you can also search Tempo by session id for every trace generated during one session:
A Grafana latency alert at 2am (trace → session)
A Grafana latency alert at 2am (trace → session)
A Grafana alert pages you (PagerDuty, Slack, whatever’s wired up): “P99 latency on checkout-service > 2s for 5m.”Open the alert, jump to Explore → Tempo, and run a TraceQL query scoped to the alerting window for slow, session-tagged traces:That single-span form matches when the root span is the slow one. If the slowness is in a child span, split it into two spansets so TraceQL matches across spans in the same trace:Open a matching trace, find the root span — that’s the one carrying the attribute — and copy
subtext.session.url. Then hand it to your agent:A spike in checkout errors in Slack (trace → session)
A spike in checkout errors in Slack (trace → session)
A Slack alert posts: “Error rate on POST /checkout up 4x in the last 15 minutes.”In Grafana → Explore → Tempo, run:Since Open a handful of the matching traces, copy
subtext.session.url lives on the root span only, this matches only traces where the root span itself errored. To also catch a session-tagged trace where the error surfaced on a child span, split the conditions into separate spansets:subtext.session.url off each, and review them together:A support ticket that only makes sense from the session (session → trace)
A support ticket that only makes sense from the session (session → trace)
A customer writes in: “Checkout just hung on me, I had to refresh.” Support already has the Subtext session link for that visit — from the in-app support widget or a saved bookmark — but no trace id yet.Open the session in Subtext and step through it up to the point it hangs. Find the
otel_trace custom event on the session timeline — your app’s instrumentation stamps one onto the session for the trace it wants linked. Open its properties and copy trace_id (and span_id if you want the exact span). In Grafana → Explore → Tempo → TraceQL tab, paste the trace id directly into the query field to jump straight to that trace — an exact id lookup, no TraceQL query needed.An incident postmortem, drilling from a dashboard (trace → session)
An incident postmortem, drilling from a dashboard (trace → session)
During or after an incident, a Grafana dashboard panel shows a clear anomaly window — a latency or error-rate spike correlated with a drop in conversions.From the dashboard panel, drill down to Explore → Tempo, scope the TraceQL query to the incident window, and filter for session-tagged traces inside the spike:For a pure latency spike, swap in the
duration > <threshold> filter from the first scenario; use the split-spanset variant if the error or slow span surfaced on a child span. Open a trace inside the window, copy subtext.session.url, and hand it over:For agents
An autonomous agent can run the whole loop against your tracing backend directly: discover the error or high-latency traces, extract the stampedsubtext.session.url, and hand each session to the Subtext MCP. Grafana ships a first-party MCP server two ways: the open-source grafana/mcp-grafana, which you run locally (stdio via uvx mcp-grafana, a downloaded binary, or go install) or self-host (Streamable-HTTP/SSE via Docker or the Helm chart) authenticated with GRAFANA_URL + GRAFANA_SERVICE_ACCOUNT_TOKEN; and, for Grafana Cloud, a hosted MCP server at https://mcp.grafana.com/mcp (Streamable HTTP, OAuth 2.1, public preview).
1
Connect the Grafana MCP and confirm the trace tools
Trace search isn’t built into
mcp-grafana — it’s proxied from Tempo’s own MCP server, exposed as tempo_-prefixed tools only when your Tempo datasource has that server enabled (query_frontend.mcp_server.enabled) and mcp-grafana wasn’t started with --disable-proxied. List the live server’s tools first; if tempo_traceql-search isn’t there, that’s why — fall back to the same TraceQL a human pastes into Grafana Explore. Use tempo_get-attribute-names / tempo_get-attribute-values to confirm subtext.session.url is actually indexed before you query on it.2
Discover error or slow traces
Run Because
tempo_traceql-search with the same queries a human would paste into Explore:subtext.session.url lives on the root span only, neither matches a trace where the error or slow span is a child span. Split into two spansets so TraceQL matches across spans in the same trace:3
Extract the session URL (root span only)
If the search response already carries the root span’s attributes, read
subtext.session.url off it directly. Otherwise — or if you matched via the split-spanset query — call tempo_get-trace(trace_id) for the full trace, find the root span (no parent span reference), and read subtext.session.url from its attributes. Verify the exact response shape (attribute key casing, nesting) against a live call.4
Hand off to Subtext
For each URL, call
review-open(session_url=<subtext.session.url>) on the Subtext MCP. The root-span URL is captured with url.now, so the review opens deep-linked to the moment the trace ran.subtext.session.url and no trace id yet): review-open the session, walk the timeline to the otel_trace custom event, read trace_id (and span_id) off its properties, then tempo_get-trace(trace_id) for an exact id lookup — no TraceQL query needed.
The examples use Grafana Cloud / Tempo, but OpenTelemetry is vendor-neutral — spans and attributes flow to whatever OTLP backend you run. Honeycomb, for instance, ships its own hosted MCP server (plus a Query Data API); the pattern is identical: discover traces filtered on
subtext.session.url != "" plus your latency/error condition, extract the attribute, then run the same review-open hand-off. subtext.session.url only round-trips if it was attached to the root span at capture time, and TraceQL syntax and the mcp-grafana / proxied tempo_ tool names and params shift between releases — confirm both against your current server before wiring an automation on top of this recipe.Related
- Session Review overview — what your agent does once a session is open.
- Install the capture snippet — required before any session URL or id exists to attach.

