Sessions
Group multiple traces from one conversation under a single session.
A session groups multiple traces from the same conversation together in the dashboard. Each turn of a chat is its own trace; a session links those turns so you can view the whole conversation as a timeline.
End-user → Session → Trace → Span
(a conversation) (one turn)Grouping turns into a session is what makes conversation-level questions answerable: how long a conversation runs, how many turns it takes to resolve, what it costs end-to-end, whether it succeeded or was abandoned — and, when a customer reports an issue, replaying their exact conversation instead of hunting through disconnected traces. Pair a session with an end-user and those metrics roll up per customer and segment.
When to use sessions
Sessions are for multi-turn applications: chatbots, voice assistants, agents where a user sends several messages and each message triggers a new run. For a single-run script or batch job, you don't need a session, the trace is its own session.
You pass the session id
Each turn is a separate trace. Neatlogs has no way to infer that two traces belong to the same conversation, only your app knows that. So you pass a session_id (use your own conversation / thread id) and pass the same value on every turn. Traces sharing a session_id are grouped under one session.
If you don't pass a session_id, the backend sets session_id = trace_id — the trace becomes its own single-turn session. Nothing breaks; the turns just won't group. Passing the same session_id across turns is what makes a multi-turn conversation.
Where you pass it depends on how you instrument, set it on the trace root if you have one, or via identify() if you only use the neatlogs wrapper.
On the trace root (decorator / trace)
If you open the root yourself as a WORKFLOW decorator or a root-level trace(), pass session_id there. This is the common case.
# Decorator root
@neatlogs.span(kind="WORKFLOW", session_id=f"conv_{conversation_id}")
def handle_turn(message):
return agent.run(message)
# or a trace() context manager
with neatlogs.trace("chat_turn", session_id=f"conv_{conversation_id}"):
agent.run(message)Call this once per turn with the same session_id and the per-turn traces group into one session.
Wrapper-only code: identify()
If you only wrap an LLM client (neatlogs.wrap(...) / WrapGenAI) and don't open a root yourself, the wrapper creates the trace root internally, there's no decorator to put session_id on. Use identify() to bind the session for the duration of a turn; the wrapper's auto-root picks it up.
client = neatlogs.wrap(OpenAI())
# At the top of each turn's handler:
with neatlogs.identify(session_id=f"conv_{conversation_id}"):
client.chat.completions.create(...) # auto-root inherits the sessionidentify() is backed by context propagation so it flows across await/async boundaries within a turn. Set it at the start of each request/turn — it does not carry across separate requests or processes, so re-supply it every turn. (Go uses ctx; see below.)
Use the latest SDK version — identify() reaches every trace root, including the ones opened by framework integrations (LangChain, OpenAI Agents, CrewAI, Strands, …). Older versions attached identity only to roots you opened yourself, so on those you'd wrap the call in trace(session_id=…, end_user_id=…) instead.
Go: one mechanism for every path
In Go, neatlogs.Identify(ctx, ...) is the only thing you call — it binds identity onto ctx, and any root span created under that context picks it up. That includes neatlogs.Trace, the WrapGenAI auto-root, and auto-instrumented passthrough spans created directly on the OpenTelemetry provider — e.g. Google ADK (whose root span the identity lands on; nladk.WrapModel only adds I/O capture to that span, it does not create the root). Just pass the Identify-derived ctx into whatever runs the turn:
ctx = neatlogs.Identify(ctx, neatlogs.IdentifyOptions{SessionID: "conv_42", EndUserID: "u_7"})
ctx, span, end := neatlogs.Trace(ctx, "chat_turn")
defer end()
runTurn(ctx)identify() is backed by context propagation so it flows across await/async boundaries within a turn. Set it at the start of each request/turn — it does not carry across separate requests or processes, so re-supply it every turn.
Multi-turn example
Three turns of one conversation, grouped under conv_42:
for message in conversation:
with neatlogs.trace("chat_turn", session_id="conv_42"):
agent.run(message)
# → 3 traces, one session (conv_42), shown as a timeline in the dashboardEnd-user goes alongside
A conversation usually belongs to one end-user. Set end_user_id in the same place you set session_id, see End-User Identity.
Browser SDK
The browser SDK (neatlogs/browser) takes a first-class sessionId — set it once on the client (per conversation) or per call. Every trace sharing the value groups into one session.
const nl = new Neatlogs({
apiKey: 'nlw_...',
project: 'my-app',
sessionId: conversationId, // same id for every turn
endUserId: 'u_812', // the person in this conversation
});
// each turn is its own trace; all share `sessionId`
await nl.trackAI({ name: 'chat', model: 'gpt-4o', input: turn1, output: reply1 });
await nl.trackAI({ name: 'chat', model: 'gpt-4o', input: turn2, output: reply2 });
// or per call (overrides the client default):
await nl.trackAI({ name: 'chat', model: 'gpt-4o', input, output, sessionId: 'conv_42' });HTTP API
Sending traces over plain HTTP /v1/trace (no SDK)? Put the session id on the root object's attributes as the canonical key:
{
"name": "chat_turn",
"project": "my-app",
"attributes": {
"neatlogs.session.id": "conv_42",
"neatlogs.end_user.id": "u_812"
},
"children": [ { "name": "answer", "kind": "LLM", "model": "gpt-4o" } ]
}Post one such trace per turn with the same neatlogs.session.id to build a multi-turn session.
Reference
| Python | TypeScript | Go | |
|---|---|---|---|
| On the root | trace(session_id=) · @span(session_id=) | trace({sessionId}) · span({sessionId}) | Identify(ctx, IdentifyOptions{SessionID}) then Trace(ctx, name) |
| Wrapper-only | with identify(session_id=) | identify({sessionId}, fn) | Identify(ctx, IdentifyOptions{SessionID}) (same) |
| Canonical attribute | neatlogs.session.id (root span) | same | same |
Related: End-User Identity · Chatbot Sessions guide
