How do I attribute the Claude Agent SDK traces to end users?
Rius attributes Claude Agent SDK traces to end users when you wrap each task call in rius.user with your own user id. Every span started inside that block carries the id as user.id, so the trace shows up on the Users page under that person. The Users page then shows each user’s traces, failures, cost and sessions.
How it works
- The user is a scope, not an argument.
rius.user(user_id)stampsuser.idon every span started inside the block.observetakes no user argument, because a decorator is bound once at import time and a user belongs to each request. - The scope must surround the task call. Your
observewrapper makes each task the rootAGENTspan, and the manual generation around thequery()loop is a child of it. Call the wrapped function inside thewithblock and both spans carry the id. - No environment variable and no default. There is no process-wide user, so a span outside any user scope is anonymous. This keeps one process from attributing every request to one person.
- Scopes combine. User scopes nest (the innermost wins) and work alongside
rius.session(). The Claude Agent SDK’s ownsession_idmakes a natural Rius session id, which groups a person’s turns into one conversation. - Rius picks the user from the span. It reads
user.id, thenenduser.id, thenenduser.pseudo.id, and takes the first one that is set. A trace with no end user on any span counts as anonymous.
The setup for tracing the framework itself is on the Claude Agent SDK page. The user and session scopes are described in Tracing your code.
Set it up
- Trace your task as the integration page shows: an
observewrapper withkind=SpanKind.AGENT, and a manual generation that sets usage from theResultMessage. Use a full model id such asclaude-haiku-4-5, because cost is computed from it. - Pick an identifier you already have for the person. Prefer an opaque id to an email or a name, because the value is stored on every span and shown in the console. If only sensitive identifiers exist, hash them first.
- Wrap the call to your task in
rius.user. Hererequest.user_idstands for your own request object andhandle_taskis the wrapped function from the integration page.
import rius
async def serve(request):
with rius.user(request.user_id):
return await handle_task(request.task) # every span of the task carries user.id- If a user has several conversations, add a session scope inside the user scope with
rius.session(conversation_id). Pass your own conversation id, or the SDK’ssession_idfor a resumable conversation. - Verify in one minute. Run one task inside a user scope with a test id such as
test-user-1. Open the Users page, typetest-user-1, click Find, and open the row. Once the trace has finished, the page shows 1 trace, your agent under Agents that served this user, and the trace under Recent traces.
Place the scope around each request, not around the whole program. Any task started outside a scope counts as anonymous.
What you get
The Users page is a register of the end users your agents serve. The figures count only finished traces. For each user you get:
- The list. Traces, failed traces and cost in the window (15m, 1h, 1d, 7d by default, or 30d), the agents that served them, and when they were last seen. Sort by traces, failed traces or cost. The list shows the top 50 for the chosen sort, so search by exact user ID to reach anyone else.
- A user’s page. Tiles for Traces, Failed traces, Error rate, Cost with the average per trace, Sessions, and TTFT p50 over streaming LLM calls. Below them are the agents that served the user, their 8 most recent traces, and their 6 latest traces with an errored span.
- Cost per person. Each task’s generation carries tokens and the model, and Rius computes the cost from them. Sort the list by Cost to see who drives spend, and compare a user’s cost per trace with the agent’s average on the Agents page. Costs are list-price estimates in US dollars.
- Support lookups. When a customer says it did not work, search their exact ID, open Failed traces, and open the trace from the time they wrote in.
- Coverage. The line above the list says how many traces carry an end user, for example 400 of 471. A large anonymous row means some code paths run outside a user scope.
- Filters elsewhere. The trace list has a User column, and the
user_idfilter works in the filter builder.
The trace you open for a user shows the root AGENT span and one generation. Tool calls and sub-turns inside the Claude Code runtime produce no spans, so the user’s page reflects each task’s totals.
When something else fits
- If you only need to group the turns of one conversation, a session scope with
rius.session()is enough, and no user scope is needed. - If a trace should be tied to a person but you cannot send an id, the trace stays anonymous. Hash the identifier and send that instead.
- For a broader view of agent health beyond users, see Monitor AI agents in production.