> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bentolabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Sessions and users

> How conversations and per-user views are stitched together from the IDs you pass.

A **session** is one conversation between your app and a user. A **user** is one person across all their conversations. The dashboard builds both by grouping trajectories that share a `convo_id` or a `user_id`. There's no separate Session or User object: you pass strings, and the grouping happens server-side.

## Conversations

Pass the same `convo_id` to every `bento.begin` or `bento.track_ai` call that belongs in one conversation.

```python theme={null}
convo_id = "user_42_chat_99"

bento.track_ai(event="turn_1", convo_id=convo_id, user_id="user_42", input=q1, output=a1, ...)
bento.track_ai(event="turn_2", convo_id=convo_id, user_id="user_42", input=q2, output=a2, ...)
```

Both turns show up under the same session row, with the timeline reconstructed from span timestamps.

<Info>
  `convo_id` becomes the OTel attribute `gen_ai.conversation.id` and lands in the `traces.session_id` column on our side. The attribute name and the column name don't match for historical reasons. Use the kwarg.
</Info>

A good `convo_id` is stable enough to survive between requests in the same conversation, unique enough that two conversations never share one, and opaque: a UUID, slug, or hash, never PII. Most apps already have a source for it. A chat thread row gives you its primary key, stringified. A web session gives you its UUID. A Slack or Discord channel gives you `channel_id:thread_ts`. If you have nothing yet, generate one with `uuid4()` at the start of the conversation and store it client-side.

## Users

`user_id` is a pass-through string. We don't store profile data: no email, no name, no traits.

```python theme={null}
bento.track_ai(event="answer", user_id="user_42", ...)
```

It maps to `gen_ai.user.id` (OTel) and the `traces.user_id` column. You can filter the trace list by `user_id`, count distinct users, and pivot any metric by user.

Use your internal stable user ID, like the users-table primary key or the auth provider `sub` claim. Pick one ID per real human, so the same person reads as one identity across devices, and treat it like a foreign key with no PII in it. For anonymous users, generate an ID and store it in a cookie or local storage. That gives you a stable identity without leaking PII.

```python theme={null}
user_id = request.cookies.get("anon_id") or new_anon_id()
bento.track_ai(event="signup_help", user_id=user_id, ...)
```

## How the dashboard groups

Grouping happens at query time, not ingest time, so the IDs you pass decide what the dashboard shows.

| You set                              | The dashboard shows                                                 |
| ------------------------------------ | ------------------------------------------------------------------- |
| Same `convo_id` on N spans           | One session with N turns                                            |
| Same `user_id` across sessions       | All those sessions under one user view                              |
| Same `convo_id`, different `user_id` | One session. The latest `user_id` wins for display. **Avoid this.** |
| Different `convo_id`, same `user_id` | N independent sessions under one user                               |

There's no migration to "create a session". Existing trajectories start grouping the moment you start passing the right ID.

## Setting IDs on a trajectory

A trajectory carries `convo_id` and `user_id` on its own span. Child spans don't inherit them, so pass the IDs to each `track_ai` call too so per-span filters work.

```python theme={null}
convo_id = "chat_99"
user_id = "user_42"

with bento.begin(event="agent_loop", convo_id=convo_id, user_id=user_id) as t:
    bento.track_ai(event="plan",   convo_id=convo_id, user_id=user_id, ...)
    bento.track_ai(event="answer", convo_id=convo_id, user_id=user_id, ...)
```

## What sessions are not

A session is a dashboard grouping, not a billing or quota concept: you're billed on span volume. It has no TTL either. Keep passing the same `convo_id` for a year and the dashboard shows one very long session, so rotate the ID when a conversation logically ends. And a session isn't user-scoped on its own. Two different `user_id`s sharing a `convo_id` look like one session with mixed users, so rotate `convo_id` whenever the user changes.
