Skip to Content

How do I attach an end user to a trace with the OpenTelemetry JavaScript SDK?

Rius reads the user.id span attribute, so you attach an end user to a trace by setting user.id on every span you start with the OpenTelemetry JavaScript SDK. The standard exporter setup does not set it for you, so a span without it counts as anonymous. Add one setAttributes entry per span, and the trace shows up under that user in Rius.

How it works

  • user.id is a span attribute, not a resource attribute. Resource attributes such as service.name describe the process. user.id describes the person a request served, so it goes on spans.
  • Rius reads it per span. Set it on every span in the trace, root span included, not only on the first one.
  • If user.id is absent, Rius also reads enduser.id, then enduser.pseudo.id. Prefer user.id, which is the key the other Rius senders use.
  • A span with none of these keys is anonymous. The NodeSDK setup, the OTEL_EXPORTER_OTLP_* variables and OTEL_SERVICE_NAME do not add a user for you.
  • Use an opaque identifier, such as an internal id, rather than an email address.

The Vanilla OpenTelemetry SDKs page covers the exporter setup, and the span attribute reference lists every attribute Rius reads, including the session and user keys.

Set it up

  1. Send traces to Rius from Node.js with NodeSDK and the protobuf exporter. This is the shared setup from Vanilla OpenTelemetry SDKs. It reads the OTEL_EXPORTER_OTLP_* variables, so set those first as that page and Trace LLM calls with OpenTelemetry show.
npm install @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-proto @opentelemetry/api
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. Get the end user’s id from your own auth or session. Use a stable, opaque value, not an email.

  2. Add user.id to each span you start. Here it sits beside the attributes of a model-call span:

import { trace } from '@opentelemetry/api' const tracer = trace.getTracer('my-agent') const span = tracer.startSpan('chat gpt-4o') span.setAttributes({ 'user.id': 'user_8f3a2c', '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 the same 'user.id' entry on the span that wraps the whole agent run and on every other span you start, such as tool spans. Keep the value in one variable so every span in a request gets the same id. Replace callTheModel() and the id with your own code and value.

  2. If you also group traces into conversations, set session.id the same way, on every span.

What you get

  • Each end user your agents served appears on the Users page, with their traces, failures, cost and time to first token.
  • Cost is computed by Rius from gen_ai.request.model and the token counts on the model-call spans, so the per-user cost needs no extra attribute from you. Costs are list-price estimates in US dollars.
  • Traces from the same person are grouped under one user id, so you can see who hit an error or ran up cost.
  • Spans where you leave out user.id stay in the trace but are counted as anonymous, which makes a missed span easy to spot.

When something else fits

  • If you use the Rius SDK for TypeScript instead of raw OpenTelemetry, withUser stamps user.id on the spans in its scope for you, so you do not set it span by span.
  • If your instrumentation already writes enduser.id or enduser.pseudo.id, Rius reads those when user.id is absent, so you can leave them as they are.

Still stuck? Get help

Last updated on