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 } },
)
ts

Anthropic / 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 /requests to just that user.
  • Search the user ID column with substring match. URL-backed so you can share filtered views.

Sort options

sortByBehaviour
cost (default)Highest-spending users first.
requestsMost-active users first.
tokensHeaviest token consumers (large prompts).
last_seenMost 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>
bash

Limitations

  • Untagged requests don't appear. Calls without an x-spanlens-user header 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.