User Tracking
User tracking lets you attribute agent runs, LLM costs, and token usage to individual end users of your application. This enables per-user cost analysis, abuse detection, and usage pattern insights.
What Gets Tracked
When runs include a user_id, Waxell Observe aggregates per-user metrics:
| Metric | Description |
|---|---|
run_count | Total runs by this user |
first_seen | Timestamp of the user's first run |
last_seen | Timestamp of the user's most recent run |
total_duration | Combined execution time across all user runs (seconds) |
total_cost | Sum of LLM costs for this user (USD) |
total_tokens | Sum of tokens consumed by this user |
agents | List of distinct agents this user has interacted with |
cost_by_model | Cost and token breakdown per LLM model |
Setting a User ID
Recommended: @observe with Auto-Instrumentation
Call waxell.init() before importing your LLM SDK and pass user_id at call time. Every LLM call inside the decorated function is auto-captured and attributed to that user:
import waxell_observe as waxell
waxell.init() # BEFORE importing the LLM SDK
import openai
client = openai.OpenAI()
@waxell.observe(agent_name="support-agent")
async def handle_ticket(ticket_id: str) -> str:
response = client.chat.completions.create( # auto-captured
model="gpt-4o",
messages=[{"role": "user", "content": f"Resolve ticket {ticket_id}"}],
)
return response.choices[0].message.content
# Pass user_id at call time -- attributes cost and runs to this user
await handle_ticket("ticket-001", user_id=f"user-{request.user.id}")
Combined Session and User Tracking
In most applications, you pass both session_id and user_id at call time:
@waxell.observe(agent_name="chat-agent")
async def generate_reply(message: str) -> str:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": message}],
)
return response.choices[0].message.content
await generate_reply(
message,
session_id=f"conv-{conversation.id}",
user_id=f"user-{request.user.id}",
)
Privacy best practice: Use opaque internal identifiers (database IDs, UUIDs) as user_id values. Do not pass email addresses, names, or other personally identifiable information. The user_id field is stored in plain text and is visible in the Waxell UI and API responses.
REST API
List Users
GET /api/v1/observability/users/
Authentication: Session (UI)
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
search | string | Filter by user_id (substring match) | |
agent | string | Filter by agent name | |
start | ISO8601 | Only users with runs after this time | |
end | ISO8601 | Only users with runs before this time | |
sort | string | -last_seen | Sort field. Options: last_seen, -last_seen, first_seen, -first_seen, run_count, -run_count |
limit | int | 25 | Page size (max 100) |
offset | int | 0 | Pagination offset |
Example:
curl -s "https://acme.waxell.dev/api/v1/observability/users/?sort=-run_count&limit=10" \
-H "Cookie: sessionid=..."
Response:
{
"results": [
{
"user_id": "user-456",
"run_count": 87,
"first_seen": "2026-01-15T08:30:00Z",
"last_seen": "2026-02-07T14:22:00Z",
"total_duration": 245.8,
"total_cost": 1.234567,
"total_tokens": 189420,
"agents": ["chat-agent", "search-agent", "code-agent"]
}
],
"count": 156,
"next": "?offset=10&limit=10",
"previous": null
}
Get User Detail
GET /api/v1/observability/users/{user_id}/
Authentication: Session (UI)
Returns detailed metrics for a specific user, including a per-model cost breakdown and recent runs.
Example:
curl -s "https://acme.waxell.dev/api/v1/observability/users/user-456/" \
-H "Cookie: sessionid=..."
Response:
{
"user_id": "user-456",
"aggregates": {
"run_count": 87,
"total_duration": 245.8,
"total_cost": 1.234567,
"total_tokens": 189420,
"agents": ["chat-agent", "search-agent"],
"first_seen": "2026-01-15T08:30:00Z",
"last_seen": "2026-02-07T14:22:00Z"
},
"cost_by_model": [
{
"model": "gpt-4o",
"total_cost": 0.987654,
"total_tokens": 142000,
"call_count": 64
},
{
"model": "gpt-4o-mini",
"total_cost": 0.246913,
"total_tokens": 47420,
"call_count": 23
}
],
"runs": [
{
"id": 1042,
"agent_name": "chat-agent",
"workflow_name": "default",
"started_at": "2026-02-07T14:22:00Z",
"completed_at": "2026-02-07T14:22:03Z",
"duration": 2.8,
"status": "success",
"cost": 0.0089,
"tokens": 1250
}
]
}
UI Walkthrough
Users List
The users list view shows all tracked users with sortable columns:
- User ID -- click to open user detail
- Runs -- total number of agent executions
- First Seen / Last Seen -- user activity time range
- Duration -- total execution time
- Cost -- total LLM spend
- Tokens -- total token usage
- Agents -- which agents this user interacted with
User Detail
The user detail page shows:
- Summary cards with run count, total cost, total tokens, and active time range
- Cost by model breakdown -- a table showing which models drove the user's costs
- Recent runs -- the last 50 runs for this user with per-run cost, tokens, duration, and status
Use Cases
Cost Attribution
Identify your highest-cost users to understand whether spend is proportional to value:
curl -s "https://acme.waxell.dev/api/v1/observability/users/?sort=-total_cost&limit=5" \
-H "Cookie: sessionid=..."
Abuse Detection
Flag users with unusually high run counts or token consumption. The cost_by_model breakdown on the detail endpoint reveals whether a user is making disproportionately expensive model calls.
Usage Patterns
Track first_seen and last_seen to understand user retention and engagement patterns. The agents list shows which product features each user exercises.
Advanced: Multiple Runs Per Function with WaxellContext
If you need to create multiple distinct runs from a single function (e.g., a batch processor that handles items for many users in one invocation), use WaxellContext directly:
from waxell_observe import WaxellContext
for item in batch:
async with WaxellContext(
agent_name="batch-processor",
user_id=item.user_id,
session_id=item.session_id,
) as ctx:
result = await process(item) # auto-captured LLM calls go here
ctx.set_result({"output": result})
For the common case of one function = one run, prefer the decorator pattern above.
Next Steps
- Sessions -- Group runs by conversation for multi-turn analysis
- Scoring -- Capture user satisfaction alongside usage data
- Cost Management -- Set budget limits and alerts based on user spend