Users
Tag every LLM call with the end-user it originated from, and Spanlens aggregates per-user cost, request count, and behaviour at /users. Answer “Which of my customers is costing me the most LLM spend?” in one click.
How tagging works
Set the x-spanlens-user header on any proxied request. The value is a string of your choosing — Spanlens never interprets it. Typical patterns: your DB user UUID, email hash, or a workspace identifier.
Easiest path with the SDK:
import { createOpenAI, withUser } from '@spanlens/sdk/openai'
const openai = createOpenAI()
await openai.chat.completions.create(
{ model: 'gpt-4o-mini', messages: [...] },
{ headers: { ...withUser(currentUser.id).headers } },
)tsAnthropic / Gemini integrations expose the same withUser() / with_user() helper. For raw HTTP, just set the header directly.
What the dashboard shows
- /users — sortable table of every tagged end-user with total requests, tokens, cost, average latency, error count, distinct models, and first/last seen.
- Row click opens
/users/[id]— the same stats plus the last 50 requests for that user, each linkable to its full request detail. - Filter pivot — from any request drawer the user_id chip links to the analytics page; the small filter link next to it scopes
/requeststo just that user. - Search the user ID column with substring match. URL-backed so you can share filtered views.
Sort options
sortBy | Behaviour |
|---|---|
cost (default) | Highest-spending users first. |
requests | Most-active users first. |
tokens | Heaviest token consumers (large prompts). |
last_seen | Most recently active first — useful for triage. |
API
Both endpoints scope to your organization automatically via JWT.
# List — sort by cost desc, page 1
GET /api/v1/users?sortBy=cost&sortDir=desc&page=1&limit=50
# Detail — aggregates + recent 50 requests
GET /api/v1/users/<user-id>?projectId=<optional>&from=<iso>&to=<iso>bashLimitations
- Untagged requests don't appear. Calls without an
x-spanlens-userheader are excluded from/users. Add the header everywhere you want attribution. - No PII protection on the value. Whatever you put in the header is what gets stored. Hash emails or use opaque IDs if you don't want raw addresses in the dashboard.
- Time-range filtering is server-side but the UI uses defaults. The list view shows lifetime totals; pass
?from=…&to=…on the API to scope. Time-window picker in the UI is on the roadmap.
Related: Requests, SDK reference, /users dashboard.