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.idis a span attribute, not a resource attribute. Resource attributes such asservice.namedescribe the process.user.iddescribes 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.idis absent, Rius also readsenduser.id, thenenduser.pseudo.id. Preferuser.id, which is the key the other Rius senders use. - A span with none of these keys is anonymous. The
NodeSDKsetup, theOTEL_EXPORTER_OTLP_*variables andOTEL_SERVICE_NAMEdo 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
- Send traces to Rius from Node.js with
NodeSDKand the protobuf exporter. This is the shared setup from Vanilla OpenTelemetry SDKs. It reads theOTEL_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/apiimport { 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()-
Get the end user’s id from your own auth or session. Use a stable, opaque value, not an email.
-
Add
user.idto 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()-
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. ReplacecallTheModel()and the id with your own code and value. -
If you also group traces into conversations, set
session.idthe 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.modeland 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.idstay 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,
withUserstampsuser.idon the spans in its scope for you, so you do not set it span by span. - If your instrumentation already writes
enduser.idorenduser.pseudo.id, Rius reads those whenuser.idis absent, so you can leave them as they are.