Try it free

Conversation ID and agent session tracking with RUM and OpenTelemetry

  • Latest Dynatrace
  • Tutorial
  • 10-min read
  • Published Jul 14, 2026

Agentic AI applications typically involve multiple back-and-forth exchanges within a single user session. Each exchange is a separate HTTP request with its own trace ID, which makes it difficult to answer questions like: "What did this user's session look like end-to-end?" or "Which LLM calls are related to the same conversation?"

This tutorial shows how to connect Real User Monitoring (RUM) with backend AI agent spans using a gen_ai.conversation.id attribute. Then, you can use a single DQL query to follow a user's click all the way through the LLM response and across every follow-up question in the same session.

What will you learn?

  • How Dynatrace RUM automatically injects W3C traceparent headers so that browser user-action spans are the parent of backend LLM spans.
  • How to generate and propagate a gen_ai.conversation.id that groups all AI interactions across a session into a single queryable identifier.
  • How to use a staging span processor and exporter wrapper to override framework-generated attributes without losing your own values.
  • How gen_ai.conversation.id aligns with dt.rum.session.id to enable the frontend link in distributed traces.

Before you begin

Prerequisites

  • Python 3.11+
  • uv
  • Node.js 18+ (for the optional Next.js frontend)
  • A Dynatrace environment with Real User Monitoring (RUM) enabled
  • At least one of:
    • AWS credentials with Amazon Bedrock model access (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
    • An Azure OpenAI resource (AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_OPENAI_DEPLOYMENT)
  • A Dynatrace API token with the following scopes: openTelemetryTrace.ingest, metrics.ingest

Sample application

The code for this tutorial is available in the rum/opentelemetry directory of dynatrace-oss/dynatrace-ai-agent-instrumentation-examples.

The application is a music history chatbot that routes requests across AWS Bedrock and Azure OpenAI, instrumented with pydantic-ai native OpenTelemetry support.

Steps

Follow these steps to set up conversation and session tracking in Dynatrace.

1. Configure RUM in your Dynatrace environment

  1. In Experience Vitals Experience Vitals, select New frontend > Web and provide a frontend name.

  2. In the Select instrumentation method step, select Agentless and then select Create.

  3. In the Setup step, under Select capability and settings, verify that RUM is enabled. If it is not enabled, select Override and turn it on.

  4. Select Next to copy the JavaScript tag URL. This is the value for DT_RUM_SCRIPT in your .env file.

2. Set up your environment variables

Create an .env file in rum/opentelemetry/ according to the following snippet.

DT_ENDPOINT=https://<your-env-id>.live.dynatrace.com
DT_API_TOKEN=dt0c01.<your-token>
DT_RUM_SCRIPT=https://js-cdn.dynatrace.com/jstag/<your-tag>.js
# At least one of the following provider groups is required:
AWS_ACCESS_KEY_ID=<aws-access-key-id>
AWS_SECRET_ACCESS_KEY=<aws-secret-access-key>
AZURE_OPENAI_ENDPOINT=https://<your-resource>.openai.azure.com/
AZURE_OPENAI_API_KEY=<your-key>
AZURE_OPENAI_DEPLOYMENT=<deployment-name>

3. Run the application

There are two ways to run the sample application: HTML or Next.js.

Vanilla HTML frontend

  1. Run the following commands in a terminal.

    cd rum/opentelemetry
    make install
    make run
  2. Open http://localhost:8000. The FastAPI server serves the HTML page directly.

Next.js frontend

  1. First, start the FastAPI backend. Run the following commands in a terminal.

    cd rum/opentelemetry
    make install
    make run # listens on port 8000
  2. In a second terminal, start Next.js.

    cd rum/opentelemetry/nextjs-frontend
    npm install
    npm run dev # listens on port 3000
  3. Open http://localhost:3000. Next.js proxies all /api/* requests to the FastAPI backend.

How conversation and session tracking works

Data flow overview

Once you've configured conversation and session tracking, here's what will happen when a user submits a question.

  1. RUM JavaScript intercepts the outgoing fetch() call.
  2. RUM generates a fresh W3C traceparent header and injects it into the request.
  3. The FastAPI backend extracts the traceparent and starts a backend span as a child of the browser user-action span. Both share the same traceId.
  4. The backend sets gen_ai.conversation.id directly on the root span and propagates it to all child LLM spans via a span processor and exporter wrapper that override whatever the AI framework sets.
  5. Dynatrace ingests both the RUM session data and the conversation ID, linking them by shared trace.id and dt.rum.session.id.

Because every question in a session carries the same gen_ai.conversation.id, you can reconstruct the full agent trajectory across multiple traces with a single DQL query.

Conversation ID propagation

Propagating a conversation ID through a Pydantic AI application requires three components: a frontend that generates and stores the UUID, a backend context variable that carries it through each request, and a two-stage span processor and exporter that write the correct value to gen_ai.conversation.id before spans are exported.

  1. The frontend generates a UUID once per browser session and stores it in sessionStorage. Every request body includes this value as conversation_id.

    const CONV_ID = sessionStorage.getItem('conversationId') || crypto.randomUUID();
    sessionStorage.setItem('conversationId', CONV_ID);
  2. A ContextVar carries the conversation ID for the duration of each request.

    _current_conversation_id: ContextVar[str | None] = ContextVar("current_conversation_id", default=None)
  3. A ConversationIdSpanProcessor stages the value on every span as it starts, using a private attribute name that the AI framework won't overwrite.

    _STAGING_ATTR = "_rum_session_id"
    class ConversationIdSpanProcessor(SpanProcessor):
    def on_start(self, span: Span, parent_context=None) -> None:
    conversation_id = _current_conversation_id.get()
    if conversation_id:
    span.set_attribute(_STAGING_ATTR, conversation_id)
  4. A SessionIdExporter wrapper copies the staged value to gen_ai.conversation.id immediately before spans leave the process, overriding whatever the framework set.

    class SessionIdExporter(SpanExporter):
    def export(self, spans: list[ReadableSpan]) -> SpanExportResult:
    for span in spans:
    attrs = getattr(span, "_attributes", None)
    if not attrs:
    continue
    session_id = attrs.get(_STAGING_ATTR)
    if session_id:
    attrs["gen_ai.conversation.id"] = session_id
    del attrs[_STAGING_ATTR]
    return self._inner.export(spans)

    Pydantic AI sets gen_ai.conversation.id on its own spans after on_start runs, so staging in a private attribute and overriding at export time ensures the correct value is always written.

Conversation ID and RUM session ID

Once RUM JavaScript is initialized, the frontend replaces the UUID with the real RUM session ID. This makes gen_ai.conversation.id equal to dt.rum.session.id, so both the AI Observability AI Observability session filter and the Experience Vitals Experience Vitals session share the same identifier.

const rumPoll = setInterval(() => {
if (typeof window.dtrum === 'undefined') return;
clearInterval(rumPoll);
const rumId = window.dtrum.getSessionId?.();
if (rumId) {
CONV_ID = rumId;
sessionStorage.setItem('conversationId', rumId);
}
window.dtrum.sendSessionProperties?.(undefined, undefined, { conversationId: CONV_ID });
}, 200);

Independently of getSessionId(), Dynatrace always propagates dt.rum.session.id from the RUM JavaScript tracestate header to every backend span. When Dynatrace sees this attribute on a span, it creates a frontend link from the distributed trace view directly to the corresponding RUM session in Experience Vitals Experience Vitals.

AttributeSet byMeaning

gen_ai.conversation.id

Sent from frontend UUID, to request body, to backend span processor

Groups all LLM spans from a single browser session

dt.rum.session.id

Dynatrace RUM JavaScript via tracestate header

Powers the frontend link in Distributed Tracing Distributed Tracing

session.id

AI Observability AI Observability

Alias for gen_ai.conversation.id in AI Observability AI Observability

Signals captured

SignalSourceDQL attribute

Browser user actions

RUM JavaScript

useraction.name, useraction.duration

End-to-end trace link

W3C traceparent (sent from RUM to the backend)

Shared traceId

Session grouping

conversation_id in request body

gen_ai.conversation.id

LLM provider and model

pydantic-ai span attributes

gen_ai.provider.name, gen_ai.request.model

Token usage

pydantic-ai and the backend span

gen_ai.usage.input_tokens, gen_ai.usage.output_tokens

User feedback

/api/feedback OTel span

feedback.rating, feedback.question

Congratulations!

Now that you've got your application running, you can explore session data, traces, and AI signals in Dynatrace.

AI Observability

In AI Observability AI Observability, select the Explorer tab and then select the rum/opentelemetry service to see LLM request counts, token usage, and latency for the chatbot. Drill into Prompts and traces to inspect individual exchanges.

To narrow down to a single user session, select the Prompts tab and filter by Session (which maps to gen_ai.conversation.id). All LLM interactions that belong to the same browser session are listed together, even though they span multiple trace IDs.

Selecting any prompt opens the full agentic trace with the system prompt, input, model output, token usage, and the Agents topology view. This shows the call chain from the root span, through the Pydantic AI agent, to the LLM provider.

For more information about the views, see AI Observability app.

Experience Vitals

Because the RUM JavaScript injects a W3C traceparent header into every fetch() call, Dynatrace links the browser user-action span to the backend spans by shared traceId. In Experience Vitals Experience Vitals, open the page load waterfall for a session and select a request to the backend. When a distributed trace is available, the details include a View trace option that opens the corresponding span in Distributed Tracing Distributed Tracing.

This gives you the full picture in one click: user action duration, active sessions, and click events in Experience Vitals Experience Vitals, with a direct path into the AI agent trace on the backend.

For more information about frontend-to-backend linking, see Analyze performance from frontend to backend.

Query with DQL

To reconstruct the full agent trajectory across all requests in a session, run the following query:

fetch spans
| filter gen_ai.conversation.id == "<paste-from-UI>"
| fields timestamp, span.name, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, feedback.rating
| sort timestamp asc

You can find the Conversation ID in AI Observability AI Observability as the gen_ai.conversation.id attribute.

The Copy for DQL button in the sample application copies this query to your clipboard, pre-filled with the current session's Conversation ID.

Related topics

  • Real User Monitoring
  • Frontend-backend linking
  • Configure frontend-backend linking for web frontends
  • Customize web frontend monitoring using the JavaScript API
  • Analyze performance from frontend to backend
  • AI Observability app
Related tags
AI Observability