How do I attribute the OpenAI API traces to end users?
Rius attributes OpenAI API calls to an end user when you wrap the request in a user scope: rius.user(...) in Python or withUser(...) in TypeScript. Every span started inside the scope carries user.id, including the spans the OpenAI integration creates for you. The Users page then shows each person’s traces, failures, cost and time to first token. You do not change your OpenAI call sites.
How it works
- The OpenAI integration traces each chat completion as a span with the model, token usage and messages. Rius computes cost from the model and tokens.
- A user scope stamps your own identifier as
user.idon every span started inside it. That includes spans made by auto-instrumentation, so the OpenAI spans are attributed without extra code. - User scopes follow async work and nest, with the innermost scope winning. They combine freely with a session scope.
observetakes no user argument, because a decorator is bound once at import time and a user belongs to each request. Put the scope around the request instead.- There is no process-wide default and no environment variable for the user. A span outside any user scope counts as anonymous.
- Rius also reads
enduser.idandenduser.pseudo.idand takes the first one that is set. The SDKs setuser.id.
The full scoping options are in Tracing your code.
Set it up
- Install the SDK with the OpenAI extra and call
init()once at startup, before you create the OpenAI client. The OpenAI integration page covers this setup in full.
pip install "glassflow-rius[openai]" openai- Wrap each request in a user scope, using the identifier your application already has for that person.
import rius
from openai import OpenAI
rius.init(api_key="ri_...", service_name="my-agent")
client = OpenAI() # use the client exactly as before
with rius.user(request.user_id):
handle(request) # every span of the request, OpenAI calls included, carries user.id- In TypeScript, use
withUseraround the same work.
import { withUser } from '@glassflow-ai/rius'
await withUser(request.userId, async () => {
await handle(request) // every span of the request carries user.id
})- Prefer an opaque identifier to 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 request inside a scope with a test ID such as
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 streamed OpenAI calls, pass
stream_options={"include_usage": True}. Without it, streamed spans have no token counts, so the user’s cost has nothing to add up.
What you get
The Users page lists the end users your agents serve, not the members of your Rius organization. The figures count only finished traces.
- The list: per user, the traces, failed traces and cost in a window of 15m, 1h, 1d, 7d (the default) or 30d, plus 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. Search by exact user ID to reach anyone else.
- The anonymous row: it always comes first. A line above the list tells you how many traces carry an end user, for example “400 of 471 traces carry an end user”. A large anonymous row means some code paths run outside a user scope.
- A user’s page: tiles for Traces, Failed traces, Error rate, Cost with average per trace, Sessions and TTFT p50. TTFT p50 is the median time to first token over that user’s streaming calls. The Python OpenAI integration records it automatically on streamed calls. TypeScript OpenAI streams carry no time to first token.
- Their recent runs: the 8 most recent traces and the 6 latest traces with an errored span, each opening the full trace. This takes you from a support ticket to the failing span in a few clicks.
- Who is costing what: sort by Cost, then compare a user’s cost per trace with the agent’s average. Costs are list-price estimates in US dollars.
- Filtering: the trace list has a User column, and the
user_idfield filters the list to one person. - A written summary: when root-cause analysis is on, an admin can click Summarise this user for a readable account of what that user experienced.
For how this fits with the rest of your monitoring, see Monitor AI agents in production.
When something else fits
- If one process handles exactly one person, such as a CLI run, the user scope still applies, because Rius offers no process-wide user default. Scope the work the same way.
- If you need to group the turns of one conversation rather than one person, use a session scope. The two combine, and a trace with no session counts as its own session.
- If you only want traces without personal identifiers, leave out the user scope. Those traces appear in the anonymous row.
Related
Last updated on