Browse resources

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.

These are lookup keys, so each has an index — a partial one, written only for spans that carry the attribute. A project that sends none pays for none. A project that sends them on every span pays one extra row write per span per attribute, which is what D1 charges for.

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-out

Spans 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.

Send an opaque id, not an email or a name. It is stored as sent, shown in the UI and returned over MCP. For an identifier that can't be traced back to a person, OTel defines 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:

AttributeWhat it does
gen_ai.conversation.idGroups spans into one session. Without it there is no session to show.
gen_ai.operation.nameWhat 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_tokens and reasoning.output_tokens. Reasoning tokens are left out of the total, since providers already count them inside output.
  • Model: gen_ai.response.model, else gen_ai.request.model.
  • Tool: gen_ai.tool.name on execute_tool spans, 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.