DocsProduction

Sessions and users

Group sub-agents into sessions and attribute traces to end users, from the SDK or from OpenTelemetry attributes.

Last updated 2026-09-07

A production agent is rarely one trace. It's a planner that spawns executors, a supervisor with sub-agents, or a chat loop across many turns. Sessions and users are how EvaliQA groups and attributes those traces, and they're the unit everything downstream works on: online evaluation scores a session, the AI Analyst reads a session, alert rules fire per session.

Sessions

Give every trace inside one user request the same session_id. EvaliQA groups them into a single session on /runtime-eval/sessions, with a gantt-style timeline of the sub-agents and one analysis for the whole exchange.

import uuid
from eval_lib.tracing import tracer

def handle_user_request(user_id: str, user_message: str) -> str:
    session_id = str(uuid.uuid4())

    tracer.start_trace("planner")
    tracer.set_trace_metadata(
        session_id=session_id,
        user_id=user_id,
        input=user_message,
    )
    plan = call_planner_agent(user_message)
    tracer.end_trace()

    # Sub-agents share the session_id. EvaliQA groups them into one
    # timeline on /runtime-eval/sessions/<id>.
    for step in plan.steps:
        tracer.start_trace("executor")
        tracer.set_trace_metadata(
            session_id=session_id,
            user_id=user_id,
            input=step.description,
        )
        run_step(step)
        tracer.end_trace()

One turn or one conversation?

Online evaluation treats the session as the dialogue: conversation-level metrics such as Role Adherence or Conversational Flow read every trace in the session, in order, as one exchange. So what a session is decides what gets judged:

  • Session = one conversation (session_id = conversation id). Each user turn is a trace; the session grows as the chat goes on. Analysis runs 60 seconds after the last trace and runs again if the conversation continues. Best for chat products, and the shape the conversation metrics expect.
  • Session = one request (session_id = request id). Right for planner-executor agents where the "conversation" is between your own sub-agents, not with a person.

Either way, a trace with no session_id is stored and shown on the Traces tab but never appears on Sessions, and nothing session-level runs for it.

Users

Pass user_id and EvaliQA aggregates sessions per person on the /runtime-eval/users tab: sessions, traces, tokens, cost, first and last seen. Open a user to list their sessions. Useful for:

  • Finding the heaviest token spenders.
  • Catching abuse patterns (one user, thousands of failed calls in an hour).
  • Debugging a specific complaint ("show me every session from user X").

The value is opaque to EvaliQA: whatever string identifies the user in your own system. Treat it as an identifier, not a profile. Don't put an email address or a name in user_id if you wouldn't want it in a trace store.

From OpenTelemetry

Coming in through the OTLP door? Set the equivalent span attributes, on the root span at minimum, and the grouping is identical. EvaliQA reads, in order of preference:

  • Session: gen_ai.conversation.id, session.id, conversation.id, thread.id, evaliqa.session_id, langfuse.session.id, session_id.
  • User: enduser.id, user.id, evaliqa.user_id, langfuse.user.id, user_id.

On the SDK side, session_id and user_id are accepted either as top-level trace fields or inside metadata, which is why older integrations that put them in metadata keep working.

Sessions vs traces vs spans: a quick recap

  • Span: one step (LLM call, tool call). Nests inside a trace.
  • Trace: one agent invocation. Contains a tree of spans.
  • Session: one user request or one conversation. Contains multiple traces sharing a session_id.
  • User: one end user. Contains multiple sessions sharing a user_id.