Skip to Content

How do I group spans into sessions with the OpenTelemetry JavaScript SDK?

Rius groups spans into sessions when you set the session.id attribute on every span your OpenTelemetry JavaScript code creates. The standard Node.js setup does not set it for you, so you add it where you start each span. Spans that carry the same value land in the same session, such as one conversation or one agent run.

How it works

The key is session.id, a string attribute on the span. It is a span attribute, listed apart from resource attributes such as service.name, because Rius reads it span by span.

  • The default setup does not set it. The NodeSDK with the OTLP exporter sets up the tracer provider and exporter, and nothing in it adds a session value. Without one, each trace is its own session.
  • Set it on every span, root included. A span without session.id is not covered by the spans that have it. Rius reads the attribute per span, so a session id on the root alone leaves the child spans out of the session.
  • A fallback exists. If a span has no session.id, Rius reads gen_ai.conversation.id, the session key of the OpenTelemetry GenAI conventions. If your spans already follow those conventions, they group into sessions without any change. If neither is set, the span is grouped by its trace id.
  • Choose a value that is stable for the whole session. Use the same string for every trace of one conversation or run, for example your own conversation id.

The span attribute reference lists session.id, gen_ai.conversation.id and user.id under sessions and users.

Set it up

  1. Register a tracer provider that exports to Rius. This is the shared setup, covered in full on Vanilla OpenTelemetry SDKs, which also lists the exporter environment variables the exporter reads. Install the packages with npm install @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-proto @opentelemetry/api, then start the SDK and shut it down before the process exits:
import { NodeSDK } from '@opentelemetry/sdk-node' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' const sdk = new NodeSDK({ // No url or headers in code, so the exporter reads OTEL_EXPORTER_OTLP_* traceExporter: new OTLPTraceExporter(), // Rius ingests traces only metricReaders: [], logRecordProcessors: [], }) sdk.start() // ... your agent code ... // Flushes queued spans before the process exits (top-level await needs an ES module) await sdk.shutdown()
  1. Pick a session id. Use one string per conversation or agent run. Keep it in a variable your code can reach wherever it starts a span.

  2. Add session.id to each span you create. Put it in the same setAttributes call as the other attributes. This is the LLM span example from the OpenTelemetry guide with the session attribute added. The value conversation-123 is your own:

import { trace } from '@opentelemetry/api' const tracer = trace.getTracer('my-agent') const sessionId = 'conversation-123' const span = tracer.startSpan('chat gpt-4o') span.setAttributes({ 'session.id': sessionId, 'openinference.span.kind': 'LLM', 'gen_ai.operation.name': 'chat', 'gen_ai.request.model': 'gpt-4o', 'gen_ai.provider.name': 'openai', }) const response = await callTheModel() span.setAttributes({ 'gen_ai.usage.input_tokens': response.usage.prompt_tokens, 'gen_ai.usage.output_tokens': response.usage.completion_tokens, 'gen_ai.response.model': response.model, }) span.end()
  1. Repeat it on the other spans of the run. Do the same on the root span that wraps the run and on every child span, such as tool and agent spans.

  2. Optionally add user.id. Set it the same way to see which end user a request served. Prefer an opaque id to an email.

If you use the Rius TypeScript SDK instead of raw OpenTelemetry, its withSession scope stamps the session attribute for you. See Trace LLM calls with OpenTelemetry for choosing between the SDK and a vanilla setup.

What you get

  • Traces that share a session.id are grouped as one session in Rius, so a multi-turn conversation reads as one unit.
  • Because the id sits on every span, the grouping holds for the whole trace, not only the root.
  • If you also set user.id, the same traces roll up under each end user with their failures and cost.
  • Spans that already carry gen_ai.conversation.id group into sessions with no code change.
  • Your attributes arrive as sent. Cost is computed by Rius from the model name and token counts, so you never send a cost value alongside the session.

When something else fits

  • If you run the Rius TypeScript SDK, use its session scope, so you do not repeat the attribute at each span.
  • If your spans already follow the GenAI conventions, gen_ai.conversation.id is enough and you do not need to add session.id.
  • If you leave the session unset, each trace is its own session, which suits one-shot runs that have no conversation to group.

Still stuck? Get help

Last updated on