How do I attribute CrewAI traces to end users?
Rius attributes CrewAI traces to an end user when you run the crew inside a rius.user scope, which stamps user.id on every span of the request. Wrap each request that kicks off the crew, passing your own identifier for the person. The Users page then shows each user’s traces, failures, cost and time to first token. Spans the openai integration creates for the crew’s model calls carry the user too.
How it works
rius.user(user_id)scopes a user around a block of code. Every span started inside it carriesuser.id, including the spans that auto-instrumentation creates for CrewAI’s model calls and your ownobservespans for the run and its tools.observetakes no user argument, because a decorator is bound once at import time and a user belongs to each request. Put the scope at the call site, around the function that callscrew.kickoff(), not on the decorator.- Scopes follow async work and nest, and the innermost one wins. They combine freely with a session scope. See Tracing your code for the full scoping rules.
- There is no process-wide default and no environment variable for the user, by design. A global user would attribute every request a process handles to one person.
- A span outside any user scope is anonymous. A trace with no end user on any span counts as anonymous on the Users page.
- Rius also accepts
enduser.idandenduser.pseudo.idand uses the first of the three that is set, so a span from another source can name its user too. - CrewAI’s crews and tasks produce no spans of their own, so a user’s page shows their runs and model calls, not which task ran. Wrap per-task entrypoints with
observeif you need that. The CrewAI integration page covers what lands in the trace.
Set it up
- Install the SDK with the OpenAI integration, which captures CrewAI’s model calls, and initialize it as the CrewAI page describes:
pip install "glassflow-rius[openai]" crewai-
Keep your entrypoint as the CrewAI integration page shows it, with
observeonhandle_ticketandkind=SpanKind.TOOLon each tool body. -
Scope the user around the call to that entrypoint, once per request. Use the identifier your application already has for the person:
import rius
with rius.user(request.user_id):
answer = handle_ticket(ticket) # every span of the run carries user.id-
If one conversation spans several kickoffs, add a session scope next to the user scope with
rius.session(conversation_id), so the user’s page can count sessions. -
Send an opaque identifier, not an email or a name. The value is stored on every span and shown in the console. If only sensitive identifiers exist, hash them first.
-
Verify in one minute. Run one ticket inside
rius.user("test-user-1"). Open Users, 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.
For the rest of the setup, see Tracing your code. To keep the whole picture in one place, start from Monitor AI agents in production.
What you get
- A register of your end users. The Users list covers a window of 15m, 1h, 1d, 7d (the default) or 30d. It shows traces, failed traces and cost per user, the agents that served them, and when they were last seen. Sort by traces, failed traces or cost. The list shows the top 50 users for the chosen sort, so search by exact user ID to reach anyone else.
- One page per user. Tiles show Traces, Failed traces, Error rate, Cost with the average per trace, Sessions, and TTFT p50, the median time to first token over that user’s streaming model calls.
- A path from a support ticket to the failing span. Search the exact user ID, open their page, check Failed traces, and open the trace from the time they wrote in. The page lists their 8 most recent traces and their 6 latest traces with an errored span.
- Cost per customer. Sort by Cost to see who drives spend, and compare a user’s cost per trace with the agent’s average on the Agents page.
- A coverage check. The line above the list, such as “400 of 471 traces carry an end user”, tells you how much of your traffic is attributed. A large anonymous row means some code paths run outside a user scope.
- Filtering elsewhere. The trace list has a User column and a
user_idfilter, and clicking a user filters the list to them.
The figures count only finished traces, so a run appears once every span of it has ended.
When something else fits
- If you send traces through plain OpenTelemetry instead of the Rius SDK, set
user.id,enduser.idorenduser.pseudo.idon the spans yourself. - If one process serves exactly one person, you still scope the user in code, because the user has no environment variable.
- If you want to know who a coding-agent session belongs to, the Rius plugin from version 0.6.2 carries the developer’s sign-in email as the user, so you set nothing yourself.