Use knotel
Sessions, people and agents
Tie spans to a browser visit, an end user, or an AI agent's conversation, and read them back.
A span can say who it was for and what it was part of: a browser visit, an end user, an AI agent's conversation. Set the attributes below and the Sessions and Agents pages fill in; set none and they stay empty and cost nothing.
Sessions
The browser script sets session.id on every visit, kept in sessionStorage, so nothing is needed to get sessions from a website. Backend spans don't carry it — nothing propagates a session across a network hop — but they share a trace with the browser span that called them, so a session's traces bring the backend work along.
From another SDK, set session.id on the span or on the resource; both are read.
People
Call identify after sign-in, and with null on sign-out:
knotel.identify("u_8fc21"); // after sign-in
knotel.identify(null); // on sign-outSpans already queued are sent with the id too: they belong to the same visit, and a page load that ends in a sign-in is the part worth attributing. On the server, set user.id (or enduser.id) on the span in your auth middleware.
user.hash and enduser.pseudo.id; both are read as the user.Both fields are filterable anywhere: session.id and user.id work in queries, on the Traces page and in outlier attribution, where "the slow requests are all one customer" is a finding worth having.
Agent sessions
Spans following the OpenTelemetry GenAI conventions are read as agent sessions: a conversation is every span sharing a gen_ai.conversation.id. The Agents page shows each one's model calls, tool calls, tokens, estimated cost and where its wall-clock time went; opening one lists every step in order, each linking to its trace.
The two attributes that matter:
| Attribute | What it does |
|---|---|
gen_ai.conversation.id | Groups spans into one session. Without it there is no session to show. |
gen_ai.operation.name | What the span did: chat, execute_tool, invoke_agent, embeddings. Decides whether it counts as a model call, a tool call or agent overhead. |
Everything else is optional and read when present:
- Tokens: the five buckets —
gen_ai.usage.input_tokens,output_tokens,cache_read.input_tokens,cache_write.input_tokensandreasoning.output_tokens. Reasoning tokens are left out of the total, since providers already count them inside output. - Model:
gen_ai.response.model, elsegen_ai.request.model. - Tool:
gen_ai.tool.nameonexecute_toolspans, which is what the tool table groups by.
The conventions are still marked Development upstream, and most frameworks shipped their own keys first, so older spellings (gen_ai.usage.prompt_tokens, the Vercel AI SDK's ai.usage.*) are read too.
What cost means
OpenTelemetry defines no cost attribute. If your span carries one (gen_ai.usage.cost), that is what's shown — you know your own contract. Otherwise cost is estimated from tokens against published list prices for the models knotel knows, matched by prefix so dated and provider-prefixed ids follow their family.
A model with no published price here reports tokens and no cost, rather than a number that would be wrong. The table is in src/lib/gen-ai.ts; it is a plain list, and adding your own models to it is the intended fix.