# Sessions

Capture sessions from your code — the primary unit of analysis in Brizz.

This page is about **capturing** sessions from your code. For how the dashboard displays sessions (timeline, filters, replays), see [Sessions in the dashboard](/docs/platform/sessions.md).

Sessions are the fundamental unit of analysis in Brizz. They allow you to group multiple interactions—LLM calls, tool executions, and events—into a single, coherent user journey.

## Why Use Sessions?

Without sessions, your analytics are just a pile of disconnected traces. Sessions enable:

- **User Journey Mapping**: See the full conversation history, not just isolated prompts.
- **Cost & Performance Aggregation**: Calculate total token usage and latency for a complete interaction.
- **Contextual Debugging**: When an error occurs, see the sequence of events that led to it.

## Implementing Sessions

:::tabs
:::tab[Python]
Use the `start_session` context manager. This is the most robust way to ensure all operations within the block are correctly tagged.

```python
from brizz import start_session

# Start a session with a unique ID (e.g., from your database or client)
with start_session("session-123") as session:
    # Add Metadata (Critical for filtering)
    session.update_properties(
        user_id="user-42",
        plan="premium",
        feature="onboarding"
    )

    # Run your agent logic
    # All OpenAI/LangChain calls here are automatically linked
    response = openai.chat.completions.create(...)
```
:::tab[Node.js]
Use the `startSession` function to group all operations into a session.

```typescript
import { startSession } from '@brizz/sdk';

const result = await startSession('session-123', async (session) => {
  // Add Metadata (Critical for filtering)
  session.updateProperties({ user_id: 'user-42', plan: 'premium' });

  // Run your agent logic
  // All OpenAI/LangChain calls here are automatically linked
  const response = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [{ role: 'user', content: userQuery }],
  });

  return response;
});
```

**Alternative: Simple session tagging** with `withSessionId`:

```typescript
import { withSessionId } from '@brizz/sdk';

async function handleRequest(userId: string, query: string) {
  await openai.chat.completions.create({ ... });
}

const handleSession = withSessionId('session-123', handleRequest);
await handleSession('user-42', 'Help me with billing');
```
:::

## Manual Input/Output Tracking

In most cases, Brizz automatically captures inputs and outputs from your LLM calls. However, there are scenarios where you may want to manually specify what gets tracked:

- **Multi-agent flows**: When multiple agents collaborate, you may want to track only the user-facing input/output, not intermediate agent communications.
- **Structured data extraction**: When you send structured JSON to the LLM but want to track only a specific field (e.g., the user's query).
- **Post-processing**: When you transform the LLM response before returning it to the user.

:::tabs
:::tab[Python]
```python
with start_session("session-123") as session:
    # Extract just the query from a complex request
    request_data = {"query": "What's the weather?", "context": {...}}
    session.set_input(request_data["query"])

    response = openai.chat.completions.create(...)

    # Extract the answer from a structured response
    response_json = json.loads(response.choices[0].message.content)
    session.set_output(response_json["answer"])
```
:::tab[Node.js]
```typescript
await startSession('session-123', async (session) => {
  // Extract just the query from a complex request
  const requestData = { query: "What's the weather?", context: {...} };
  session.setInput(requestData.query);

  const response = await openai.chat.completions.create({...});

  // Extract the answer from a structured response
  const responseJson = JSON.parse(response.choices[0].message.content);
  session.setOutput(responseJson.answer);
});
```
:::

## Per-turn context

Two identical questions can mean different things. "What changed?" asked from an empty
dashboard and the same words asked with a record open are different questions, and the
text alone cannot tell them apart.

Pass the state of the surface the user sent the message from as `context` on `set_input`.
It is stored per turn, alongside that turn's text, so you can tell those two apart later.

:::tabs
:::tab[Python]
```python
with start_session("session-123") as session:
    session.set_input(
        user_text,
        context={
            "page": "billing/invoices",
            "selected_record": record.id,
            "workspace": workspace.name,
        },
    )
```
:::tab[Node.js]
```typescript
await startSession('session-123', async (session) => {
  session.setInput(userText, {
    page: 'billing/invoices',
    selectedRecord: record.id,
    workspace: workspace.name,
  });
});
```
:::

Send what the user could see or had chosen — the page or screen, what was selected, which
workspace or project they were in, which mode the assistant was running in. Keep the
values short and repeating, the same way you would for a filter.

### Context on the reply

`set_output` takes the same argument, and the two answer different questions. Input
context is **where the question came from**; output context is **how the answer was
produced** — which documents you retrieved, which prompt version answered, which model
served it.

That is what tells a regression from a bad question. When an answer gets worse, the input
context says whether users started asking something different, and the output context says
whether your pipeline changed underneath them.

:::tabs
:::tab[Python]
```python
with start_session("session-123") as session:
    session.set_input(user_text, context={"page": "billing/invoices"})

    answer, sources = agent.run(user_text)

    session.set_output(
        answer,
        context={
            "retrieved_docs": [d.id for d in sources],
            "prompt_version": prompt.version,
        },
    )
```
:::tab[Node.js]
```typescript
await startSession('session-123', async (session) => {
  session.setInput(userText, { page: 'billing/invoices' });

  const { answer, sources } = await agent.run(userText);

  session.setOutput(answer, {
    retrievedDocs: sources.map((d) => d.id),
    promptVersion: prompt.version,
  });
});
```
:::

:::tip
Both bags are stored per turn and stay aligned with the text they describe, so the i-th question keeps the context it was asked in and the i-th reply keeps the context it was produced from.
:::

## Accessing the Session from Anywhere

In complex applications, you often need to access the session deep in your call stack without threading it through every function. Use `getActiveSession` / `get_active_session` to retrieve the current session from anywhere within a `startSession` / `start_session` scope.

:::tabs
:::tab[Python]
```python
from brizz import get_active_session, start_session

def log_step(step_name: str):
    session = get_active_session()
    if session:
        session.update_properties(last_step=step_name)

def process_query(query: str):
    # No need to pass session as a parameter
    log_step("processing")
    return openai.chat.completions.create(...)

with start_session("session-123") as session:
    session.set_input("What's the weather?")
    result = process_query("What's the weather?")
```
:::tab[Node.js]
```typescript
import { getActiveSession, startSession } from '@brizz/sdk';

function logStep(stepName: string) {
  const session = getActiveSession();
  session?.updateProperties({ last_step: stepName });
}

async function processQuery(query: string) {
  // No need to pass session as a parameter
  logStep('processing');
  return openai.chat.completions.create({ ... });
}

await startSession('session-123', async (session) => {
  session.setInput("What's the weather?");
  const result = await processQuery("What's the weather?");
});
```
:::

:::info
`getActiveSession` / `get_active_session` returns `undefined` / `None` when called outside a session scope, so it's safe to use in shared utility functions.
:::

## Session Title Generation

If you use an LLM call to name or title a session, wrap it so those spans don't show up as part of the conversation.

:::tabs
:::tab[Python]
```python
from brizz import start_session, start_session_title

with start_session("session-123") as session:
    response = openai.chat.completions.create(...)

    # Title generation — excluded from conversation view
    with start_session_title() as title:
        t = openai.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": "Summarize this chat in 3 words"}]
        )
        title.set_title(t.choices[0].message.content)

# Alternative: use mode='title' on start_session
with start_session("session-123", mode="title") as session:
    t = openai.chat.completions.create(...)
    session.set_title(t.choices[0].message.content)
```
:::tab[Node.js]
```typescript
import { startSession, startSessionTitle } from '@brizz/sdk';

await startSession('session-123', async (session) => {
  const response = await openai.chat.completions.create({...});

  // Title generation — excluded from conversation view
  await startSessionTitle(async (title) => {
    const t = await openai.chat.completions.create({
      model: 'gpt-4',
      messages: [{ role: 'user', content: 'Summarize this chat in 3 words' }],
    });
    title.setTitle(t.choices[0].message.content);
  });
});

// Alternative: use mode: 'title' on startSession
await startSession('session-123', async (session) => {
  const t = await openai.chat.completions.create({...});
  session.setTitle(t.choices[0].message.content);
}, undefined, { mode: 'title' });
```
:::

Both `start_session_title` / `startSessionTitle` accept an optional `session_id` / `sessionId` parameter for use outside a `start_session` scope.

## External Session Replays

If your frontend runs FullStory, Mixpanel Session Replay, or LogRocket alongside `@brizz/browser`, Brizz auto-correlates the recording with the matching session. A deep-link badge appears on the session in the dashboard — one click to the replay.

Setup is nothing extra beyond installing the browser SDK:

- **FullStory** and **Mixpanel** — auto-detected when their snippet is on the page.
- **LogRocket** — auto-detected via the CDN snippet, or via a one-line side-effect import for npm users (`import '@brizz/browser/integrations/logrocket'`).

Opt out per provider with `disableFullStory` / `disableMixpanel` / `disableLogRocket` in `Brizz.init`.

## Best Practices

1. **Stable IDs**: Use a stable identifier for the session ID (like a conversation ID from your DB) so you can correlate Brizz data with your own records.
2. **User Metadata**: Identify the end-user with [`set_user` / `setUser`](/docs/instrument/identify-users.md). This unlocks user-level analytics in the dashboard.
3. **Granularity**: A session should represent a logical unit of work, like a single conversation thread or a specific task execution.

## See also

- [Sessions in the dashboard](/docs/platform/sessions.md) — how to read what you've captured.
- [External links](/docs/instrument/external-links.md) — attach a Datadog trace, Sentry issue, or dashboard URL to a session.
- [Custom events](/docs/instrument/custom-events.md) — enrich sessions with business signals.
- [Identify users](/docs/instrument/identify-users.md) — attach user properties so sessions roll up by person.
- [User feedback](/docs/instrument/user-feedback.md) — capture thumbs-up/thumbs-down on individual responses.
- [PII & privacy](/docs/instrument/pii-and-privacy.md) — mask sensitive data before it leaves your infrastructure.
