# Welcome to Brizz

Brizz is the analytics layer for AI agents. Capture every interaction, measure quality, and connect agent behavior to product outcomes.

Brizz traces every conversation your agent has, evaluates the quality of its responses, surfaces failure patterns, clusters user intents, and ties everything back to product and business outcomes. It integrates via an OpenTelemetry-compatible SDK.

## Where do you want to go?

### I'm integrating an SDK

Get a trace from your code into the Brizz dashboard for the first time.

→ [Get started](/docs/get-started/install.md) — install, send your first session, and verify it landed.

### I want to understand the dashboard

You're already shipping data and want to know what each page in Brizz means.

→ [Platform](/docs/platform.md) — what Issues, Intents, Journeys, and Sessions show you, and how to act on them.

### I'm setting up access

Manage API keys, configure SSO, or invite teammates.

→ [Admin](/docs/admin/api-keys.md) — API keys, SSO, account-level setup.

## See also

- [Core concepts](/docs/introduction/concepts.md) — definitions for trace, span, session, event, user, issue, intent, journey.
- [Choose your SDK](/docs/sdks.md) — Python, Node.js, and framework adapters.
- [Glossary](/docs/help/glossary.md) — alphabetical reference for every term.

Support: support@brizz.ai


---

# Core concepts

A short mental model for Brizz: trace, span, session, event, user, issue, intent, journey.

This page is the shared vocabulary every other doc links into. Read it once; come back when a term feels fuzzy.

## Trace

A **trace** is the record of a single end-to-end operation through your agent — for example, one user message and the chain of LLM calls, tool calls, and processing it triggered. Brizz reconstructs traces from OpenTelemetry data the SDK emits.

## Span

A **span** is one step inside a trace — an LLM call, a tool invocation, a function execution. Traces are trees of spans with start/end timestamps and attributes. Most automatic instrumentation produces spans without any code on your side.

## Session

A **session** groups multiple traces into a single user journey — typically one conversation thread. You provide the session ID; Brizz attaches every trace, event, and feedback signal that occurs inside it. Sessions are the primary lens for analysis in the dashboard.

→ Capture: [Sessions (instrumentation)](/docs/instrument/sessions.md)
→ Read: [Sessions (in the dashboard)](/docs/platform/sessions.md)

## Event

An **event** is a discrete signal you emit from your code — a business outcome, a milestone, a feedback action. Events are not LLM calls; they describe *what happened* (`order.placed`, `feedback.positive`) rather than *how* the model produced an answer.

→ [Custom events](/docs/instrument/custom-events.md)

## Metric

A **metric** is a number scored against a session — a quality rating, a latency, a cost. Where an event says *what happened*, a metric says *how well it went*. Brizz computes some itself; you can report your own (an eval score, a customer rating) as an **external metric**, and then filter and slice sessions by it.

→ Report: [Record metrics](/docs/instrument/record-metric.md)
→ Read: [External metrics](/docs/platform/external-metrics.md)

## User

A **user** is the person interacting with your agent. When you attach a `user_id` (and optionally `user_name`, `user_email`) to a session, Brizz unlocks user-level analytics: journeys, retention, segment-specific issues.

→ [Identify users](/docs/instrument/identify-users.md)

## Issue

An **issue** is a deduplicated problem detected across your traffic. When many occurrences describe the same underlying problem, Brizz groups them so you see *one row* with a frequency count, first/last seen, and affected users — not a flood of individual detections. Each issue carries an **issue type** (missing capability, technical failure, behavioral, and so on) and a **priority** — what to work on first, derived from how serious the problem is and how much of your traffic it affects.

→ [Issues](/docs/platform/issues.md)

## Intent

A **user intent** is a semantic cluster of similar prompts — "cancel my subscription," "stop auto-renew," and "turn off billing" collapse into one **Cancellation** intent. Brizz discovers intents automatically from your conversation data.

→ [User intents](/docs/platform/user-intents.md)

## Journey

A **user journey** is a common path users take through your agent — an aggregated flow built from many sessions. Journeys reveal where users drop off, what sequences lead to success, and how people actually use the product.

→ [User journeys](/docs/platform/user-journeys.md)

## See also

- [Glossary](/docs/help/glossary.md) — alphabetical, every term in one place.
- [Choose your SDK](/docs/sdks.md) — when you're ready to instrument.
- [Platform overview](/docs/platform.md) — what to look at in your first week.


---

# Install the SDK

Install the Brizz SDK in your Python or Node.js project.

Brizz provides SDKs for Python and Node.js/TypeScript. Pick the one that matches your runtime — the rest of *Get started* is shared across both.

## Prerequisites

- A Brizz account and access to the dashboard.
- A credential, created in **Organization Settings → API Keys**: either a [Server DSN](/docs/admin/server-dsn.md) (one string carrying the credential, endpoint, and service name) or an [API key](/docs/admin/api-keys.md). Your workspace issues one or the other.
- Python 3.10+ or Node.js 18+.

:::tip
Store your API key in an environment variable. Never commit it to version control.
:::

## Install

:::tabs
:::tab[Python]
**Requirements**: Python 3.10+

```bash
pip install brizz
```
:::tab[Node.js]
**Requirements**: Node.js 18+

```bash
npm install @brizz/sdk
# or
yarn add @brizz/sdk
# or
pnpm add @brizz/sdk
```
:::

## See also

- [Send your first session](/docs/get-started/first-session.md) — initialize the SDK and capture your first session.
- [Python SDK reference](/docs/sdks/python.md) — full configuration and advanced usage.
- [Node.js / TypeScript SDK reference](/docs/sdks/typescript.md) — full configuration and advanced usage.


---

# Send your first session

Initialize the Brizz SDK and capture your first session.

How you send your first session depends on what's already in your stack — a plain OpenAI/Anthropic client looks different from LangChain, Vercel AI, or Agno. Before you copy the snippet below, **head to [Choose your SDK](/docs/sdks.md)** and follow the page that matches your setup. The example here is a generic starting point; the SDK page for your framework is the source of truth.

## 1. Initialize Brizz

Initialize the SDK before your app makes any AI calls. In Python, `Brizz.initialize()` must run before you import your AI libraries. In Node, hand those libraries to `instrumentModules` — then import order doesn't matter.

:::tabs
:::tab[Python]
```python
import os
from brizz import Brizz

# Initialize BEFORE importing other AI libraries
Brizz.initialize(
    api_key=os.environ.get("BRIZZ_API_KEY"),
    app_name="my-ai-app",
)

# Now import your AI libraries
from openai import OpenAI
```
:::tab[Node.js]
For Node, prefer **manual instrumentation** — pass the modules you want to instrument explicitly. This works for ESM, CJS, bundled, and `tsx`-driven setups.

```typescript
import 'dotenv/config';
import { Brizz } from '@brizz/sdk';
import * as CallbackManagerModule from '@langchain/core/callbacks/manager';
import OpenAI from 'openai';

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY,
  appName: process.env.BRIZZ_APP_NAME || 'my-app',
  environment: process.env.NODE_ENV || 'development',
  instrumentModules: {
    // Pass only the modules your app actually uses
    openAI: OpenAI,
    langchain: {
      callbackManagerModule: CallbackManagerModule,
    },
  },
});
```

The exact `instrumentModules` shape depends on which framework you're using — see [Node.js / TypeScript SDK](/docs/sdks/typescript.md) and the framework-specific adapter page for the right keys.
:::
:::

## 2. Wrap an agent call in a session

Sessions group multiple interactions (LLM calls, tool usage, events) into one user journey. This is the unit you'll spend most of your time looking at in the dashboard.

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

with start_session("session-123"):
    # Your agent logic here — any LLM/tool call inside this block is traced
    response = openai.chat.completions.create(...)
```
:::tab[Node.js]
```typescript
import { withSessionId } from '@brizz/sdk';

async function runAgent() {
  // Your agent logic here
}

await withSessionId('session-123', runAgent)();
```
:::

## 3. Run it

Run your application. Brizz batches and flushes spans asynchronously — within a few seconds you should see a session in the dashboard.

## See also

- [Choose your SDK](/docs/sdks.md) — the framework-specific quickstart for your stack.
- [Verify it landed](/docs/get-started/verify.md) — what to look for in the dashboard, what to do if nothing shows up.
- [Next steps](/docs/get-started/next-steps.md) — identify users, send custom events, plug in a framework.
- [Python SDK reference](/docs/sdks/python.md) and [Node.js / TypeScript SDK reference](/docs/sdks/typescript.md) — advanced configuration.


---

# Verify it landed

Confirm your first session arrived in the Brizz dashboard, and what to do if it didn't.

You ran the code. Did the session make it into Brizz? This page closes that loop.

## Where to look

1. Open the Brizz dashboard and select your tenant.
2. Go to **Sessions** in the sidebar.
3. Look for a session with the ID you passed to `start_session` / `withSessionId` (e.g. `session-123`).

Click the session to drill in. You should see:

- The agent's input and final output at the top.
- A timeline of spans — every LLM call, tool call, and any events you emitted.
- Token counts, latency, and the raw request/response for each LLM span.

If you instrumented with one of the supported frameworks (LangChain, Vercel AI SDK, Agno, Strands, LiveKit Agents), the framework's own structure (chains, tools, agents) shows up natively in the timeline — you don't need to add spans by hand.

## Nothing showed up?

Run through this checklist before anything else:

- **API key is set.** `BRIZZ_API_KEY` is in the environment your app actually runs with — not just your shell. Print it from inside the app if unsure.
- **Initialization runs first.** `Brizz.initialize()` must execute *before* any AI library is imported.
- **Server-side only.** The SDK is meant to run on a trusted server. If you're trying to send telemetry from a browser, use [`@brizz/browser`](/docs/sdks/browser.md) instead.
- **Wait a few seconds.** Spans are batched. Refresh the Sessions page after ~5–10 seconds.
- **Check service filter.** The Sessions page may be filtered to a specific `service_name` from a previous look. The name you passed as `app_name` / `appName` is what appears here.

Still not seeing data? Head to [Troubleshooting](/docs/help/troubleshooting.md) — it covers the most common ingestion failures with fixes.

## See also

- [Next steps](/docs/get-started/next-steps.md) — what to instrument next now that the pipeline works.
- [Sessions (in the dashboard)](/docs/platform/sessions.md) — how to read the session view in depth.
- [Troubleshooting](/docs/help/troubleshooting.md) — initialization order, 401s, and more.


---

# Next steps

Where to go after your first session lands — identify users, send events, plug in a framework, explore the dashboard.

You've sent a session and confirmed it landed. Pick whichever direction is closest to what you actually need to do next.

## Identify your users

Attach a `user_id` (and optionally name/email) to every session. This unlocks user-level analytics: per-user journeys, retention, and segmenting issues by user cohort.

→ [Identify users](/docs/instrument/identify-users.md)

## Send custom events

Emit domain events from your code — `feedback.positive`, `order.placed`, `agent.goal.achieved` — so the dashboard can correlate them with the traces around them.

→ [Custom events](/docs/instrument/custom-events.md)

## Report your own scores

Already grading your agent with an eval harness, an LLM judge, or a human reviewer? Send those numbers to Brizz and they become real metrics — badged on the session and filterable ("every conversation my judge scored below 0.6").

→ [Record metrics](/docs/instrument/record-metric.md)

## Connect a framework

Already using LangChain, the Vercel AI SDK, Agno, Strands, or LiveKit Agents? The auto-instrumentation gives you native span shapes for each. Skim the relevant guide to make sure you're wired up correctly.

→ [Choose your SDK](/docs/sdks.md)

## Browse the platform

Curious what the dashboard actually shows? Start with the five things worth looking at in your first week.

→ [Platform overview](/docs/platform.md)

## See also

- [Sessions (instrumentation)](/docs/instrument/sessions.md) — enrich sessions with metadata, manual input/output, title generation.
- [User feedback](/docs/instrument/user-feedback.md) — capture thumbs-up/thumbs-down and filter sessions by sentiment.
- [PII & privacy](/docs/instrument/pii-and-privacy.md) — mask sensitive data before it leaves your infrastructure.


---

# Choose your SDK

Pick your runtime, then check whether the framework you're using needs explicit wiring.

Install the SDK for your runtime — **Python**, **Node.js / TypeScript**, or **Browser**. Everything else on this page is *framework support* — config that tells the SDK how to capture sessions from a specific framework you're already using.

**Rule of thumb**: in Python, most frameworks are auto-instrumented as soon as you call `Brizz.initialize()` — you don't need to do anything else. In Node, popular frameworks (LangChain, Vercel AI, MCP) usually need an explicit `instrumentModules: { ... }` entry passed at init so Brizz can hook them. The framework page tells you exactly which.

Pick your runtime first, then jump to the framework page only if your stack needs it.

## SDKs

The base SDKs capture calls to common AI provider clients (OpenAI, Anthropic, etc.) and let you wrap any code in sessions and events.

- **[Python](/docs/sdks/python.md)** — Python 3.10+. Most frameworks auto-instrument with no extra setup.
- **[Node.js / TypeScript](/docs/sdks/typescript.md)** — Node.js 18+, ESM and CJS. Pass the libraries you use via `instrumentModules`.
- **[Browser](/docs/sdks/browser.md)** — `@brizz/browser`, for capturing product events from a web frontend and correlating them with backend sessions. Requires a client DSN.

## Auto-instrumented integrations

The Python SDK captures these automatically when you call `Brizz.initialize()` — no extra wiring. (In Node, some auto-instrument on import and others need an `instrumentModules` entry; the framework pages below say which.)

- **LLM providers & platforms** — OpenAI, Anthropic, Google Generative AI (`google-generativeai`, the legacy Gemini SDK), Google Vertex AI, AWS Bedrock, Amazon SageMaker, IBM watsonx, Cohere, Mistral, Groq, Ollama, Together, Replicate, Aleph Alpha, Hugging Face Transformers
- **Agent & data frameworks** — LangChain, LlamaIndex, CrewAI, Agno, Claude Agent SDK
- **Voice frameworks** — LiveKit Agents (emits OpenTelemetry natively, no extra wiring)
- **Vector stores** — Chroma, Pinecone, Qdrant, Milvus, Weaviate, Marqo, LanceDB

If your framework isn't listed, it may still be captured through the framework's own OpenTelemetry output — [contact us](mailto:support@brizz.ai) and we'll confirm.

## Framework support

Use these pages when the base SDK isn't already capturing what you need. Each one tells you whether it works automatically or requires explicit wiring at init.

- **[LangChain (JavaScript)](/docs/sdks/langchain-js.md)** — Node: pass `instrumentModules: { langchain: { callbackManagerModule } }`. (LangChain in Python is auto-instrumented.)
- **[Vercel AI SDK](/docs/sdks/vercel-ai.md)** — Node: enable `experimental_telemetry: { isEnabled: true }` on `generateText` / `streamText` calls.
- **[Google GenAI (Gemini)](/docs/sdks/google-genai.md)** — Node: build your client from `instrumentGoogleGenAI(genai)`. The `@google/genai` client is ESM-only, so `instrumentModules` doesn't apply.
- **[Vercel eve](/docs/sdks/vercel-eve.md)** — Node: register Brizz in your agent's `instrumentation.ts` and set `BRIZZ_DSN`.
- **[Agno](/docs/sdks/agno.md)** — Python framework; works with the Python SDK.
- **[Strands Agents](/docs/sdks/strands-agents.md)** — Python framework; works with the Python SDK.
- **[LiveKit Agents](/docs/sdks/livekit-agents.md)** — Python voice framework; works with the Python SDK.

## Protocols

- **[MCP (auto-instrument your server)](/docs/sdks/mcp.md)** — Add Brizz observability to an MCP server you operate (Python FastMCP or TypeScript). For *consuming* Brizz from AI agents like Claude or Cursor, see the [MCP server integration](/docs/integrations/mcp-server/overview.md) instead.

## Available on request

- **LibreChat** — requires custom configuration tailored to your deployment. [Contact us](mailto:support@brizz.ai) and we'll help you wire it up.

## See also

- [Install the SDK](/docs/get-started/install.md) — start here if you haven't yet.
- [Instrument](/docs/instrument/sessions.md) — once installed, the cookbook for what to send to Brizz.
- [Telemetry ingestion API](/docs/api/telemetry.md) — raw HTTP endpoint for custom pipelines.


---

# Python SDK

Install, initialize, and add sessions/events in Python.

The Brizz Python SDK provides automatic instrumentation for your AI applications, capturing traces, metrics, and logs with minimal configuration. It is fully compatible with OpenTelemetry.

## Installation

```bash
pip install brizz
```

## Initialization

To ensure all libraries are correctly instrumented, **initialize Brizz before importing other AI libraries**.

```python
import os
from brizz import Brizz

# 1. Initialize Brizz
Brizz.initialize(
    api_key=os.environ.get("BRIZZ_API_KEY"),
    app_name="my-ai-app",
)

# 2. Import your AI libraries (OpenAI, LangChain, etc.)
from openai import OpenAI
```

:::warning Import Order
If you use `python-dotenv`, load your environment variables **before** importing `brizz`.
:::

## Sessions

Sessions are the core unit of analysis in Brizz. They group related operations (like a multi-turn chat) into a single entity.

### Basic Usage

Use the `start_session` context manager to wrap your agent's execution.

```python
from brizz import start_session

with start_session("session-123"):
    # All LLM calls and events here are linked to session-123
    response = openai.chat.completions.create(...)
```

### Adding Metadata

You can attach user IDs and other custom properties to a session. This is highly recommended for filtering and user journey analysis.

```python
with start_session("session-123") as session:
    session.update_properties(
        user_id="user-42",
        plan="enterprise",
        feature="support-bot"
    )

    # Your agent logic...
```

### Per-Turn Context

Pass kwargs to `set_input` / `set_output` to attach metadata to a single turn. Each turn's bag is rendered under a collapsible **Context** panel on that message in the dashboard.

```python
with start_session("session-123") as session:
    session.set_input("Why is my bill high?", selected_invoice="INV-9182")
    reply = openai.chat.completions.create(...)
    session.set_output(
        reply.choices[0].message.content,
        message_id="msg-42",
        sources=["doc-abc"],
    )
```

### Session Title Generation

Wrap LLM-based title generation so it doesn't appear in the conversation:

```python
from brizz import start_session, start_session_title

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

    with start_session_title() as title:
        t = openai.chat.completions.create(model="gpt-4", messages=[...])
        title.set_title(t.choices[0].message.content)
```

You can also use `start_session("session-123", mode="title")` to mark an entire session scope as title generation.

### Muting internal calls

Some LLM calls — classification, routing, guardrail checks, internal summarization — aren't part of the conversation your user had. Wrap them so their content is left out of the captured conversation. The call still runs and its latency/tokens/cost are recorded; only the user prompt, assistant reply, and tool calls are dropped.

```python
import brizz

# Keep an internal call out of the conversation (everything).
with brizz.mute():
    summary = agent.run("Summarize this conversation for internal logging.")

# Hide the text but keep the tool calls.
with brizz.mute(tools=False):
    reply = agent.run("…a prompt you'd rather not store…")
```

See [Mute messages](/docs/instrument/mute.md) for one-sided muting, muting tool calls on their own, async (`amute`), and reuse.

### Accessing the Session from Anywhere

Use `get_active_session` to retrieve the current session from nested functions without passing it as a parameter:

```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)

with start_session("session-123") as session:
    log_step("start")  # Accesses the session automatically
    response = openai.chat.completions.create(...)
```

Returns `None` when called outside a session scope.

### Async Support

For asynchronous applications, use `astart_session`.

```python
from brizz import astart_session

async with astart_session("session-123") as session:
    session.update_properties(user_id="user-42")
    await agent.run(...)
```

## Custom Events

Emit custom events to track specific business logic or user actions that aren't automatically captured.

```python
from brizz import emit_event

emit_event(
    "tool.search.completed",
    attributes={"query": "pricing", "results_count": 5},
    body={"top_result": "Pricing Page"}
)
```

## Configuration

You can configure the SDK via environment variables or the `initialize` method.

| Environment Variable | Description |
|----------------------|-------------|
| `BRIZZ_API_KEY` | Your Brizz API Key (Required) |
| `BRIZZ_APP_NAME` | Name of your application |
| `BRIZZ_ENVIRONMENT` | Deployment environment (e.g., `production`, `staging`) |

### PII Masking

Brizz can automatically mask sensitive information (emails, phone numbers, keys) in your traces.

```python
Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    masking=True,  # Enable default masking rules
)
```

## Integrations

### Langfuse

If you already use Langfuse, see the [Langfuse SDK page](/docs/sdks/langfuse.md) — Brizz ingests Langfuse OTel spans directly, and the page covers init, session grouping, and per-turn context.

### LangSmith

If you use LangChain + LangSmith, route LangSmith's per-run telemetry through Brizz's TracerProvider. Brizz stays out of LangChain instrumentation and ingests LangSmith's OTel-native spans.

```python
from dotenv import load_dotenv
load_dotenv()

import os
os.environ.setdefault("LANGSMITH_TRACING", "true")
os.environ.setdefault("LANGSMITH_OTEL_ENABLED", "true")

from brizz import Brizz

# Initialize BEFORE importing langchain/langsmith so Brizz's TracerProvider
# is the global one. LangSmith's OTel exporter reuses it.
Brizz.initialize(
    api_key=os.getenv("BRIZZ_API_KEY"),
    app_name="my-app",
    allowed_instrumentations=[],
)
```

Wrap your LangChain calls in a Brizz session to group them:

```python
from brizz import start_session, astart_session
from langchain_core.messages import HumanMessage

# `llm` is your existing LangChain chat model (ChatOpenAI, ChatAnthropic, etc.).

with start_session("chat-session-123"):
    response = llm.invoke([HumanMessage(content="Hello")])

# Async equivalent
async with astart_session("chat-session-123"):
    response = await llm.ainvoke([HumanMessage(content="Hello")])

# Alternative — no start_session needed:
# Pass thread_id (or session_id / conversation_id) in LangChain's per-call
# metadata. Brizz reads langsmith.metadata.thread_id and groups the calls
# into the same Brizz session automatically.
response = llm.invoke(
    [HumanMessage(content="Hello")],
    config={"metadata": {"thread_id": "chat-session-123"}},
)
```

## Complete example

A single file you can copy, set `BRIZZ_API_KEY` + `OPENAI_API_KEY`, and run with `python main.py`.

```python
# main.py
# Load env vars BEFORE importing brizz so BRIZZ_API_KEY is available at init time.
from dotenv import load_dotenv
load_dotenv()

import os

# Initialize Brizz BEFORE importing any AI libraries (openai, langchain, anthropic, ...).
# Auto-instrumentation hooks them at import time — wrong order and spans go missing.
from brizz import Brizz, start_session, emit_event

Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-ai-app",
    environment=os.getenv("APP_ENV", "development"),
    masking=True,  # mask emails / phones / API keys; flip off if you handle PII yourself
)

# Now safe to import AI libs.
from openai import OpenAI
client = OpenAI()


def run_turn(session_id: str, user_id: str, user_message: str) -> str:
    with start_session(session_id) as session:
        # Session-wide properties — show up on every span in the session.
        session.update_properties(user_id=user_id, plan="enterprise")

        # set_input attaches the user message + per-turn context (kwargs render
        # under the collapsible "Context" panel on that message).
        session.set_input(user_message, selected_invoice="INV-9182")

        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": user_message}],
        )
        reply = response.choices[0].message.content

        # set_output stamps the assistant reply + per-turn metadata.
        session.set_output(reply, message_id=response.id, sources=["doc-abc"])

        # User feedback — stays linked to this session via the active scope.
        # Map "feedback.positive" / "feedback.negative" to system events in
        # Org Settings -> Event so it powers filters and the Overview chart.
        emit_event(
            "feedback.positive",
            attributes={"category": "helpfulness"},
            body={"comment": "Exactly what I needed!", "context": "billing-assistant"},
        )
        return reply


if __name__ == "__main__":
    print(run_turn("session-123", "user-42", "Why is my bill high?"))
```

**Gotchas**

- Load `.env` before importing `brizz`.
- Call `Brizz.initialize()` before importing OpenAI / LangChain / Anthropic.
- For async code, use `astart_session(...)` (the `with` becomes `async with`).
- Routing through Langfuse or LangSmith? Pass `allowed_instrumentations=[]` (see the [Langfuse SDK page](/docs/sdks/langfuse.md)).

## See also

- [Sessions](/docs/instrument/sessions.md) — capture and enrich sessions in depth.
- [Custom events](/docs/instrument/custom-events.md) — emit business events and feedback from Python.
- [Record metrics](/docs/instrument/record-metric.md) — report your own eval scores and measurements with `record_metric`.
- [Identify users](/docs/instrument/identify-users.md) — attach user properties so analytics roll up by person.
- [PII & privacy](/docs/instrument/pii-and-privacy.md) — masking configuration.
- [Mute messages](/docs/instrument/mute.md) — keep internal or unrelated calls out of the captured conversation.


---

# TypeScript/Node.js SDK

Instrument AI libraries in Node.js and emit sessions/events.

The Brizz Node.js SDK instruments your AI libraries from a single `Brizz.initialize()` call. One setup covers ESM, CommonJS, bundlers and `tsx`.

## Installation

```bash
npm install @brizz/sdk
```

## Initialization

Call `Brizz.initialize()` in your entry file and hand it the AI libraries you want captured via `instrumentModules`. Passing the actual imported module is what lets Brizz hook it, so it works the same under ESM, CommonJS, bundlers (Next.js, Webpack) and `tsx`.

```typescript
import { Brizz } from '@brizz/sdk';
import OpenAI from 'openai';

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY,
  appName: 'my-ai-app',
  instrumentModules: { openAI: OpenAI },
});

const openai = new OpenAI();
```

In CommonJS, the same call works with `require` instead of `import`.

Run your app the way you already do — `node app.js`, `tsx app.ts`, or your framework's own start command. There are no launch flags to add.

## Sessions

Sessions group related operations into a single user journey.

### Using `startSession` (Recommended)

The `startSession` function creates a session span and provides a `Session` object:

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

// All LLM calls within the callback are automatically linked to the session
const result = await startSession('session-123', async (session) => {
  // Add custom properties (optional)
  session.updateProperties({ user_id: 'user-42', plan: 'premium' });

  // Your AI logic
  const response = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [{ role: 'user', content: userQuery }],
  });

  return response;
});
```

For manual input/output tracking in special scenarios (multi-agent flows, structured data), see [Sessions](/docs/instrument/sessions.md#manual-inputoutput-tracking).

### Per-Turn Context

Pass a second argument to `setInput` / `setOutput` to attach metadata to a single turn. Each turn's bag is rendered under a collapsible **Context** panel on that message in the dashboard.

```typescript
await startSession('session-123', async (session) => {
  session.setInput('Why is my bill high?', { selected_invoice: 'INV-9182' });
  const response = await openai.chat.completions.create({ ... });
  session.setOutput(response.choices[0].message.content, {
    message_id: 'msg-42',
    sources: ['doc-abc'],
  });
});
```

### Using `getActiveSession`

Retrieve the current session from anywhere within a `startSession` scope, without passing it as a parameter:

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

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

await startSession('session-123', async (session) => {
  logStep('start'); // Accesses the session automatically
  const response = await openai.chat.completions.create({ ... });
});
```

Returns `undefined` when called outside a session scope.

### Session Title Generation

Wrap LLM-based title generation so it doesn't appear in the conversation:

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

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

  await startSessionTitle(async (title) => {
    const t = await openai.chat.completions.create({ model: 'gpt-4', messages: [...] });
    title.setTitle(t.choices[0].message.content);
  });
});
```

You can also use `startSession('id', callback, undefined, { mode: 'title' })` to mark an entire session scope as title generation.

### Muting internal calls

Some LLM calls — classification, routing, guardrail checks, internal summarization — aren't part of the conversation your user had. Wrap them so their content is left out of the captured conversation. The call still runs and its latency/tokens/cost are recorded; only the user prompt, assistant reply, and tool calls are dropped.

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

// Keep an internal call out of the conversation (everything).
await callWithMute({}, () => agent.run('Summarize this conversation for internal logging.'));

// Hide the text but keep the tool calls.
await callWithMute({ tools: false }, () => agent.run('…a prompt you’d rather not store…'));
```

See [Mute messages](/docs/instrument/mute.md) for one-sided muting, muting tool calls on their own, async, and reuse.

### Using `withSessionId`

For simpler cases where you just need to tag traces with a session ID:

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

async function runAgent(userId: string, query: string) {
  // All LLM calls here are part of the session
  await openai.chat.completions.create({ ... });
}

// Create a wrapped version of your function
const runWithSession = withSessionId('session-123', runAgent);

// Execute it
await runWithSession('user-42', 'Hello world');
```

## Vercel AI SDK

If you are using the Vercel AI SDK, you must enable the `experimental_telemetry` flag in your function calls for Brizz to capture them.

```typescript
import { generateText } from 'ai';

const result = await generateText({
  model: openai('gpt-4'),
  prompt: 'Hello',
  experimental_telemetry: { isEnabled: true }, // Required!
});
```

See the [Vercel AI quickstart](/docs/sdks/vercel-ai.md) for install (including the extra package `ai@7` needs), tool approvals, and a complete example.

## Google GenAI (Gemini)

`@google/genai` is ESM-only, and a Node ES module namespace is read-only — it can't be patched in place, and `instrumentModules` doesn't apply. Build your client from the class `instrumentGoogleGenAI` returns (after `Brizz.initialize()`):

```typescript
import { instrumentGoogleGenAI } from '@brizz/sdk/google-genai';
import * as genai from '@google/genai';

const GoogleGenAI = instrumentGoogleGenAI(genai);
const ai = new GoogleGenAI({ vertexai: true, project, location });
```

See the [Google GenAI quickstart](/docs/sdks/google-genai.md) for install, tool calling, and a complete example.

## Custom Events

Emit custom events to track business logic.

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

emitEvent('user.signup', {
  plan: 'pro',
  source: 'referral'
});
```

### Attaching a session ID

If you know the session ID but aren't inside a `startSession` / `withSessionId` scope, use `emitEventWithSessionId` to stamp it on a single event:

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

emitEventWithSessionId('session-123', 'button.click', {
  source: 'sidebar',
});
```

## Configuration

| Option | Description |
|--------|-------------|
| `apiKey` | Your Brizz API Key (Required) |
| `appName` | Name of your application |
| `environment` | Deployment environment (e.g., `production`) |
| `masking` | Configuration for PII masking |

### Reporting as several services

`appName` names the whole process. When one process runs several agents that should show up as separate services in Brizz, scope the name around each one instead:

```typescript
import { callWithServiceName, withServiceName, setServiceName } from '@brizz/sdk';

// Everything traced inside the callback reports as 'checkout-agent',
// including auto-instrumented LLM calls
await callWithServiceName('checkout-agent', async () => {
  return openai.chat.completions.create({ ... });
});

// Pre-wrap a handler
const handleSupport = withServiceName('support-agent', supportHandler);

// When the enclosing span already exists — an MCP tool or HTTP handler
// opens its span before your code runs
setServiceName('billing-agent');
```

Sessions and events created outside any of these keep the configured `appName`.

### PII Masking

```typescript
Brizz.initialize({
  apiKey: '...',
  masking: {
    spanMasking: {
      rules: [
        {
          attributePattern: 'gen_ai\\.(prompt|completion)',
          mode: 'partial',
          patterns: ['sk-[a-zA-Z0-9]{48}'], // Mask API keys
        },
      ],
    },
  },
});
```

### Dropping Spans

Filter spans before they leave the SDK. Return `false` to drop, `true` to keep. Useful for stripping noisy paths (health checks, internal tooling) or excluding telemetry for specific end-users.

```typescript
Brizz.initialize({
  apiKey: '...',
  beforeSendSpan: (span) => {
    // OpenInference (LangChain JS, LangGraph JS) packs per-call
    // `config.metadata` into a JSON-stringified `metadata` attribute.
    const raw = span.attributes['metadata'];
    if (typeof raw !== 'string') return true;
    try {
      return JSON.parse(raw).customer_id !== 'cust_42';
    } catch {
      return true;
    }
  },
});
```

Tag the call site so the filter has something to match on:

```typescript
await llm.invoke([new HumanMessage('Hello')], {
  metadata: { customer_id: 'cust_42' },
});
```

Both sync and async filters are supported. Exceptions are caught — the span passes through.

To modify attribute values without dropping the span, use [PII Masking](#pii-masking) instead.

## Complete example

A single file you can copy, set `BRIZZ_API_KEY` + `OPENAI_API_KEY`, and run with `tsx app.ts`.

```typescript
// app.ts
import { Brizz, startSession, emitEvent } from '@brizz/sdk';
import OpenAI from 'openai';

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY!,
  appName: 'my-ai-app',
  environment: process.env.NODE_ENV ?? 'development',
  instrumentModules: { openAI: OpenAI },
  masking: {
    spanMasking: {
      rules: [
        // Mask the literal OpenAI API key pattern inside prompt/completion attributes.
        { attributePattern: 'gen_ai\\.(prompt|completion)', mode: 'partial', patterns: ['sk-[a-zA-Z0-9]{48}'] },
      ],
    },
  },
  beforeSendSpan: (span) => {
    // Drop telemetry for an opted-out customer; tagged via metadata at the call site.
    const raw = span.attributes['metadata'];
    if (typeof raw !== 'string') return true;
    try { return JSON.parse(raw).customer_id !== 'cust_optout'; } catch { return true; }
  },
});

const openai = new OpenAI();

async function runTurn(sessionId: string, userId: string, userMessage: string) {
  return await startSession(sessionId, async (session) => {
    // Session-wide properties — every span in the session inherits them.
    session.updateProperties({ user_id: userId, plan: 'enterprise' });
    session.setInput(userMessage, { selected_invoice: 'INV-9182' });

    const response = await openai.chat.completions.create({
      model: 'gpt-4o-mini',
      messages: [{ role: 'user', content: userMessage }],
    });
    const reply = response.choices[0].message.content ?? '';

    session.setOutput(reply, { message_id: response.id, sources: ['doc-abc'] });

    // User feedback — map "feedback.positive" / "feedback.negative" to system
    // events in Org Settings -> Event so it powers filters and the Overview chart.
    emitEvent(
      'feedback.positive',
      { category: 'helpfulness' },
      { comment: 'Exactly what I needed!', context: 'billing-assistant' },
    );
    return reply;
  });
}

console.log(await runTurn('session-123', 'user-42', 'Why is my bill high?'));
```

**Gotchas**

- Put `Brizz.initialize()` at the top of your entrypoint, before your app makes any AI call, and list every AI library you use in `instrumentModules` — handing Brizz the imported module is what makes instrumentation independent of import order.
- [LangChain JS](/docs/sdks/langchain-js.md) and [Vercel AI](/docs/sdks/vercel-ai.md) need framework-specific setup — see their pages.

## See also

- [Sessions](/docs/instrument/sessions.md) — capture and enrich sessions in depth.
- [Custom events](/docs/instrument/custom-events.md) — emit business events and feedback from Node.
- [Record metrics](/docs/instrument/record-metric.md) — report your own eval scores and measurements with `recordMetric`.
- [Identify users](/docs/instrument/identify-users.md) — attach user properties so analytics roll up by person.
- [PII & privacy](/docs/instrument/pii-and-privacy.md) — masking configuration.
- [Mute messages](/docs/instrument/mute.md) — keep internal or unrelated calls out of the captured conversation.


---

# Browser

Capture product events from a web frontend with @brizz/browser and correlate them with the same backend agent session.

`@brizz/browser` sends product-analytics events from a web app — `track('clicked_submit', { plan: 'pro' })` — to Brizz. Because the frontend reports the **same service name and session id** as your agent backend, those UI events land on the same session as the agent's traces.

It's a separate package from the Node SDK ([`@brizz/sdk`](/docs/sdks/typescript.md)): small, dependency-light, no framework lock-in.

## Install

:::tabs
:::tab[npm]
```bash
npm install @brizz/browser
```
:::tab[pnpm]
```bash
pnpm add @brizz/browser
```
:::tab[CDN]
```html
<script src="https://unpkg.com/@brizz/browser/dist/index.iife.js"></script>
```
:::

## Initialize

Initialize with a **client DSN**, a browser-safe credential you create in your Brizz settings. The service segment must match the service name your backend reports, so the two halves join into one session.

```ts
import { Brizz } from '@brizz/browser';

const brizz = Brizz.init({
  dsn: 'https://brizz-ing-c-XXXX@telemetry.brizz.dev/<service-name>',
});
```

From the CDN build there's no import — the script exposes `Brizz` as a global:

```html
<script src="https://unpkg.com/@brizz/browser/dist/index.iife.js"></script>
<script>
  const brizz = Brizz.init({ dsn: 'https://brizz-ing-c-XXXX@telemetry.brizz.dev/<service-name>' });
</script>
```

Initialized with `dsn`, the SDK accepts only client DSNs (`brizz-ing-c-`). Requests need a browser `Origin` header, which is why this is a browser credential.

## Track events

```ts
brizz.track('clicked_submit', { plan: 'pro', step: 'checkout' });
```

Page URL, referrer, browser, OS, locale, and screen size are attached automatically.

## Correlate with the backend session

The SDK never mints or persists a session id — mint it on your backend and hand it to both halves. Set it once and every later `track()` uses it:

```ts
brizz.setSessionId('session-from-your-backend');
brizz.track('clicked_submit', { plan: 'pro' });
brizz.setSessionId(null); // e.g. on logout
```

If one page hosts more than one logical session, use a session-bound emitter instead of swapping the default:

```ts
const supportChat = brizz.session('support-thread-7');
const copilot = brizz.session('copilot-session-42');

supportChat.track('typed', { length: 12 });
```

## Session-replay correlation

If your page runs FullStory, Mixpanel Session Replay, or LogRocket, Brizz correlates the recording with the session and shows a deep link on it.

| Provider | Setup | Opt out |
|---|---|---|
| FullStory | Auto-detected | `disableFullStory: true` |
| Mixpanel | Auto-detected | `disableMixpanel: true` |
| LogRocket | CDN: none. npm: add the import below | `disableLogRocket: true` |

```ts
import { Brizz } from '@brizz/browser';
import '@brizz/browser/integrations/logrocket';
```

## Availability

Create a client DSN under **Organization Settings → API Keys** by choosing **Client DSN** as the credential type — see [Server DSN](/docs/admin/server-dsn.md#client-dsn) for the credential classes side by side. If it isn't offered there, [contact us](mailto:support@brizz.ai).

## See also

- [Node.js / TypeScript SDK](/docs/sdks/typescript.md) — instrument your agent backend.
- [Sessions](/docs/platform/sessions.md) — where the correlated frontend and backend events show up.


---

# LangChain (JavaScript)

Install Brizz alongside LangChain JS and add session tracking.

Quickstart for the LangChain JS integration shown during onboarding. For the full TypeScript SDK reference, see the [TypeScript SDK guide](/docs/sdks/typescript.md).

## Install

:::tabs
:::tab[npm]
```bash
npm install @brizz/sdk @langchain/core
```
:::tab[yarn]
```bash
yarn add @brizz/sdk @langchain/core
```
:::tab[pnpm]
```bash
pnpm add @brizz/sdk @langchain/core
```
:::

`@langchain/core` is an optional peer dependency of `@brizz/sdk` — install it alongside Brizz whenever you use LangChain so Brizz and LangChain share a single instance and callbacks flow correctly. The OpenInference LangChain instrumentation ships with `@brizz/sdk`, so you don't need to install it separately.

## Initialize

Pass LangChain's callback manager module so Brizz can wire in the OpenInference tracer.

```typescript
import * as CallbackManagerModule from "@langchain/core/callbacks/manager";
import { Brizz } from "@brizz/sdk";

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY,
  appName: process.env.BRIZZ_APP_NAME || "my-app",
  environment: process.env.NODE_ENV || "development",
  instrumentModules: {
    langchain: {
      callbackManagerModule: CallbackManagerModule,
    },
  },
});
```

## Sessions

Wrap your agent calls with `withSessionId` (and `withProperties` for user/account context).

```typescript
import { withSessionId, withProperties } from "@brizz/sdk";
import { HumanMessage } from "@langchain/core/messages";

// `agent` is your existing LangChain agent (createReactAgent, AgentExecutor, etc.).
async function callLLM(userMessage: string) {
  const output = await agent.invoke({
    messages: [new HumanMessage(userMessage)],
  });
  return output;
}

const sessionId = "unique-session-id";
const properties = {
  userId: "user-1234",
  accountId: "account-5678",
};

const wrappedCall = withProperties(
  properties,
  withSessionId(sessionId, callLLM),
);

await wrappedCall("Hello, how are you?");
```

### Alternative — no `withSessionId`, use LangChain metadata

If wrapping your call site with `withSessionId` is awkward (already-existing thread/conversation IDs, helper functions invoked from many places), pass the session identifier in LangChain's per-call `config.metadata` instead. Brizz reads it from the OpenInference span attribute and writes `brizz.session.id` for you — same conversation grouping, no SDK wrapper needed.

```typescript
import { HumanMessage } from "@langchain/core/messages";

// `llm` is your existing LangChain chat model (ChatOpenAI, ChatAnthropic, etc.).
await llm.invoke([new HumanMessage("Hello")], {
  metadata: { thread_id: "chat-session-123" },
});
```

Recognized keys, in priority order: `thread_id` → `session_id` → `conversation_id`. The first non-empty string wins.

Precedence: a Brizz `withSessionId` / `start_session` wrapper, or a standard OTel `session.id` attribute, still win over the metadata fallback when both are present.

## Dropping spans per end-user

Anything you put in `config.metadata` shows up on the OpenInference span as a JSON-stringified `metadata` attribute. You can read it from `beforeSendSpan` to drop telemetry for specific end-users — internal staff, test traffic, opted-out tenants:

```typescript
Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY,
  instrumentModules: { langchain: { callbackManagerModule: CallbackManagerModule } },
  beforeSendSpan: (span) => {
    const raw = span.attributes['metadata'];
    if (typeof raw !== 'string') return true;
    try {
      return JSON.parse(raw).customer_id !== 'cust_42';
    } catch {
      return true;
    }
  },
});

await llm.invoke([new HumanMessage('Hello')], {
  metadata: { customer_id: 'cust_42' },
});
```

See the [TypeScript SDK guide](/docs/sdks/typescript.md#dropping-spans) for the full reference.

## Complete example

A single file you can copy, set `BRIZZ_API_KEY` + `OPENAI_API_KEY`, and run with `node agent.mjs`.

```typescript
// agent.ts
import * as CallbackManagerModule from '@langchain/core/callbacks/manager';
import { Brizz, withSessionId, withProperties, emitEvent } from '@brizz/sdk';
import { HumanMessage } from '@langchain/core/messages';
import { ChatOpenAI } from '@langchain/openai';

// callbackManagerModule MUST be the wildcard import — Brizz wires the
// OpenInference tracer through it. @langchain/core is a peer dep;
// install it alongside @brizz/sdk.
Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY!,
  appName: 'my-app',
  environment: process.env.NODE_ENV ?? 'development',
  instrumentModules: { langchain: { callbackManagerModule: CallbackManagerModule } },
});

const llm = new ChatOpenAI({ model: 'gpt-4o-mini' });

async function callLLM(userMessage: string) {
  const out = await llm.invoke([new HumanMessage(userMessage)]);
  // User feedback — map "feedback.positive" / "feedback.negative" to system
  // events in Org Settings -> Event so it powers filters and the Overview chart.
  emitEvent(
    'feedback.positive',
    { category: 'helpfulness' },
    { comment: 'Exactly what I needed!', context: 'support-agent' },
  );
  return out.content;
}

// Option A — wrap your function with withSessionId + withProperties.
const wrapped = withProperties(
  { userId: 'user-42', accountId: 'acct-100' },
  withSessionId('chat-session-123', callLLM),
);
console.log(await wrapped('Hello!'));

// Option B — pass thread_id (or session_id / conversation_id) via LangChain
// metadata; Brizz reads it and groups calls under the same session.
const directReply = await llm.invoke(
  [new HumanMessage('Hello!')],
  { metadata: { thread_id: 'chat-session-123', customer_id: 'cust_42' } },
);
console.log(directReply.content);
```

**Gotchas**

- Install `@langchain/core` alongside `@brizz/sdk` — it's a peer dep.
- `callbackManagerModule` must be the wildcard `import * as ...`, not a default import.
- Anything in `config.metadata` becomes a JSON-stringified `metadata` span attribute — readable from `beforeSendSpan` (see [Dropping spans per end-user](#dropping-spans-per-end-user)).
- Recognized session keys (priority order): `thread_id` → `session_id` → `conversation_id`.

## See also

- [Node.js / TypeScript SDK](/docs/sdks/typescript.md) — the underlying SDK and full configuration.
- [Sessions](/docs/instrument/sessions.md) — session capture patterns.
- [Custom events](/docs/instrument/custom-events.md) — emit business events from LangChain flows.


---

# Vercel AI SDK

Install Brizz alongside the Vercel AI SDK and tag sessions.

Quickstart for the Vercel AI SDK integration shown during onboarding. For the full TypeScript SDK reference, see the [TypeScript SDK guide](/docs/sdks/typescript.md).

## Install

:::tabs
:::tab[npm]
```bash
npm install @brizz/sdk
```
:::tab[yarn]
```bash
yarn add @brizz/sdk
```
:::tab[pnpm]
```bash
pnpm add @brizz/sdk
```
:::

On Vercel AI SDK **v7+** (`ai@7`), also install `@ai-sdk/otel` — v7 moved AI SDK span
collection into that package, so without it your `generateText` / `streamText` calls
produce no spans. It isn't needed on `ai@6`.

```bash
npm install @ai-sdk/otel
```

## Initialize

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

Brizz.initialize({
  apiKey: 'your-brizzai-api-key',
  appName: 'my-app',
});
```

## Usage

You **must** set `experimental_telemetry: { isEnabled: true }` on every Vercel AI SDK call so Brizz can capture spans. Then wrap your function with `withSessionId` to group calls under a single session.

```typescript
import { withSessionId } from '@brizz/sdk';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

async function chat(userMessage: string) {
  return await generateText({
    model: openai('gpt-4'),
    prompt: userMessage,
    experimental_telemetry: { isEnabled: true }, // Required
  });
}

const sessionedChat = withSessionId('conversation-123', chat);
await sessionedChat('Hello!');
```

## Human-in-the-loop tool approvals

Vercel AI SDK 6 supports tools that pause for human approval before executing. Add `needsApproval: true` to a `tool()` definition, then resume the run with a `tool-approval-response`. Brizz captures the approval request and the user's verdict, and renders them in the conversation view as an interrupt item next to the user response — once as **approved**, again as **denied** if the user rejected.

```typescript
import { generateText, stepCountIs, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { startSession } from '@brizz/sdk';

const askUserQuestions = tool({
  description: 'Confirm a change with the user before applying it.',
  inputSchema: z.object({ question: z.string() }),
  needsApproval: true,
  // Runs only after the user approves; the "work" is the question itself.
  execute: async () => ({}),
});

await startSession('approval-demo', async () => {
  const turn1 = await generateText({
    model: openai('gpt-4o-mini'),
    tools: { ask_user_questions: askUserQuestions },
    prompt: 'Apply the mapping change. Use ask_user_questions to confirm first.',
    stopWhen: stepCountIs(1),
    experimental_telemetry: { isEnabled: true },
  });

  // Find the approval request the model produced.
  const approvalReq = turn1.content.find((p: any) => p.type === 'tool-approval-request');

  // In a real app, the user's verdict comes from your UI. Feed it back in:
  const turn2 = await generateText({
    model: openai('gpt-4o-mini'),
    tools: { ask_user_questions: askUserQuestions },
    messages: [
      { role: 'user', content: 'Apply the mapping change.' },
      ...turn1.response.messages,
      {
        role: 'tool',
        content: [{
          type: 'tool-approval-response',
          approvalId: (approvalReq as any).approvalId,
          approved: true,
        }],
      },
    ],
    stopWhen: stepCountIs(2),
    experimental_telemetry: { isEnabled: true },
  });
});
```

A full runnable version lives at `apps/sdk/typescript/examples/vercel-ai-hitl.ts`.

## Complete example

A single file you can copy, set `BRIZZ_API_KEY` + `OPENAI_API_KEY`, and run with `node chat.mjs`.

```typescript
// chat.ts
import { Brizz, withSessionId, withProperties, emitEvent } from '@brizz/sdk';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY!,
  appName: 'my-app',
  environment: process.env.NODE_ENV ?? 'development',
});

// Vercel AI SDK does NOT emit spans by default — you MUST pass
// experimental_telemetry: { isEnabled: true } on every call.
async function chat(userMessage: string) {
  const result = await generateText({
    model: openai('gpt-4o-mini'),
    prompt: userMessage,
    experimental_telemetry: {
      isEnabled: true,
      // Optional — lands on the span as attributes (filters, beforeSendSpan).
      metadata: { feature: 'support-bot' },
    },
  });
  // User feedback — map "feedback.positive" / "feedback.negative" to system
  // events in Org Settings -> Event so it powers filters and the Overview chart.
  emitEvent(
    'feedback.positive',
    { category: 'helpfulness' },
    { comment: 'Exactly what I needed!', context: 'support-agent' },
  );
  return result.text;
}

const sessioned = withProperties(
  { userId: 'user-42' },
  withSessionId('conversation-123', chat),
);

console.log(await sessioned('Hello!'));

// Streaming works the same way — flip experimental_telemetry on streamText too:
// const stream = streamText({ model: openai('gpt-4o-mini'), prompt: '...',
//   experimental_telemetry: { isEnabled: true } });
```

**Gotchas**

- Forgetting `experimental_telemetry: { isEnabled: true }` is the #1 reason calls don't show up. Add it to `generateText`, `streamText`, `generateObject`, `streamObject`, and `embed`.
- `experimental_telemetry.metadata` lands on the span as attributes — usable for dashboard filters and `beforeSendSpan`.

## See also

- [Node.js / TypeScript SDK](/docs/sdks/typescript.md) — full SDK reference.
- [Sessions](/docs/instrument/sessions.md) — session capture patterns.
- [Custom events](/docs/instrument/custom-events.md) — emit business events from `generateText` / `streamText` flows.
- [Troubleshooting](/docs/help/troubleshooting.md) — common issue: forgetting `experimental_telemetry: { isEnabled: true }`.


---

# Vercel eve

Install Brizz alongside Vercel's eve framework and wire up your DSN.

Quickstart for the Vercel `eve` integration shown during onboarding. For the full TypeScript SDK reference, see the [TypeScript SDK guide](/docs/sdks/typescript.md).

## Install

:::tabs
:::tab[npm]
```bash
npm install @brizz/sdk
```
:::tab[yarn]
```bash
yarn add @brizz/sdk
```
:::tab[pnpm]
```bash
pnpm add @brizz/sdk
```
:::

## Register Brizz

Add Brizz to your agent's instrumentation file:

```typescript
// agent/instrumentation.ts
import { defineInstrumentation } from 'eve/instrumentation';
import { registerOTel } from '@vercel/otel';
import { createEveOtelConfig } from '@brizz/sdk/eve';

export default defineInstrumentation({
  setup: ({ agentName }) => registerOTel(createEveOtelConfig({ serviceName: agentName })),
});
```

`createEveOtelConfig` reads your Brizz DSN from the environment:

```bash
export BRIZZ_DSN="<your-dsn>"
```

## See also

- [Node.js / TypeScript SDK](/docs/sdks/typescript.md) — full SDK reference.
- [Telemetry ingestion API](/docs/api/telemetry.md) — raw HTTP endpoint for custom pipelines.
- [Troubleshooting](/docs/help/troubleshooting.md) — common setup issues.


---

# Google GenAI (Gemini)

Install Brizz alongside Google's @google/genai SDK — Gemini on Vertex AI or the Gemini Developer API.

Quickstart for Gemini via [`@google/genai`](https://www.npmjs.com/package/@google/genai) — Google's unified SDK for both **Vertex AI** and the **Gemini Developer API**. For the full TypeScript SDK reference, see the [TypeScript SDK guide](/docs/sdks/typescript.md).

## Install

:::tabs
:::tab[npm]
```bash
npm install @brizz/sdk @google/genai @traceloop/instrumentation-google-generativeai
```
:::tab[yarn]
```bash
yarn add @brizz/sdk @google/genai @traceloop/instrumentation-google-generativeai
```
:::tab[pnpm]
```bash
pnpm add @brizz/sdk @google/genai @traceloop/instrumentation-google-generativeai
```
:::

`@traceloop/instrumentation-google-generativeai` is an optional peer dependency of `@brizz/sdk` — it's only needed when you use Gemini.

## Initialize

`@google/genai` is ESM-only, and a Node ES module namespace is read-only — the SDK can't patch the module in place (and `instrumentModules` doesn't apply). Instead, `instrumentGoogleGenAI` hands back an instrumented `GoogleGenAI` class; **build your client from that class** and every call is traced.

```typescript
import { Brizz } from '@brizz/sdk';
import { instrumentGoogleGenAI } from '@brizz/sdk/google-genai';
import * as genai from '@google/genai';

Brizz.initialize({
  apiKey: 'your-brizzai-api-key',
  appName: 'my-app',
});

// Call after Brizz.initialize() so the wrapper binds to the live tracer.
const GoogleGenAI = instrumentGoogleGenAI(genai);

// Vertex AI (uses Application Default Credentials):
const ai = new GoogleGenAI({
  vertexai: true,
  project: process.env.GOOGLE_CLOUD_PROJECT,
  location: 'us-central1',
});

// …or the Gemini Developer API:
// const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
```

## Usage

`generateContent` and `generateContentStream` are captured automatically — messages, tool calls, token usage, and cost — with no per-call instrumentation. Wrap your conversation in `startSession` so calls group into one session:

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

await startSession('conversation-123', async () => {
  setUser({ id: 'user-42', email: 'dana@example.com' });

  const response = await ai.models.generateContent({
    model: 'gemini-2.5-flash',
    contents: 'What can you help me with?',
  });
  console.log(response.text);
});
```

## Complete example

Copy, set `BRIZZ_API_KEY` + `GOOGLE_CLOUD_PROJECT`, and run with `tsx chat.ts`.

```typescript
// chat.ts
import { Brizz, startSession, setUser, emitEvent } from '@brizz/sdk';
import { instrumentGoogleGenAI } from '@brizz/sdk/google-genai';
import * as genai from '@google/genai';

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY!,
  appName: 'my-app',
});

const GoogleGenAI = instrumentGoogleGenAI(genai);
const ai = new GoogleGenAI({
  vertexai: true,
  project: process.env.GOOGLE_CLOUD_PROJECT!,
  location: 'us-central1',
});

await startSession('conversation-123', async () => {
  setUser({ id: 'user-42' });

  const response = await ai.models.generateContent({
    model: 'gemini-2.5-flash',
    contents: 'Best time of year to visit Tel Aviv?',
  });
  console.log(response.text);

  // Tool calling needs no extra wiring: declare your tools in config.tools, and when
  // you execute a functionCall and send the functionResponse turn back, Brizz renders
  // the tool call and its result in the conversation.

  // User feedback — map "feedback.positive" / "feedback.negative" to system
  // events in Org Settings -> Event so it powers filters and the Overview chart.
  emitEvent(
    'feedback.positive',
    { category: 'helpfulness' },
    { comment: 'Exactly what I needed!', context: 'travel-assistant' },
  );
});
```

The conversation renders in Brizz with the user and assistant messages, model badge, token counts, and cost. With a tool round-trip implemented, it renders as user → tool call → tool result → assistant.

**Gotchas**

- Construct your client from the class `instrumentGoogleGenAI` returns — clients built from the original `genai.GoogleGenAI` produce **no spans**.
- Call `instrumentGoogleGenAI` **after** `Brizz.initialize()`.
- On Vertex AI, an expired `gcloud auth application-default login` surfaces as a **503 "Reauthentication is needed"**, not an auth error.
- Both `@google/genai` v1 and v2 are supported.

## See also

- [Node.js / TypeScript SDK](/docs/sdks/typescript.md) — full SDK reference.
- [Sessions](/docs/instrument/sessions.md) — session capture patterns.
- [Record feedback](/docs/instrument/record-feedback.md) — capture end-user reactions on a conversation.


---

# Agno

Install Brizz with the Agno agent framework.

Quickstart for the Agno integration shown during onboarding. For the full Python SDK reference, see the [Python SDK guide](/docs/sdks/python.md).

## Install

:::tabs
:::tab[pip]
```bash
pip install brizz
```
:::tab[uv]
```bash
uv add brizz
```
:::tab[poetry]
```bash
poetry add brizz
```
:::

## Initialize and run

Initialize Brizz before importing Agno — Agno is auto-instrumented.

```python
from dotenv import load_dotenv
load_dotenv()

import os
from brizz import Brizz, start_session

Brizz.initialize(
    api_key=os.getenv("BRIZZ_API_KEY"),
    app_name="my-app",
)

from agno.agent import Agent
from agno.models.openai import OpenAIChat

agent = Agent(
    name="My Agent",
    model=OpenAIChat(id="gpt-4o-mini"),
    markdown=True,
)

with start_session("my-session"):
    response = agent.run("What is 2+2?")
    print(response.content)
```

## With Langfuse

If you already use Langfuse to trace Agno (via OpenLIT), set `allowed_instrumentations=[]` so Brizz stays out of Agno instrumentation and only ingests Langfuse's OTel spans.

```python
from dotenv import load_dotenv
load_dotenv()

import os
from brizz import Brizz

Brizz.initialize(
    api_key=os.getenv("BRIZZ_API_KEY"),
    app_name="my-app",
    allowed_instrumentations=[],
)

from langfuse import get_client, propagate_attributes
import openlit
langfuse = get_client()
openlit.init(tracer=langfuse._otel_tracer, disable_batch=True)

from agno.agent import Agent
from agno.models.openai import OpenAIChat

agent = Agent(
    name="My Agent",
    model=OpenAIChat(id="gpt-4o-mini"),
    markdown=True,
)

with langfuse.start_as_current_observation(as_type="span", name="agno-run") as span:
    with propagate_attributes(session_id="my-session", user_id="user-123"):
        response = agent.run("What is 2+2?")
        print(response.content)
```

## Complete example

A single file you can copy, set `BRIZZ_API_KEY` + `OPENAI_API_KEY`, and run with `python agent.py`.

```python
# agent.py
from dotenv import load_dotenv
load_dotenv()

import os
# Initialize Brizz BEFORE importing Agno — Agno is auto-instrumented at import time.
from brizz import Brizz, start_session, emit_event

Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-app",
    environment=os.getenv("APP_ENV", "development"),
)

from agno.agent import Agent
from agno.models.openai import OpenAIChat

agent = Agent(
    name="Support Bot",
    model=OpenAIChat(id="gpt-4o-mini"),
    markdown=True,
)


def run_turn(session_id: str, user_id: str, user_message: str) -> str:
    with start_session(session_id) as session:
        session.update_properties(user_id=user_id, plan="enterprise")
        session.set_input(user_message, agent_name=agent.name)

        response = agent.run(user_message)
        session.set_output(response.content, message_id=getattr(response, "run_id", None))

        # User feedback — map "feedback.positive" / "feedback.negative" to system
        # events in Org Settings -> Event so it powers filters and the Overview chart.
        emit_event(
            "feedback.positive",
            attributes={"category": "helpfulness"},
            body={"comment": "Exactly what I needed!", "context": "support-agent"},
        )
        return response.content


if __name__ == "__main__":
    print(run_turn("session-123", "user-42", "What is 2+2?"))
```

**Gotchas**

- Initialize Brizz before `from agno.agent import Agent`. Auto-instrumentation hooks at import time.
- Already tracing Agno via Langfuse + OpenLIT? Use the `allowed_instrumentations=[]` pattern in [With Langfuse](#with-langfuse) above so Brizz only ingests Langfuse's OTel spans.

## See also

- [Python SDK](/docs/sdks/python.md) — full SDK reference.
- [Sessions](/docs/instrument/sessions.md) — session capture patterns.
- [Identify users](/docs/instrument/identify-users.md) — attach user properties to Agno runs.


---

# Strands Agents

Install Brizz with the Strands Agents framework.

Quickstart for the Strands Agents integration shown during onboarding. For the full Python SDK reference, see the [Python SDK guide](/docs/sdks/python.md).

## Install

:::tabs
:::tab[pip]
```bash
pip install brizz
```
:::tab[uv]
```bash
uv add brizz
```
:::tab[poetry]
```bash
poetry add brizz
```
:::

## Initialize and run

```python
from dotenv import load_dotenv
load_dotenv()

import os
from brizz import Brizz, start_session

Brizz.initialize(
    api_key=os.getenv("BRIZZ_API_KEY"),
    app_name="my-app",
)

from strands import Agent
from strands.models.openai import OpenAIModel

model = OpenAIModel(model_id="gpt-4o-mini")
agent = Agent(model=model)

with start_session("my-session", {"user_id": "user-123"}):
    response = agent("What is 2+2?")
    print(response.message['content'][0]['text'])
```

## With Langfuse

If you already use Langfuse to trace Strands (via OpenLIT), set `allowed_instrumentations=[]` so Brizz stays out and only ingests Langfuse's OTel spans.

```python
from dotenv import load_dotenv
load_dotenv()

import os
from brizz import Brizz

Brizz.initialize(
    api_key=os.getenv("BRIZZ_API_KEY"),
    app_name="my-app",
    allowed_instrumentations=[],
)

from langfuse import get_client, propagate_attributes
import openlit
langfuse = get_client()
openlit.init(tracer=langfuse._otel_tracer, disable_batch=True)

from strands import Agent
from strands.models.openai import OpenAIModel

model = OpenAIModel(model_id="gpt-4o-mini")
agent = Agent(model=model)

with langfuse.start_as_current_observation(as_type="span", name="strands-run") as span:
    with propagate_attributes(session_id="my-session", user_id="user-123"):
        response = agent("What is 2+2?")
        print(response.message['content'][0]['text'])
```

## Complete example

A single file you can copy, set `BRIZZ_API_KEY` + `OPENAI_API_KEY`, and run with `python agent.py`.

```python
# agent.py
from dotenv import load_dotenv
load_dotenv()

import os
# Initialize Brizz BEFORE importing Strands so auto-instrumentation can hook.
from brizz import Brizz, start_session, emit_event

Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-app",
    environment=os.getenv("APP_ENV", "development"),
)

from strands import Agent
from strands.models.openai import OpenAIModel

model = OpenAIModel(model_id="gpt-4o-mini")
agent = Agent(model=model)


def run_turn(session_id: str, user_id: str, user_message: str) -> str:
    # The properties dict on start_session becomes session-wide span attributes.
    with start_session(session_id, {"user_id": user_id, "plan": "enterprise"}) as session:
        session.set_input(user_message)

        response = agent(user_message)
        text = response.message["content"][0]["text"]
        session.set_output(text)

        # User feedback — map "feedback.positive" / "feedback.negative" to system
        # events in Org Settings -> Event so it powers filters and the Overview chart.
        emit_event(
            "feedback.positive",
            attributes={"category": "helpfulness"},
            body={"comment": "Exactly what I needed!", "context": "support-agent"},
        )
        return text


if __name__ == "__main__":
    print(run_turn("session-123", "user-42", "What is 2+2?"))
```

**Gotchas**

- Brizz initialization must happen before `from strands import Agent`.
- Already tracing Strands via Langfuse + OpenLIT? Use the `allowed_instrumentations=[]` pattern in [With Langfuse](#with-langfuse) above.

## See also

- [Python SDK](/docs/sdks/python.md) — full SDK reference.
- [Sessions](/docs/instrument/sessions.md) — session capture patterns.
- [Identify users](/docs/instrument/identify-users.md) — attach user properties to Strands runs.


---

# LiveKit Agents

Install Brizz with the LiveKit Agents voice framework.

Quickstart for the LiveKit Agents integration shown during onboarding. For the full Python SDK reference, see the [Python SDK guide](/docs/sdks/python.md).

## Install

:::tabs
:::tab[pip]
```bash
pip install brizz
```
:::tab[uv]
```bash
uv add brizz
```
:::tab[poetry]
```bash
poetry add brizz
```
:::

LiveKit Agents emits OpenTelemetry spans natively, so no extra Brizz package or plugin is needed — Brizz picks them up as soon as it's initialized.

The examples below use LiveKit's OpenAI and Silero plugins. Your agent already has whichever plugins it needs; add these only if you're starting from the example:

:::tabs
:::tab[pip]
```bash
pip install "livekit-agents[openai,silero]"
```
:::tab[uv]
```bash
uv add "livekit-agents[openai,silero]"
```
:::tab[poetry]
```bash
poetry add "livekit-agents[openai,silero]"
```
:::

## Initialize and run

Brizz reads `BRIZZ_DSN` — create one under **Organization Settings → API Keys → Create Telemetry API Key → Server DSN** (see [Server DSN](/docs/admin/server-dsn.md)), then put it in your agent's environment:

```bash
export BRIZZ_DSN="https://<credential>@<ingest-host>/<service-name>"
```

```python
from dotenv import load_dotenv
load_dotenv()

from brizz import Brizz, astart_session

# LiveKit runs each job in its own process, which re-imports this module —
# initialize at import time so every job process is instrumented.
# Reads BRIZZ_DSN; pass dsn="..." to set it in code.
Brizz.initialize()

from livekit.agents import Agent, AgentSession, JobContext, WorkerOptions, cli
from livekit.plugins import openai, silero


async def entrypoint(ctx: JobContext):
    # Recommended: name the session yourself so you can attach attributes to it.
    async with astart_session(ctx.room.name):
        session = AgentSession(
            stt=openai.STT(),
            llm=openai.LLM(model="gpt-4o-mini"),
            tts=openai.TTS(),
            vad=silero.VAD.load(),
        )
        await session.start(
            room=ctx.room,
            agent=Agent(instructions="You are a helpful voice assistant."),
        )


if __name__ == "__main__":
    cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))
```

## Complete example

A single file you can copy and set `BRIZZ_DSN` on (`.env` works — the example loads it with `load_dotenv()`). Run `python agent.py console` to talk to it in the terminal without a LiveKit room, or `python agent.py dev` once your LiveKit credentials are set.

It hands off from a greeter to a weather specialist, so the session renders one lane per agent.

```python
# agent.py
from dotenv import load_dotenv
load_dotenv()

from brizz import Brizz, astart_session, emit_event

# LiveKit runs each job in its own process, which re-imports this module —
# initialize at import time so every job process is instrumented.
Brizz.initialize()

from livekit.agents import Agent, AgentSession, JobContext, RunContext, WorkerOptions, cli, function_tool
from livekit.plugins import openai, silero


@function_tool
async def lookup_weather(context: RunContext, city: str) -> str:
    """Look up the current weather for a city."""
    return f"It is 21 degrees and sunny in {city}."


class WeatherAgent(Agent):
    """Answers weather questions."""

    def __init__(self) -> None:
        super().__init__(
            instructions="You are the weather specialist. Answer weather questions in one sentence.",
            tools=[lookup_weather],
        )


@function_tool
async def transfer_to_weather(context: RunContext) -> Agent:
    """Hand the conversation to the weather specialist."""
    return WeatherAgent()


class Greeter(Agent):
    """Greets the caller and hands weather questions to the specialist."""

    def __init__(self) -> None:
        super().__init__(
            instructions=(
                "You are the greeter. Greet the user in one sentence. "
                "Whenever the user asks about the weather, call transfer_to_weather."
            ),
            tools=[transfer_to_weather],
        )


async def entrypoint(ctx: JobContext):
    # Recommended: name the session yourself so you can attach attributes to it.
    async with astart_session(ctx.room.name):
        session = AgentSession(
            stt=openai.STT(),
            llm=openai.LLM(model="gpt-4o-mini"),
            tts=openai.TTS(),
            vad=silero.VAD.load(),
        )
        await session.start(room=ctx.room, agent=Greeter())

        # User feedback — map "feedback.positive" / "feedback.negative" to system
        # events in Org Settings -> Event so it powers filters and the Overview chart.
        # Call this wherever your room UI reports a thumbs-up/thumbs-down.
        emit_event("feedback.positive", attributes={"category": "helpfulness"})


if __name__ == "__main__":
    cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))
```

**Gotchas**

- Call `Brizz.initialize()` at module import time, not inside `if __name__ == "__main__":`. LiveKit runs each job in its own process, which re-imports this module — initializing under the main guard means job processes never get instrumented.
- `async with astart_session(ctx.room.name)` inside the entrypoint is the recommended way to name a session and get a `Session` object for attributes, events, and feedback. Skip it and Brizz falls back to the room name automatically.
- Prefer the room name as the session id. Anything the agent starts inside the `astart_session` block keeps that id, but work kicked off after the block exits falls back to the room name — so a custom id can split one call across two sessions.
- Sending telemetry to Brizz doesn't stop LiveKit Cloud from receiving its own — both exports run side by side.
- Voice sessions render as conversations: system prompt, user turns, agent replies, tool calls with their results, and errors. Per-turn instructions passed to `generate_reply(instructions=…)` appear as context on the reply they shaped.
- A session that hands off between agents shows each agent's turns separately, with the system prompt and tools that agent ran under.
- A caller talking over the agent lands on the session timeline as an `agent_interrupted` event, on the turn it cut short.

## Speech-to-speech

An agent built on a realtime model swaps the STT → LLM → TTS trio for a single model. Everything else in the example stays the same, and sessions capture both sides of the conversation either way:

```python
session = AgentSession(llm=openai.realtime.RealtimeModel())
```

## See also

- [Python SDK](/docs/sdks/python.md) — full SDK reference.
- [Sessions](/docs/instrument/sessions.md) — session capture patterns.
- [Identify users](/docs/instrument/identify-users.md) — attach user properties to LiveKit runs.


---

# MCP (auto-instrument your server)

Add Brizz observability to an MCP server you operate, in Python (FastMCP) or TypeScript.

This page is for **adding Brizz to an MCP server you run**. If you want to connect Brizz *to* an AI agent (Claude, Cursor, Codex) so the agent can query your Brizz data, see the [MCP server integration](/docs/integrations/mcp-server/overview.md) instead.

Brizz auto-instruments every MCP tool call on your server — arguments, results, errors, and session context are captured as spans without touching tool bodies. Works for **Python** (via [FastMCP](https://gofastmcp.com/)) and **TypeScript** (via [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk)).

For the full SDK reference, see the [Python SDK guide](/docs/sdks/python.md) or the [Node.js / TypeScript SDK guide](/docs/sdks/typescript.md).

:::warning Initialization Order
Call `Brizz.initialize()` **before** constructing the MCP server. The auto-instrumentation hooks the MCP protocol module at init time — if the server is built first, tool calls won't be traced.
:::

## Python (FastMCP)

### Install

:::tabs
:::tab[pip]
```bash
pip install brizz fastmcp
```
:::tab[uv]
```bash
uv add brizz fastmcp
```
:::tab[poetry]
```bash
poetry add brizz fastmcp
```
:::

Install `fastmcp` alongside `brizz` — Brizz instruments your FastMCP server automatically once it's present in the environment.

### Server

```python
import os
from brizz import Brizz
from fastmcp import FastMCP

Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-mcp-server",
)

mcp = FastMCP("my-mcp-server")

@mcp.tool()
def echo(text: str) -> str:
    """Echo a message back."""
    return text

if __name__ == "__main__":
    mcp.run(transport="http", host="127.0.0.1", port=8765)
```

Supported transports: `http` (streamable HTTP, default), `sse`, and `stdio`.

### FastAPI

Mount the MCP server alongside an existing FastAPI API. See [FastMCP's FastAPI guide](https://gofastmcp.com/integrations/fastapi).

#### Basic mounting

Serve the MCP server as a sub-app instead of calling `mcp.run(...)`:

```python
import os
import uvicorn
from brizz import Brizz
from fastapi import FastAPI
from fastmcp import FastMCP

Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-mcp-server",
)

mcp = FastMCP("my-mcp-server")

@mcp.tool()
def echo(text: str) -> str:
    """Echo a message back."""
    return text

# Build the MCP app and mount it. The endpoint lands at <mount-prefix>/mcp.
mcp_app = mcp.http_app(path="/mcp")
app = FastAPI(lifespan=mcp_app.lifespan)   # FastAPI must use the MCP app's lifespan
app.mount("/mcp-server", mcp_app)          # served at http://127.0.0.1:8000/mcp-server/mcp

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)
```

:::warning Lifespan
Pass the MCP app's lifespan to FastAPI (`FastAPI(lifespan=mcp_app.lifespan)`). Without it the MCP server never starts up and every request fails.
:::

Telemetry is identical to the standalone server — `Brizz.initialize()` instruments the `FastMCP(...)` instance no matter how it's served.

## TypeScript

### Install

:::tabs
:::tab[npm]
```bash
npm install @brizz/sdk zod
```
:::tab[yarn]
```bash
yarn add @brizz/sdk zod
```
:::tab[pnpm]
```bash
pnpm add @brizz/sdk zod
```
:::

### Server

Pass `instrumentModules.mcp.protocolModule` so Brizz can patch the MCP protocol module even when bundled by Next.js, Webpack, or run under `tsx`.

```typescript
import { randomUUID } from 'node:crypto';
import { createServer } from 'node:http';

import { Brizz } from '@brizz/sdk';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import * as protocolModule from '@modelcontextprotocol/sdk/shared/protocol.js';
import { z } from 'zod';

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY,
  appName: 'my-mcp-server',
  instrumentModules: { mcp: { protocolModule } },
});

const server = new McpServer({ name: 'my-mcp-server', version: '1.0.0' });

server.registerTool(
  'echo',
  { description: 'Echoes the input text back', inputSchema: { text: z.string() } },
  async ({ text }) => {
    return { content: [{ type: 'text', text: `echo:${text}` }] };
  },
);

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
});
await server.connect(transport);

createServer((req, res) => void transport.handleRequest(req, res))
  .listen(8765, '127.0.0.1', () => {
    console.log('MCP server listening on http://127.0.0.1:8765/mcp');
  });
```

For `stdio` transport, replace the HTTP setup with `new StdioServerTransport()` from `@modelcontextprotocol/sdk/server/stdio.js`.

## Identity & custom attributes

Two kinds of attributes show up on a tool-call trace — keep them separate:

- **Identity** (who is calling — user, company/tenant) belongs to your auth layer and should land on the whole tool call: the `tools/call` span **and** every child span it creates. Attach it once per call at your auth layer; the exact calls differ per SDK (see below), because Python authenticates before the span opens while TypeScript authenticates after.
- **Tool-specific attributes** (anything about a single tool's work) belong to that one tool's span. Set them inside the tool with `set_current_span_custom_properties` / `setCurrentSpanCustomProperties`.

When identity comes from an HTTP `Authorization` header (as below), it requires an HTTP-based transport (`http`/`sse`); `stdio` carries no headers.

### Python

Authenticate in a middleware and propagate identity with `custom_properties`. The middleware runs before FastMCP opens the `tools/call` span, so that span and its children inherit the attributes:

```python
from brizz import custom_properties, set_current_span_custom_properties
from fastmcp.server.dependencies import get_http_headers
from fastmcp.server.middleware import Middleware

class AuthMiddleware(Middleware):
    async def on_call_tool(self, context, call_next):
        # `authorization` is excluded from get_http_headers() by default — opt it back in.
        identity = resolve_identity(get_http_headers(include={"authorization"}))
        # -> {"user.id": "...", "company.id": "..."}; propagates to tools/call + children.
        with custom_properties(identity):
            return await call_next(context)

mcp.add_middleware(AuthMiddleware())

@mcp.tool()
def echo(text: str) -> str:
    set_current_span_custom_properties({"echo.text_length": str(len(text))})  # this span only
    return text
```

### TypeScript

Brizz opens the `tools/call` span before your handler runs, so it's already the active span inside the handler. Wrap each handler to authenticate, stamp that active span, and propagate to its children:

```typescript
import { callWithProperties, setCurrentSpanCustomProperties } from '@brizz/sdk';

function withAuth(handler) {
  return (args, extra) => {
    const identity = resolveIdentity(extra.requestInfo?.headers); // { 'user.id': '...', 'company.id': '...' }
    setCurrentSpanCustomProperties(identity);                      // stamp the active tools/call span
    return callWithProperties(identity, () => handler(args, extra)); // propagate to child spans
  };
}

server.registerTool(
  'echo',
  { description: 'Echoes the input text back', inputSchema: { text: z.string() } },
  withAuth(({ text }) => {
    setCurrentSpanCustomProperties({ 'echo.text_length': text.length }); // this span only
    return { content: [{ type: 'text', text: `echo:${text}` }] };
  }),
);
```

## Complete example

End-to-end servers you can copy and run. Identity flows from an `Authorization` header, so use the HTTP transport (`stdio` carries no headers).

### Python (FastMCP)

Run with `python server.py` (`BRIZZ_API_KEY` required).

```python
# server.py
import os
from brizz import Brizz, custom_properties, set_current_span_custom_properties
from fastmcp import FastMCP
from fastmcp.server.dependencies import get_http_headers
from fastmcp.server.middleware import Middleware

# Initialize Brizz BEFORE constructing FastMCP — auto-instrumentation hooks
# the MCP protocol module at init time.
Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-mcp-server",
    environment=os.getenv("APP_ENV", "development"),
)

mcp = FastMCP("my-mcp-server")


def resolve_identity(headers: dict[str, str]) -> dict[str, str]:
    # Replace with your real auth — JWT decode, API key lookup, etc.
    token = headers.get("authorization", "")
    return {"user.id": "user-42", "company.id": "acme"} if token else {}


class AuthMiddleware(Middleware):
    async def on_call_tool(self, context, call_next):
        # `authorization` is excluded from get_http_headers() by default — opt in.
        identity = resolve_identity(get_http_headers(include={"authorization"}))
        # Propagates to the tools/call span AND every child span the tool creates.
        with custom_properties(identity):
            return await call_next(context)


mcp.add_middleware(AuthMiddleware())


@mcp.tool()
def echo(text: str) -> str:
    """Echo a message back."""
    # Tool-specific attributes go on THIS span only.
    set_current_span_custom_properties({"echo.text_length": str(len(text))})
    return text


if __name__ == "__main__":
    mcp.run(transport="http", host="127.0.0.1", port=8765)
```

### TypeScript

Run with `node server.mjs` (`BRIZZ_API_KEY` required).

```typescript
// server.ts
import { randomUUID } from 'node:crypto';
import { createServer } from 'node:http';

import {
  Brizz,
  callWithProperties,
  setCurrentSpanCustomProperties,
} from '@brizz/sdk';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import * as protocolModule from '@modelcontextprotocol/sdk/shared/protocol.js';
import { z } from 'zod';

// Init BEFORE constructing the server. instrumentModules.mcp.protocolModule
// is required so Brizz patches the right module under bundlers / tsx.
Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY!,
  appName: 'my-mcp-server',
  environment: process.env.NODE_ENV ?? 'development',
  instrumentModules: { mcp: { protocolModule } },
});

function resolveIdentity(headers?: Record<string, string | string[] | undefined>) {
  // Replace with your real auth — JWT decode, API key lookup, etc.
  const auth = headers?.['authorization'];
  return auth ? { 'user.id': 'user-42', 'company.id': 'acme' } : {};
}

function withAuth<A>(handler: (args: A, extra: any) => any) {
  return (args: A, extra: any) => {
    const identity = resolveIdentity(extra.requestInfo?.headers);
    // Stamp the already-open tools/call span...
    setCurrentSpanCustomProperties(identity);
    // ...and propagate identity to any child spans the handler creates.
    return callWithProperties(identity, () => handler(args, extra));
  };
}

const server = new McpServer({ name: 'my-mcp-server', version: '1.0.0' });

server.registerTool(
  'echo',
  { description: 'Echoes the input text back', inputSchema: { text: z.string() } },
  withAuth(({ text }: { text: string }) => {
    setCurrentSpanCustomProperties({ 'echo.text_length': text.length });
    return { content: [{ type: 'text', text: `echo:${text}` }] };
  }),
);

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
});
await server.connect(transport);

createServer((req, res) => void transport.handleRequest(req, res))
  .listen(8765, '127.0.0.1', () => {
    console.log('MCP server listening on http://127.0.0.1:8765/mcp');
  });
```

**Gotchas**

- Call `Brizz.initialize()` **before** constructing the MCP server. Hooks install at init time.
- TypeScript: pass `instrumentModules.mcp.protocolModule` so the patch survives bundlers (Next.js, Webpack, `tsx`).
- Identity goes at the auth boundary (middleware / `withAuth`) so it lands on the `tools/call` span and every child. Tool-specific attributes go inside the tool body via `set_current_span_custom_properties` / `setCurrentSpanCustomProperties`.
- HTTP-header–based identity requires `http` or `sse` transport — `stdio` carries no headers.

## Sessions on a stateless server

A stateless MCP server handles every request on a fresh connection. That keeps it
cheap and easy to scale, but it means the server cannot tell that ten tool calls
came from one person doing one thing — so in Brizz each call arrives as its own
one-call session, and there is no conversation to read.

Turn on session tracking and Brizz asks the calling client to carry that context
for you:

- Your server publishes one extra tool, `brizz_start_session`
  ([rename it](#configuration)). The client calls it once and gets back a session id.
- Every one of your own tools gains a `brizz_mcp_session_id` parameter
  ([rename it](#configuration)), and its description tells the client to send that
  id back on every call.
- Optionally, a `brizz_intent` parameter asks what the user is trying to
  accomplish, in the client's own words.

That is the whole handshake, and it rides on the tool schemas the client already
reads. Nothing is required of your customers — they don't install anything, change
anything, or even know it happened.

### The handshake

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Your MCP server

    C->>S: tools/list
    S-->>C: your tools (+ brizz_mcp_session_id)<br/>+ brizz_start_session

    C->>S: tools/call brizz_start_session
    S-->>C: "a1b2c3…"

    Note over C: keeps the id for<br/>the whole conversation

    C->>S: tools/call search_orders (a1b2c3…)
    C->>S: tools/call get_order (a1b2c3…)
    C->>S: tools/call request_refund (a1b2c3…)

    Note over S: all three land in<br/>one Brizz session
```

Every call above arrives on its own connection. The id is what ties them together.

### Turn it on

:::tabs
:::tab[Python]
```python
Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-mcp-server",
    mcp_session_tracking=True,
)
```
:::tab[TypeScript]
```typescript
Brizz.initialize({
  appName: 'my-mcp-server',
  apiKey: process.env.BRIZZ_API_KEY,
  mcpSessionTracking: true,
});
```
:::

You can also set `BRIZZ_MCP_SESSION_TRACKING=true` instead of passing the flag.

### Configuration

`True` turns everything on with the default names. Pass the config object instead to
choose what Brizz asks for and what it calls things.

:::tabs
:::tab[Python]
```python
from brizz import MCPSessionTrackingConfig

Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-mcp-server",
    mcp_session_tracking=MCPSessionTrackingConfig(
        enabled=True,
        intent=True,
        session_param_name="brizz_mcp_session_id",
        session_tool_name="brizz_start_session",
    ),
)
```

| Option | Default | What it does |
| --- | --- | --- |
| `enabled` | `False` | Master switch. Everything else applies only when this is on. |
| `intent` | `True` | Also ask what the user is trying to accomplish. |
| `session_param_name` | `"brizz_mcp_session_id"` | Name of the parameter added to your tools. |
| `session_tool_name` | `"brizz_start_session"` | Name of the tool that issues the session id. |

:::tab[TypeScript]
```typescript
Brizz.initialize({
  appName: 'my-mcp-server',
  apiKey: process.env.BRIZZ_API_KEY,
  mcpSessionTracking: {
    enabled: true,
    intent: true,
    sessionParamName: 'brizz_mcp_session_id',
    sessionToolName: 'brizz_start_session',
  },
});
```

| Option | Default | What it does |
| --- | --- | --- |
| `enabled` | `false` | Master switch. Everything else applies only when this is on. |
| `intent` | `true` | Also ask what the user is trying to accomplish. |
| `sessionParamName` | `'brizz_mcp_session_id'` | Name of the parameter added to your tools. |
| `sessionToolName` | `'brizz_start_session'` | Name of the tool that issues the session id. |

:::

Rename the parameter or the tool when the defaults would collide with something your
server already publishes. The tool descriptions Brizz writes use whatever names you
pick, so the calling client is told the right ones.

### What changes for you

Nothing in your code. Your tools keep their own signatures and never receive the
extra parameters — Brizz removes them before your tool runs, and keeps them out of
the arguments it records. Tools you register while the server is running get the
same treatment, because Brizz works on whatever your server publishes at the moment
it publishes it.

A client that ignores the handshake keeps working exactly as before; those calls
simply fall back to one session each.

Your `additionalProperties` setting is left as you wrote it. If a tool's schema is
closed (`additionalProperties: false`), Brizz also marks `brizz_intent` required on
that tool, because a closed schema is how OpenAI strict function calling is
signalled and strict mode requires every property to be listed in `required` — so
the tool stays usable for clients that forward your schemas into strict mode.

:::info
This is off by default, because it changes the tools your server advertises.
:::

## Where the data lands

Once your MCP server is reporting, Brizz adds an **MCP Servers** tab to the dashboard for that service — an operations console built around tool calls rather than conversations:

- **KPI strip** — total tool invocations, sessions, success rate, and the count of distinct issues affecting the service.
- **Tool calls** — call volume over time, with markers for tool-definition changes so a shift lines up with a change to the tool itself.
- **Tool inventory** — a paged, sortable list with call volume, usage share, issue count, p95 latency, last-call time, and a health stripe per tool. Filter it to one or more tools, or open a row for its detail drawer; registered tools with no calls in the selected range appear as inactive.
- **Tool latency over time** — avg, p50, p75, or p95 for one selected tool. It defaults to the slowest tool for the selected statistic within the active date range and filters.
- **Top issues** — the most impactful issues for the service.

:::info
The MCP Servers tab appears only for services registered as MCP servers, and it's a plan-gated feature. If you're reporting MCP telemetry but don't see the tab, [contact us](mailto:support@brizz.ai).
:::

## See also

- [MCP server integration](/docs/integrations/mcp-server/overview.md) — the *other* MCP page: connecting Brizz to AI agents (not the other way around).
- [Python SDK](/docs/sdks/python.md) and [Node.js / TypeScript SDK](/docs/sdks/typescript.md) — full SDK references.
- [Identify users](/docs/instrument/identify-users.md) — propagating identity from auth into MCP tool spans.


---

# Langfuse SDK

Instrument your assistant with Langfuse and let Brizz ingest the OTel spans.

If you already instrument your assistant with the [Langfuse Python SDK](https://langfuse.com/), Brizz ingests its OpenTelemetry spans directly. Sessions, generations, tool calls, and per-turn context all show up in the Brizz dashboard without you having to call Brizz's session API (`start_session` / `set_input` / `set_output`).

## Quick start

Let Langfuse own AI-library instrumentation and have Brizz ingest its OTel spans. Brizz events and sessions still work the same way.

```python
import os
from brizz import Brizz

Brizz.initialize(
    api_key=os.getenv("BRIZZ_API_KEY"),
    app_name="my-app",
    allowed_instrumentations=[], # Disable auto-instrumentation
)

# Langfuse reads LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_HOST from env.
from langfuse import get_client
langfuse = get_client()
```

Group related LLM calls under a Langfuse session. Brizz picks up `session.id` from Langfuse automatically.

```python
from langfuse import get_client, propagate_attributes
# Drop-in replacement: Langfuse's OpenAI wrapper creates observations automatically.
from langfuse.openai import openai

langfuse = get_client()

with langfuse.start_as_current_observation(as_type="span", name="chat") as span:
    with propagate_attributes(session_id="chat-session-123", user_id="user-123"):
        response = openai.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": "Hello"}],
        )

# Alternative — wrap with Brizz's session API instead:
# with start_session("chat-session-123"):
#     response = openai.chat.completions.create(...)
```

## Per-turn context

Brizz renders a collapsible **Context** panel on both the user and the assistant message in the conversation view. Populate the user side with a reserved `brizz_user_context` key and the assistant side with `brizz_assistant_context`. Brizz reads only those two keys — anything else in Langfuse metadata stays out of the Context panel.

You can attach each bag two ways, and they compose:

- **Trace-level** — `propagate_attributes(metadata=…)`. Applies to the turn produced inside the block.
- **Per-observation** — `start_as_current_observation(metadata=…)` (or `span.update(metadata=…)`). Applies to that observation.

When the same key is set both ways, the **per-observation value wins** — per key, so trace-only keys still merge in.

Langfuse's metadata bag accepts string values only, so JSON-encode each bag. Set the trace-level context with `propagate_attributes`:

```python
import json
from langfuse import get_client, propagate_attributes

langfuse = get_client()

with propagate_attributes(
    session_id="chat-session-123",
    user_id="user-42",
    metadata={
        "brizz_user_context": json.dumps({
            "ui_scope_id":    scope_id,
            "ui_origin_view": view,
            "ui_model_type":  model_type,
        }),
        "brizz_assistant_context": json.dumps({
            "answer_source": "knowledge_base",
            "confidence":    "high",
        }),
    },
):
    response = await graph.ainvoke(state)
```

To override (or add) context for a single observation, pass the same keys on `start_as_current_observation`. Any shared key wins over the trace-level value:

```python
with langfuse.start_as_current_observation(
    as_type="span",
    name="chat",
    metadata={
        "brizz_user_context": json.dumps({"ui_scope_id": scope_id}),
    },
) as span:
    response = await graph.ainvoke(state)
    # Assistant outcome is known only after the model responds — set it on the
    # observation once you have it.
    span.update(metadata={
        "brizz_assistant_context": json.dumps({"answer_source": "live_tool"}),
    })
```

Brizz parses each JSON object server-side and renders its key/values on the Context panel — `brizz_user_context` on the user message, `brizz_assistant_context` on the assistant message produced inside that block. Use a fresh scope per turn — the contents are per-turn, not per-session. An empty value is ignored (it never blanks out a value set the other way).

Identity fields like `user_id`, `user_email`, or `organization_id` — placed at the top level of Langfuse metadata (alongside `brizz_user_context`, not inside it) — are preserved on the underlying span but kept out of the Context panel so it stays focused on the signals you choose to surface.

## Complete example

A single file you can copy and run with `python main.py`. Required env: `BRIZZ_API_KEY`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST` (optional — defaults to Langfuse Cloud), `OPENAI_API_KEY`.

```python
# main.py
from dotenv import load_dotenv
load_dotenv()

import json
import os
from brizz import Brizz, emit_event

# allowed_instrumentations=[] tells Brizz to skip AI-library auto-instrumentation
# and only ingest Langfuse's OTel spans (which already cover OpenAI / LangChain / etc).
Brizz.initialize(
    api_key=os.environ["BRIZZ_API_KEY"],
    app_name="my-app",
    environment=os.getenv("APP_ENV", "development"),
    allowed_instrumentations=[],
)

# Langfuse's OpenAI drop-in creates observations for every call automatically.
from langfuse import get_client, propagate_attributes
from langfuse.openai import openai

langfuse = get_client()


def run_turn(session_id: str, user_id: str, user_message: str, scope_id: str) -> str:
    with langfuse.start_as_current_observation(as_type="span", name="chat") as span:
        # Langfuse metadata is string-only — JSON-encode each Brizz Context panel
        # payload. user_id / session_id stay top-level on propagate_attributes.
        with propagate_attributes(
            session_id=session_id,
            user_id=user_id,
            metadata={
                # User-turn context (trace-level): known before the model runs.
                "brizz_user_context": json.dumps({
                    "ui_scope_id": scope_id,
                    "ui_origin_view": "support_inbox",
                }),
            },
        ):
            response = openai.chat.completions.create(
                model="gpt-4o-mini",
                messages=[{"role": "user", "content": user_message}],
            )
            reply = response.choices[0].message.content

        # Assistant-turn context (per-observation): known only after the model
        # responds. A shared key here would win over any trace-level value.
        span.update(metadata={
            "brizz_assistant_context": json.dumps({
                "answer_source": "billing_kb",
                "finish_reason": response.choices[0].finish_reason,
            }),
        })

    # Brizz custom events still work — the same session_id is honored.
    # Map "feedback.positive" / "feedback.negative" to system events in
    # Org Settings -> Event so it powers filters and the Overview chart.
    emit_event(
        "feedback.positive",
        attributes={"category": "helpfulness"},
        body={"comment": "Exactly what I needed!", "context": "billing-assistant"},
    )
    return reply


if __name__ == "__main__":
    print(run_turn("chat-session-123", "user-42", "Why is my bill high?", "scope-9"))
```

**Gotchas**

- `allowed_instrumentations=[]` is what keeps Brizz and Langfuse from double-instrumenting the same OpenAI / LangChain call.
- Langfuse metadata (`propagate_attributes` or observation) accepts strings only — JSON-encode each context bag (`brizz_user_context`, `brizz_assistant_context`).
- Open a fresh `propagate_attributes` scope per user turn; the Context panel is per-turn, not per-session.
- `user_id` (and `user_email`, `organization_id`) belong at the top level of `propagate_attributes`, not inside `brizz_user_context`.

## See also

- [Python SDK](/docs/sdks/python.md) — full SDK reference.
- [Sessions](/docs/instrument/sessions.md) — session capture patterns.
- [Identify users](/docs/instrument/identify-users.md) — propagating identity into spans.


---

# Identify users

Attach a user identity to your telemetry to unlock user-level analytics and insights.

User identification lets you track individual users across sessions, unlocking analytics like user journeys, retention, and per-user debugging.

## Why track users?

When you attach a user id to your telemetry, Brizz can:

- **Track user journeys** across multiple AI interactions
- **Identify power users** and understand their behavior patterns
- **Measure retention** and engagement over time
- **Spot issues** affecting specific user segments

## Identify a user

Call `set_user` / `setUser` inside a session. The identity applies to the turn's spans and propagates to the child spans created within the same context. Only `id` is required; every other field is optional.

:::tabs
:::tab[Python]

```python
from brizz import set_user, start_session

with start_session("session-123"):
    set_user(id=user.id, email=user.email, name=user.name, role=user.role, plan=user.plan)

    reply = agent.run(prompt)
```

:::tab[Node.js]

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

await startSession('session-123', async () => {
  setUser({ id: user.id, email: user.email, name: user.name, role: user.role, plan: user.plan });

  return agent.run(prompt);
});
```

:::

Each field maps to its own dotted attribute — `brizz.user.id`, `brizz.user.email`, `brizz.user.name`, `brizz.user.role`, `brizz.user.plan`. The same setter is available on the session object (`session.set_user(...)` / `session.setUser(...)`).

## Extra attributes with traits

For anything beyond the named fields, pass a `traits` record. Each entry becomes `brizz.user.<key>`; named fields win on key collision.

:::tabs
:::tab[Python]

```python
set_user(id=user.id, traits={"department": "sales", "signup_source": "referral"})
```

:::tab[Node.js]

```typescript
setUser({ id: user.id, traits: { department: 'sales', signup_source: 'referral' } });
```

:::

## Scoped form

To apply identity to a single block and reset it on exit, use the scoped wrapper instead of the imperative setter.

:::tabs
:::tab[Python]

```python
from brizz import with_user

# Runs process_request with brizz.user.* on every span created inside it, then resets.
with_user("user-123", process_request, email="ada@example.com", name="Ada Lovelace")
```

:::tab[Node.js]

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

await callWithUser({ id: 'user-123', email: 'ada@example.com', name: 'Ada Lovelace' }, processRequest);
```

:::

## Legacy: user properties on the session

You can also pass user properties when you start the session. This still works, but the typed `set_user` / `setUser` above is the recommended path.

:::tabs
:::tab[Python]

```python
from brizz import start_session

with start_session("session-123", {"user_id": "user-42", "user_name": "Ada Lovelace", "user_email": "ada@example.com"}):
    response = openai.chat.completions.create(...)
```

:::tab[Node.js]

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

await startSession('session-123', async (session) => {
  session.updateProperties({ user_id: 'user-42', user_name: 'Ada Lovelace', user_email: 'ada@example.com' });

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

:::

### Accepted property names (legacy)

These names are recognized as user identifiers **in the session-properties path above**:

| Property     | Variants                               |
| ------------ | -------------------------------------- |
| `user_id`    | `userId`, `UserID`, `USER_ID`          |
| `user_name`  | `userName`, `UserName`, `USER_NAME`    |
| `user_email` | `userEmail`, `UserEmail`, `USER_EMAIL` |
| `user`       | `User`                                 |

## Best practices

1. **Use consistent IDs** — always use the same user id format across sessions.
2. **Identify every session** — even anonymous users can carry a temporary id for session linking.

## See also

- [Identify organizations](/docs/instrument/identify-organizations.md) — attach the account or workspace the user belongs to.
- [Message IDs](/docs/instrument/message-ids.md) — tag individual replies so you can reference them later.
- [Sessions](/docs/instrument/sessions.md) — attach user properties to the session you're already creating.
- [User intents](/docs/platform/user-intents.md) and [User journeys](/docs/platform/user-journeys.md) — what becomes available once users are identified.


---

# Identify organizations

Attach the account, workspace, or tenant a user belongs to for org-level analytics.

Organization identification groups users under the account, workspace, or tenant they belong to — so you can see usage, cost, and quality per customer, not just per user.

## Why track organizations?

When you attach an organization id to your telemetry, Brizz can:

- **Roll up activity by account** — usage and cost per customer
- **Compare segments** — behavior across plans, industries, or tiers
- **Spot at-risk accounts** — issues concentrated in a single organization

## Identify an organization

Call `set_organization` / `setOrganization` inside a session. It applies to the turn's spans and propagates to the child spans created within the same context. Only `id` is required; every other field is optional.

For `id`, pass whatever your product already uses as the customer identifier — an account id, customer id, or company id. Brizz builds the per-customer roll-ups from `brizz.organization.*`, so an identifier sent under a name of your own stays a plain attribute and no account view appears.

:::tabs
:::tab[Python]

```python
from brizz import set_organization, start_session

with start_session("session-123"):
    set_organization(id=org.id, name=org.name, plan=org.plan, domain=org.domain)

    reply = agent.run(prompt)
```

:::tab[Node.js]

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

await startSession('session-123', async () => {
  setOrganization({ id: org.id, name: org.name, plan: org.plan, domain: org.domain });

  return agent.run(prompt);
});
```

:::

Each field maps to its own dotted attribute — `brizz.organization.id`, `brizz.organization.name`, `brizz.organization.plan`, `brizz.organization.domain`. The same setter is available on the session object (`session.set_organization(...)` / `session.setOrganization(...)`).

## Extra attributes with traits

For anything beyond the named fields, pass a `traits` record. Each entry becomes `brizz.organization.<key>`; named fields win on key collision. Segment attributes such as tier, industry, or region belong here — that keeps them grouped with the account instead of scattered as top-level attributes.

:::tabs
:::tab[Python]

```python
set_organization(id=org.id, traits={"industry": "fintech", "region": "emea"})
```

:::tab[Node.js]

```typescript
setOrganization({ id: org.id, traits: { industry: 'fintech', region: 'emea' } });
```

:::

## Scoped form

To apply the organization to a single block and reset it on exit, use the scoped wrapper instead of the imperative setter.

:::tabs
:::tab[Python]

```python
from brizz import with_organization

# Runs handle_org with brizz.organization.* on every span created inside it, then resets.
with_organization("org-123", handle_org, name="Acme Inc")
```

:::tab[Node.js]

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

await callWithOrganization({ id: 'org-123', name: 'Acme Inc', plan: 'enterprise' }, handleOrg);
```

:::

## Best practices

1. **Pair it with a user** — set the user and their organization together so both roll-ups stay in sync.
2. **Use stable IDs** — a durable account id (not a display name) keeps history consistent across renames.

## See also

- [Identify users](/docs/instrument/identify-users.md) — attach the individual end-user.
- [Message IDs](/docs/instrument/message-ids.md) — tag individual replies so you can reference them later.
- [Sessions](/docs/instrument/sessions.md) — the session these attributes attach to.


---

# Message IDs

Tag individual replies with your own id so you can reference them later — for example, to attach feedback.

A message id is your own identifier for a single reply in a conversation. Attaching it lets you reference that exact message later — most commonly to [record feedback](/docs/instrument/record-feedback.md) on it, even after the turn has finished.

## Set a message id

Call `set_message_id` / `setMessageId` inside a session, on the turn you want to tag. It applies to the turn's spans and propagates to the child spans created within the same context.

:::tabs
:::tab[Python]

```python
from brizz import set_message_id, start_session

with start_session("session-123"):
    set_message_id(message.id)  # your own id for this reply

    reply = agent.run(prompt)
```

:::tab[Node.js]

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

await startSession('session-123', async () => {
  setMessageId(message.id); // your own id for this reply

  return agent.run(prompt);
});
```

:::

The id is emitted as `brizz.message.id`. Use whatever id you already have for the reply — a database row id, a chat message id, or a UUID you generate.

## Scoped form

To apply the message id to a single block and reset it on exit, use the scoped wrapper instead of the imperative setter.

:::tabs
:::tab[Python]

```python
from brizz import message

with message("msg-123"):
    reply = agent.run(prompt)
```

:::tab[Node.js]

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

await callWithMessageId('msg-123', () => agent.run(prompt));
```

:::

## See also

- [Record feedback](/docs/instrument/record-feedback.md) — attach a 👍/👎 or rating to the message you tagged.
- [Sessions](/docs/instrument/sessions.md) — the session a message belongs to.
- [Identify users](/docs/instrument/identify-users.md) — attach the end-user behind the message.


---

# Custom events

Emit custom events to track product actions, business outcomes, and feedback signals.

Events let you capture specific actions, milestones, and outcomes inside your application. Traces capture *how* something happened (latency, tokens, tool calls); events capture *what* happened (`order.placed`, `feedback.positive`, `agent.goal.achieved`).

:::info
**Always attach events to a session.** Emit them inside a session context, or include `brizz.session.id` as an attribute. Without a session, events show up but can't be correlated with the conversation that produced them.
:::

## When to use events

Use events to track:

- **Business outcomes** — `subscription.upgraded`, `order.placed`, `signup.completed`
- **Agent milestones** — `agent.goal.achieved`, `tool.execution.failed`
- **User interactions** — `feedback.submitted`, `button.clicked`, `flow.abandoned`

:::tip
**Reporting a number?** If the thing you're recording is a measurement — an eval score, a rating, a latency — use [`record_metric`](/docs/instrument/record-metric.md) instead of a custom event. You get a typed shape (value, unit, scale, polarity) and a real metric you can filter and chart by, rather than a free-form attribute bag Brizz has to be told how to read.
:::

## Inside a session

Events emitted within a session context inherit the session ID automatically.

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

with start_session("session-123", {"user_id": "user-42"}):
    # Tagged with session.id = session-123 automatically
    emit_event("user.action", {"action": "chat", "flow": "smart"})
    response = openai.chat.completions.create(...)
```
:::tab[Node.js]
```typescript
import { withSessionId, emitEvent } from '@brizz/sdk';

const processChat = withSessionId('session-123', async () => {
  emitEvent('chat.started', { userId: 'user-42', flow: 'smart' });

  const result = await generateText({
    model: openai('gpt-4'),
    prompt: 'Hello!',
    experimental_telemetry: { isEnabled: true },
  });

  return result;
});
```
:::

## Outside a session

If you can't be inside a session scope, include the session ID as an attribute. Brizz will link the event to the right session.

:::tabs
:::tab[Python]
```python
from brizz import emit_event

emit_event(
    "order.placed",
    attributes={
        "brizz.session.id": "session-123",
        "plan": "pro",
        "amount": 49,
    },
    body={
        "order_id": "ord-789",
    },
)
```
:::tab[Node.js]
```typescript
import { emitEvent } from '@brizz/sdk';

emitEvent(
  'order.placed',
  {
    'brizz.session.id': 'session-123',
    plan: 'pro',
    amount: 49,
  },
  {
    order_id: 'ord-789',
  },
);
```
:::

## Attributes vs body

- **`attributes`** — high-level metadata you want to filter and chart by in the dashboard. Keep them flat: strings, numbers, booleans.
- **`body`** — free-form JSON, comments, or larger blobs that provide context but don't need to be filterable.

A good rule: if you'd ever want a chart "events broken down by X", put X in attributes.

## Naming conventions

Use dot-notation namespaces — they sort sensibly and stay readable as the catalog grows.

- `category.action` — `auth.login`, `feedback.positive`
- `category.object.action` — `agent.tool.error`, `agent.run.completed`

Stick to one convention across your team. `user.signup` everywhere is better than mixing `user.signup`, `UserSignup`, and `user_signed_up`.

## Best practices

1. **Use consistent naming.** Lock in a convention up front; renaming events later means breaking dashboard mappings.
2. **Track outcomes, not just starts.** Emit `agent.run.completed` *and* `agent.run.failed` — funnels need both.
3. **Keep payloads lean.** Don't dump every variable in scope into the body — use it for context, not state snapshots.
4. **Map system events early.** If you're emitting feedback events, set up [system event mappings](/docs/instrument/user-feedback.md) so dashboard filters and charts pick them up.

## See also

- [Events (in the dashboard)](/docs/platform/events.md) — browse and search the events you emit, and verify new instrumentation landed.
- [Sessions](/docs/instrument/sessions.md) — capture and enrich the session context events attach to.
- [User feedback](/docs/instrument/user-feedback.md) — the special case of feedback events and system mappings.
- [Identify users](/docs/instrument/identify-users.md) — attach user IDs so events correlate with the right person.


---

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


---

# External links

Attach an external URL — a Datadog trace, Sentry issue, or internal dashboard — to a Brizz session.

Attach an arbitrary URL to a session so you can jump straight from Brizz to the matching record in another tool — a Datadog trace, a Sentry issue, an internal dashboard, a LangSmith run. The link shows up as a clickable badge on the session's detail panel.

This is the explicit, server-SDK counterpart to the auto-detected session-replay badges (see [Sessions](/docs/instrument/sessions.md)): replay badges are detected for you (one per provider), while external links are caller-supplied — you pass the URL, and a session can carry several.

:::info
**External links attach to a session.** Call them inside a session context, or pass the session id explicitly. If no session id can be resolved the call is a safe no-op — it logs a warning and never throws, so telemetry can't break your code.
:::

## Attach a link

Inside a session, the link attaches to the active session automatically — via the session object or the standalone function.

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

with start_session("session-123") as session:
    # Via the session object.
    session.add_external_link("https://app.datadoghq.com/trace/abc", title="Datadog trace")

    # Or the module-level function — resolves the active session from context.
    add_external_link("https://sentry.io/issues/456", link_type="sentry")
```
:::tab[Node.js]
```typescript
import { addExternalLink, startSession } from '@brizz/sdk';

startSession('session-123', (session) => {
  // Via the session object.
  session.addExternalLink('https://app.datadoghq.com/trace/abc', { title: 'Datadog trace' });

  // Or the top-level function — resolves the active session from context.
  addExternalLink('https://sentry.io/issues/456', { linkType: 'sentry' });
});
```
:::

## Outside a session

If you're not inside a session scope, pass the session id explicitly.

:::tabs
:::tab[Python]
```python
from brizz import add_external_link

add_external_link(
    "https://grafana.example.com/d/abc",
    session_id="session-123",
    title="Grafana dashboard",
    link_type="dashboard",
)
```
:::tab[Node.js]
```typescript
import { addExternalLink } from '@brizz/sdk';

addExternalLink('https://grafana.example.com/d/abc', {
  sessionId: 'session-123',
  title: 'Grafana dashboard',
  linkType: 'dashboard',
});
```
:::

## Options

- **`url`** *(required)* — the link target.
- **`title`** *(optional)* — display text for the badge. Defaults to the URL's host.
- **`link_type` / `linkType`** *(optional)* — a free-form category. Defaults to `generic`.
- **`session_id` / `sessionId`** *(standalone function only)* — the target session. Defaults to the session in context.

Re-sending the same URL is idempotent — it updates the existing link (title and type included). Distinct URLs add separate badges.

## See also

- [Sessions](/docs/instrument/sessions.md) — capture and enrich the session a link attaches to.
- [Sessions in the dashboard](/docs/platform/sessions.md) — where the external-link badge appears.
- [Custom events](/docs/instrument/custom-events.md) — attach business signals to a session.


---

# User Feedback

Collect user feedback on AI responses, map it to system events, and filter sessions by sentiment.

User feedback is one of the most valuable signals for improving your AI product. Brizz lets you capture thumbs-up/thumbs-down reactions (or any custom feedback), surface them in your session views, and filter by sentiment to quickly find what's working and what isn't.

This guide walks through the full setup: emitting feedback events from your code, mapping them to system events in the dashboard, and using the built-in filters and charts.

## 1. Emitting Feedback Events

Use the SDK to emit events whenever a user provides feedback. There are two common patterns.

### Pattern A: Separate Event Names

Emit distinct events for positive and negative feedback:

:::tabs
:::tab[Python]

```python
from brizz import emit_event

# Positive feedback
emit_event(
    "feedback.positive",
    attributes={
        "category": "helpfulness"       # optional
    },
    body={
        "comment": "This was exactly what I needed!",  # optional
        "context": "billing-assistant"                 # optional
    }
)

# Negative feedback
emit_event(
    "feedback.negative",
    attributes={
        "category": "accuracy"          # optional
    },
    body={
        "comment": "The answer was outdated.",         # optional
        "context": "billing-assistant"                 # optional
    }
)
```

:::tab[Node.js]

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

// Positive feedback
emitEvent(
  'feedback.positive',
  {
    category: 'helpfulness', // optional
  },
  {
    comment: 'This was exactly what I needed!', // optional
    context: 'billing-assistant', // optional
  },
);

// Negative feedback
emitEvent(
  'feedback.negative',
  {
    category: 'accuracy', // optional
  },
  {
    comment: 'The answer was outdated.', // optional
    context: 'billing-assistant', // optional
  },
);
```

:::

### Pattern B: Single Event with Sentiment Attribute

Use one event name and differentiate with an attribute:

:::tabs
:::tab[Python]

```python
from brizz import emit_event

emit_event(
    "feedback.submitted",
    attributes={
        "sentiment": "positive",   # or "negative"
        "category": "helpfulness"  # optional
    },
    body={
        "comment": "Great response!",      # optional
        "context": "support-agent"         # optional
    }
)
```

:::tab[Node.js]

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

emitEvent(
  'feedback.submitted',
  {
    sentiment: 'positive', // or 'negative'
    category: 'helpfulness', // optional
  },
  {
    comment: 'Great response!', // optional
    context: 'support-agent', // optional
  },
);
```

:::

:::info
**Which pattern should I use?** Pattern A (separate events) is simpler to map in the dashboard — each event maps directly to a system event with no conditions. Pattern B (single event) is more flexible if you plan to add more sentiment types later (e.g., `neutral`), but requires conditional mapping.
:::

## 2. Configuring System Event Mappings

Once your application is emitting feedback events, you need to tell Brizz which of your custom events correspond to the built-in **Positive Feedback** and **Negative Feedback** system events. This enables feedback badges, preset filters, and dashboard charts.

### Opening Event Settings

1. Navigate to **Organization Settings** (gear icon in the sidebar).
2. Select the **Event** tab.

### Simple Mapping (Pattern A)

If you're using separate event names (`feedback.positive` / `feedback.negative`):

1. Find the **Positive Feedback** row in the system events list.
2. Click **Add Mapping**.
3. Select the trace service that emits your events.
4. Choose `feedback.positive` from the source event dropdown.
5. Click **Save**.

![Selecting a source event type for the Positive Feedback system event](event_mapping_multi_events.png)

Repeat for **Negative Feedback**, mapping it to `feedback.negative`.

### Conditional Mapping (Pattern B)

If you're using a single `feedback.submitted` event with a `sentiment` attribute:

1. Find the **Positive Feedback** row and click **Add Mapping**.
2. Select your trace service and choose `feedback.submitted` as the source event.
3. Click **Add Condition**.
4. Set the condition: `sentiment` **equals** `positive`.
5. Click **Save**.

![Maaping event params to system event](event_mapping_single_event_type.png)

Repeat for **Negative Feedback** with the condition `sentiment` **equals** `negative`.

### Multiple Mappings (OR Logic)

You can add more than one mapping to a system event. For example, if different services emit feedback events with different names, map them all to the same system event. Brizz treats multiple mappings as **OR** — any match triggers the system event.

:::info
**Changes take effect immediately.** Once you save a mapping, Brizz will start classifying incoming events. Historical events are not retroactively reclassified.
:::

## 3. Filtering Sessions by Feedback

With mappings configured, Brizz automatically tags sessions that contain feedback events.

### Preset Filters

The Sessions page includes built-in filter presets:

- **Negative Sessions** — Shows only sessions containing at least one negative feedback event.
- **Positive Sessions** — Shows only sessions containing at least one positive feedback event.

![Multiple event mappings configured for a single system event](event_mapping_multi_events.png)

Click a preset chip to apply it instantly.

### Feedback Badges

Sessions that contain feedback events display a badge in the session list:

- A **thumbs-up** icon for sessions with positive feedback.
- A **thumbs-down** icon for sessions with negative feedback.
- If a session has both, both badges appear.

![Session list with feedback badges marking positive and negative sessions](session_mark_with_feedback.png)

### Dashboard Feedback Charts

The Overview dashboard includes a feedback distribution chart showing the ratio of positive to negative feedback over time. Use this to track sentiment trends and measure the impact of improvements.

![Feedback distribution chart on the Overview dashboard](feedback_chart.png)

## Best Practices

1. **Always emit feedback inside a session.** Feedback events are most useful when they're linked to a session, so Brizz can correlate them with the traces and LLM calls that produced the response.
2. **Include a category attribute.** Adding a `category` (e.g., `accuracy`, `helpfulness`, `speed`) lets you break down feedback by dimension later.
3. **Use the body for free-text comments.** Put user comments in the `body` parameter so they're stored but don't clutter your filterable attributes.
4. **Map early.** Configure your event mappings as soon as you start emitting feedback events — mappings only apply to new incoming data.

## See also

- [Record feedback](/docs/instrument/record-feedback.md) — the `record_feedback` / `recordFeedback` helper. It emits a `brizz.feedback` event but does *not* feed the badges, filters, or chart described here — map a system event on this page for that.
- [Custom events](/docs/instrument/custom-events.md) — naming conventions and the attributes/body distinction.
- [Sessions](/docs/instrument/sessions.md) — make sure feedback events are attached to a session.
- [Issues](/docs/platform/issues.md) — sustained negative feedback surfaces here as a quality issue.


---

# Record feedback

Capture an end-user reaction to a specific reply — a thumbs-up/down, a rating, a reason — as a structured brizz.feedback event. Note: it emits telemetry only, and does not yet drive the session feedback badges, Positive/Negative filters, or the Overview feedback chart — those need the system-event mapping on the User feedback page.

`record_feedback` / `recordFeedback` captures an end-user's reaction to a specific reply — a 👍/👎, a numeric rating, a reason, a comment — as a structured `brizz.feedback` event. Because it's anchored by a [message id](/docs/instrument/message-ids.md) and/or a session id, the reaction can arrive out-of-band: minutes or days after the reply, even once the original trace has closed.

:::info
**Emitting vs. surfacing.** `record_feedback` / `recordFeedback` emits a structured `brizz.feedback` event into your telemetry — that is what it does today. It is **not** yet wired into the session feedback badges, the Positive/Negative session filters, or the Overview feedback chart. Those dashboard surfaces are driven by the system-event mapping flow on the [User feedback](/docs/instrument/user-feedback.md) page and don't consume `brizz.feedback` automatically.
:::

## Record feedback

Pair it with the message id you set on the turn. With a message in context, feedback defaults to that message.

:::tabs
:::tab[Python]

```python
from brizz import record_feedback, set_message_id, start_session

with start_session("session-123"):
    set_message_id(message.id)  # the id you'll reference this reply by

    reply = agent.run(prompt)
    record_feedback("thumbs_up")  # defaults to the current message
```

:::tab[Node.js]

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

await startSession('session-123', async () => {
  setMessageId(message.id); // the id you'll reference this reply by

  const reply = await agent.run(prompt);
  recordFeedback('thumbs_up'); // defaults to the current message
});
```

:::

## Out-of-band feedback

Feedback often arrives after the turn — a user clicks 👎 an hour later. Pass the `message_id` and/or `session_id` explicitly to attach it, from anywhere; no session scope needed.

:::tabs
:::tab[Python]

```python
from brizz import record_feedback

record_feedback("thumbs_down", message_id=message.id, session_id=session_id, reason="inaccurate")
```

:::tab[Node.js]

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

recordFeedback('thumbs_down', { messageId: message.id, sessionId, reason: 'inaccurate' });
```

:::

## Fields

- **`type`** *(required)* — canonical reaction, e.g. `"thumbs_up"` / `"thumbs_down"`. Emitted as `brizz.feedback.type`.
- **`score`** *(optional)* — numeric rating, e.g. 1–5. Emitted as `brizz.feedback.score`.
- **`reason`** *(optional)* — a category, e.g. `"inaccurate"` / `"unhelpful"` / `"unsafe"`.
- **`comment`** *(optional)* — free-text comment, masked like normal telemetry.
- **`source`** *(optional)* — origin, e.g. `"user"` / `"system"` / `"evaluator"`.
- **`message_id` / `messageId`** *(optional)* — target message. Defaults to the message in context; an explicit value wins.
- **`session_id` / `sessionId`** *(optional)* — target conversation. Defaults to the active session; an explicit value wins.
- **`attributes`** *(optional)* — arbitrary categorical facts, each emitted as `brizz.feedback.<key>`. Named fields above win on key collision.

If neither a message nor a session can be resolved, the call still emits but logs a warning — the feedback can't be attributed to a message or conversation.

## See also

- [Message IDs](/docs/instrument/message-ids.md) — tag the reply feedback attaches to.
- [User feedback](/docs/instrument/user-feedback.md) — the event-mapping flow that powers session badges, filters, and the Overview feedback chart today.


---

# Record metrics

Report a numeric measurement your own system produced about an interaction — an eval score, a rating, a latency — as a first-class metric.

`record_metric` / `recordMetric` reports a number your own system already computes about an interaction — an eval score, a customer rating, a latency, a cost — as a first-class Brizz metric. Brizz records it against the session so you can track it over time and slice it alongside everything else it knows about that conversation.

It becomes a real metric, not an event: the value shows up in the session's **Metrics** panel and as a numeric filter on the Sessions page ("every session my judge scored below 0.6"). It does **not** clutter your conversation transcript or your Events page.

It's the typed replacement for hand-rolling this with [custom events](/docs/instrument/custom-events.md): you get a fixed shape (name, value, unit, scale, polarity) instead of a free-form attribute bag, so Brizz can chart and compare the metric without being told how to read it.

Because the metric is anchored by a session id — and optionally carries its own timestamp — it can arrive out-of-band: minutes or days after the turn it describes, even once the original trace has closed.

## Record a metric

Inside a session, the metric attaches to that session automatically.

:::tabs
:::tab[Python]

```python
from brizz import record_metric, start_session

with start_session("session-123"):
    reply = agent.run(prompt)

    score = my_evaluator.score(prompt, reply)
    record_metric("quality_score", score, unit="score", min_value=0, max_value=1, polarity="positive")
```

:::tab[Node.js]

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

await startSession('session-123', async () => {
  const reply = await agent.run(prompt);

  const score = await myEvaluator.score(prompt, reply);
  recordMetric({
    name: 'quality_score',
    value: score,
    unit: 'score',
    minValue: 0,
    maxValue: 1,
    polarity: 'positive',
  });
});
```

:::

`polarity` tells Brizz which direction is good — whether a rising `quality_score` is an improvement (`"positive"`) or a rising `hallucination_rate` is a regression (`"negative"`). `min_value` / `max_value` describe the scale, so a 4 on a 1–5 rating isn't read as a 4 on a 0–1 score.

### Describe a metric once

`unit`, `polarity`, `min_value` and `max_value` describe the *metric*, not one report of it — so Brizz remembers them per metric name. Declare them on any call, and later bare reports inherit them:

```python
record_metric("quality_score", 0.85, unit="score", polarity="positive", min_value=0, max_value=1)
record_metric("quality_score", 0.42)  # same scale and polarity — still renders correctly
```

A descriptor you pass explicitly always wins, so you can re-scale a metric later without an earlier declaration overriding it.

Brizz never *guesses* these. A metric never described anywhere renders as a plain number rather than a gauge — assuming a scale, or which direction is good, would be worse than admitting we don't know. (A `hallucination_rate` of 0.9 assumed to be `positive` would show up bright green.)

## Offline and batch evaluation

Scoring often happens after the fact — a nightly eval job, a human reviewer the next morning. Pass `session_id` to attach the metric from anywhere (no session scope needed), and `timestamp` to say *when the measured thing happened*, so the metric lands on the turn it describes rather than on the evaluation run.

:::tabs
:::tab[Python]

```python
from brizz import record_metric

for session in sessions_to_grade:
    record_metric(
        "quality_score",
        judge.score(session),
        session_id=session.id,
        timestamp=session.ended_at,
        comment="graded by the nightly LLM judge",
        attributes={"evaluator": "gpt-4o", "rubric": "v2"},
    )
```

:::tab[Node.js]

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

for (const session of sessionsToGrade) {
  recordMetric({
    name: 'quality_score',
    value: await judge.score(session),
    sessionId: session.id,
    timestamp: session.endedAt,
    comment: 'graded by the nightly LLM judge',
    attributes: { evaluator: 'gpt-4o', rubric: 'v2' },
  });
}
```

:::

`attributes` are flat labels you can slice the metric by — which evaluator produced it, which rubric version, which variant the user saw.

## Fields

- **`name`** *(required)* — the metric identifier, e.g. `"quality_score"`. Emitted as `brizz.internal.metric.name`.
- **`value`** *(required)* — the measurement. A finite number, or a boolean (recorded as `1` / `0` — handy for pass/fail checks).
- **`unit`** *(optional)* — e.g. `"score"` / `"ms"` / `"usd"`.
- **`comment`** *(optional)* — free text, e.g. the evaluator's rationale.
- **`attributes`** *(optional)* — flat labels, each emitted as `brizz.internal.metric.attribute.<key>`.
- **`polarity`** *(optional)* — `"positive"` if a higher value is better, `"negative"` if a higher value is worse.
- **`min_value` / `minValue`**, **`max_value` / `maxValue`** *(optional)* — the metric's scale.
- **`timestamp`** *(optional)* — when the measured thing happened. Defaults to now.
- **`session_id` / `sessionId`** *(optional)* — target conversation. Defaults to the active session; an explicit value wins.

When called inside a [message](/docs/instrument/message-ids.md) scope, the metric also carries that message id, so it can be attributed to a single turn.

## Validation

Telemetry must never break your code, so bad input is warned about and dropped rather than raised:

- **`name`** is lowercased, then must match `^[a-z][a-z0-9_.-]{0,63}$`. An invalid name emits nothing.
- **`value`** must be a finite number. `NaN`, infinity, and non-numeric values emit nothing.
- **`comment`** is truncated at 4096 characters.
- **`attributes`** are capped at 20 keys. Keys are lowercased and then validated like `name` (so `Complexity` becomes `complexity`; a key that's still invalid, like `has spaces`, is dropped); values are truncated at 256 characters.
- **`polarity`** accepts only `"positive"` / `"negative"`. Anything else is dropped, and the metric still emits without it.
- **A session id is required for the metric to be recorded.** If none can be resolved — no `session_id` argument and no surrounding session scope — the SDK logs a warning and Brizz discards the metric, because there's no conversation to attach it to.

## Known limitation

The `comment` field and any `attributes` values are stored **unmasked** on the metric record — the default log masking rules don't reach them. Don't put personal data in either.

## See also

- [External metrics](/docs/platform/external-metrics.md) — where your metrics show up in the product, and how to slice by them.
- [Custom events](/docs/instrument/custom-events.md) — the free-form escape hatch for anything that isn't a metric.
- [Record feedback](/docs/instrument/record-feedback.md) — capture an end-user's reaction to a reply.
- [Sessions](/docs/instrument/sessions.md) — the conversation a metric attaches to.
- [PII & privacy](/docs/instrument/pii-and-privacy.md) — masking configuration.


---

# PII & Privacy

Protect sensitive data with masking and safe integration patterns.

Brizz is designed for production telemetry, where data privacy is paramount. We provide **privacy-by-default** patterns to ensure sensitive data is handled correctly.

## Golden Rules

1. **Server-Side Only**: Never use your Brizz API key in client-side code (browsers, mobile apps). Always send telemetry from a trusted server environment.
2. **Mask Sensitive Data**: Use our built-in masking to redact PII (Personally Identifiable Information) and secrets before they leave your infrastructure.
3. **Least Privilege**: Rotate API keys regularly and restrict access to your Brizz organization.

## Built-in Masking

Both SDKs include a powerful masking engine that can automatically detect and redact common sensitive data patterns.

:::tabs
:::tab[Python]
Enable default masking rules:

```python
import os
from brizz import Brizz

Brizz.initialize(
    api_key=os.environ.get("BRIZZ_API_KEY"),
    app_name="my-ai-app",
    masking=True,  # Enables default PII patterns (email, phone, SSN, etc.)
)
```

Configure custom masking rules:

```python
from brizz import Brizz, MaskingConfig, SpanMaskingConfig, AttributesMaskingRule

Brizz.initialize(
    api_key=os.environ.get("BRIZZ_API_KEY"),
    masking=MaskingConfig(
        span_masking=SpanMaskingConfig(
            rules=[
                AttributesMaskingRule(
                    attribute_pattern=r"gen_ai\.(prompt|completion)",
                    mode="partial",
                    patterns=[r"sk-[a-zA-Z0-9]{32,}"], # Mask API keys
                ),
            ],
        ),
    ),
)
```
:::tab[Node.js]
```typescript
import { Brizz } from '@brizz/sdk';

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY,
  appName: 'my-ai-app',
  masking: {
    spanMasking: {
      rules: [
        {
          attributePattern: 'gen_ai\\.(prompt|completion)',
          mode: 'partial',
          patterns: ['sk-[a-zA-Z0-9]{32,}'], // Mask API keys
        },
      ],
    },
  },
});
```
:::

## What Should You Mask?

Common candidates for masking include:

- **API Keys & Secrets**: OpenAI keys, database credentials, etc.
- **PII**: Email addresses, phone numbers, social security numbers.
- **Customer Identifiers**: Internal IDs that shouldn't be exposed.
- **Raw Content**: If your compliance policy requires it, you may need to mask the raw prompts and completions.

:::info
Masking strategy depends on your product. Some teams mask prompts/completions entirely; others keep them for debugging but mask specific entities.
:::

## See also

- [Python SDK reference](/docs/sdks/python.md) and [Node.js / TypeScript SDK reference](/docs/sdks/typescript.md) — full masking configuration options.
- [API keys](/docs/admin/api-keys.md) — rotate keys and follow least-privilege.
- [Issues](/docs/platform/issues.md) — repeated PII detections roll up here as content issues.
- [Mute messages](/docs/instrument/mute.md) — drop a call's content entirely instead of masking parts of a turn, including tool arguments and results on their own.


---

# Mute messages

Keep internal or unrelated LLM calls out of the captured conversation, so Brizz shows exactly what your users saw.

Brizz auto-captures your agent's LLM calls and renders them as the conversation your user had. But not every call belongs in that conversation — agents make **internal or unrelated** calls the user never sees: session summarization, title generation, classification or routing, guardrail checks. Captured as-is, those show up as extra turns and drift from what the user actually experienced.

Muting tells Brizz to leave a call's content out of the conversation, so what you see matches what the user saw. The session, its spans, latency, and token/cost are still recorded — only the **content** of the muted call is dropped: the user prompt, the assistant reply, and the tool calls, each of which you can control separately. It also works for sensitive content you'd rather not store, but its main job is keeping the captured conversation faithful to the real one.

:::info
**Telemetry stays on.** Muting only removes conversation content items — the session and its spans are still recorded, so session counts, latency, and cost are unaffected. Errors, custom events, and system messages are never dropped.
:::

## Mute a block

Wrap the call you want left out of the conversation. Every span created inside the block is marked, and the backend drops the matching conversation items. By default everything is dropped — the user prompt, the assistant reply, and any tool calls.

:::tabs
:::tab[Python]
```python
import brizz

# A session-summarization call the user never sees — keep it out of the conversation.
with brizz.mute():
    summary = agent.run("Summarize this conversation for internal logging.")
```
:::tab[Node.js]
```typescript
import { callWithMute } from '@brizz/sdk';

// A session-summarization call the user never sees — keep it out of the conversation.
await callWithMute({}, () => agent.run('Summarize this conversation for internal logging.'));
```
:::

## Mute one side

By default everything is muted. Disable `input` to keep the user prompt visible, or `output` to keep the assistant reply.

:::tabs
:::tab[Python]
```python
import brizz

# Mute just the input — keep the assistant reply, drop the prompt.
with brizz.mute(output=False):
    reply = agent.run("…a long internal prompt the user never wrote…")

# Mute just the output — keep the user prompt, drop the reply.
with brizz.mute(input=False):
    reply = agent.run("the question the user asked")
```
:::tab[Node.js]
```typescript
import { callWithMute } from '@brizz/sdk';

// Mute just the input — keep the assistant reply, drop the prompt.
await callWithMute({ output: false }, () => agent.run('…a long internal prompt the user never wrote…'));

// Mute just the output — keep the user prompt, drop the reply.
await callWithMute({ input: false }, () => agent.run('the question the user asked'));
```
:::

## Mute tool calls separately

Tool arguments and results often carry the sensitive payload — CRM records, file contents, internal API responses — while the user and assistant turns are exactly what you want analytics on. `tools` controls that side on its own. It follows `output` unless you set it.

:::tabs
:::tab[Python]
```python
import brizz

# Keep the conversation, drop every tool call and result.
with brizz.mute(input=False, output=False, tools=True):
    reply = agent.run("Look up this customer's account.")

# The inverse — hide the conversation, keep tool telemetry for debugging.
with brizz.mute(tools=False):
    reply = agent.run("…a prompt you'd rather not store…")
```
:::tab[Node.js]
```typescript
import { callWithMute } from '@brizz/sdk';

// Keep the conversation, drop every tool call and result.
await callWithMute({ input: false, output: false, tools: true }, () =>
  agent.run("Look up this customer's account."),
);

// The inverse — hide the conversation, keep tool telemetry for debugging.
await callWithMute({ tools: false }, () => agent.run('…a prompt you’d rather not store…'));
```
:::

:::warning
**Tool inputs can echo a muted prompt.** Keeping tool calls while muting `input` (for example `mute(tools=False)`) drops the user's turn but keeps tool arguments — and agents frequently pass the user's wording straight through to a tool. If you are muting for privacy rather than for a faithful conversation, mute the tools too.
:::

:::info
**One turn goes with `output` regardless.** When a single assistant turn both replies and calls a tool, Brizz treats it as assistant text, so muting `output` drops it even with `tools` kept. You'll notice it viewing a session with **Show tools** on — that turn's tool call won't be there. Turns where the tool call arrives on its own are unaffected.
:::

## Reuse and async

In Python, use `amute` for `async with` blocks. In Node.js, pre-wrap a function with `withMute` to reuse the same muting (and bind `this`).

:::tabs
:::tab[Python]
```python
import brizz

async with brizz.amute():
    summary = await agent.arun("Summarize this conversation for internal logging.")
```
:::tab[Node.js]
```typescript
import { withMute } from '@brizz/sdk';

const muteTitle = withMute({}, agent.run, agent);
await muteTitle('Generate a short title for this conversation.');
```
:::

## What gets dropped

- **`input`** — the user side: user prompt turns.
- **`output`** — the assistant side: assistant reply text.
- **`tools`** — tool calls and their results. Follows `output` unless you set it. One exception: a combined assistant-message-with-tool-call turn counts as assistant text, so muting `output` drops it even with `tools` kept. If you view a session with **Show tools** on, that turn is the one you won't see.
- Everything else — errors, custom events, system messages — is always kept.
- The session, spans, latency, and token/cost metrics are unaffected; only conversation content items are removed.

Muting applies to the spans created **inside** the block, so place the call around the LLM or agent work whose content you want hidden. It also applies at ingest time only — muting a block affects data sent from that point on, not conversations already captured.

## See also

- [PII & privacy](/docs/instrument/pii-and-privacy.md) — mask or redact fields when you want to keep the turn but hide parts of it.
- [Sessions](/docs/instrument/sessions.md) — the session a muted block belongs to is still captured.


---

# Where to start in the dashboard

A short tour of the Brizz dashboard, in the order most teams find useful.

You've shipped data into Brizz. Here's a recommended order to walk the dashboard for the first time — each step builds on the previous one.

## 1. Sessions

Start here. Open any session, read the conversation, then expand the timeline. This is the unit you'll spend most of your time in — every other page is a different *aggregation* of the same data.

→ [Sessions](/docs/platform/sessions.md)

## 2. Issues

Triage what's broken. Issues groups technical failures, behavioral problems, and capability gaps into one row each, ranked by priority — Critical and High first.

→ [Issues](/docs/platform/issues.md)

## 3. User intents

Understand *why* people are talking to your agent. Brizz clusters thousands of prompts into a small set of intent categories — your top 10 by volume cover most of your traffic.

→ [User intents](/docs/platform/user-intents.md)

## 4. User journeys

Zoom out to flow patterns. Where do conversations succeed? Where do they drop off? Journeys aggregate sessions into the common paths users follow.

→ [User journeys](/docs/platform/user-journeys.md)

## See also

- [Core concepts](/docs/introduction/concepts.md) — definitions for trace, span, session, event, user, issue, intent, journey.
- [Sessions (instrumentation)](/docs/instrument/sessions.md) — the SDK side of what shows up on the Sessions page.
- [Glossary](/docs/help/glossary.md) — every term in one place.


---

# Sessions

Read the sessions you've captured — timeline, filters, replays, and conversation context.

This page is about reading sessions *in the dashboard*. For how to capture and enrich sessions from your code, see [Sessions (instrumentation)](/docs/instrument/sessions.md).

The **Sessions** page is where most analysis starts. Every session is one user journey through your agent — a conversation with all its LLM calls, tool calls, events, and feedback in one place.

## What it is

A **session** is a logical conversation thread, identified by a session ID you control. Inside one session you see:

- **Messages** — every user turn and agent reply.
- **Tool calls** — function calls the agent made (with arguments and results).
- **LLM spans** — each model call with its prompt, completion, token usage, and latency.
- **Events** — custom events you emitted from your code.
- **Feedback** — thumbs-up/thumbs-down signals captured for individual responses.

## How it's populated

Sessions are built from spans the SDK emits with a `brizz.session.id`. The session ID is whatever you set in `start_session` / `withSessionId` — usually a conversation ID from your database. See [Sessions (instrumentation)](/docs/instrument/sessions.md) for the SDK calls.

## How to read it in the dashboard

The Sessions page has two halves:

- **Session list (left).** Sortable by start time, session id, duration, or total tokens. Filter chips at the top apply common views.
- **Session detail (right).** Click any session to open the timeline, conversation, and metadata panes.

### Filter chips

- **Negative Sessions** / **Positive Sessions** — only sessions containing matching feedback events. These appear once the corresponding system-event mappings exist; see [User feedback](/docs/instrument/user-feedback.md).
- **Low User Satisfaction** — sessions Brizz scored as negatively received.

### Search and filter

Filter sessions by `user_id`, `service_name`, custom session properties, time range, or free-text search across conversation content. Combine filters; combinations URL-encode so you can share a link.

If you report your own scores with [`record_metric`](/docs/instrument/record-metric.md), each one becomes a numeric filter here too — "show me every session my judge scored below 0.6". See [External metrics](/docs/platform/external-metrics.md).

If your agent is built out of named skills, a **Skill** filter narrows to the sessions that activated a given skill — useful for asking whether one skill is behind a spike in problems. The same filter exists on [Issues](/docs/platform/issues.md). It only offers values once Brizz has seen skill activity for the service.

### Export conversations

The **Export conversations** action exports the sessions matching your current filters as Markdown — a fast way to hand a set of real conversations to a teammate or to an AI coding agent.

- **Format** — one Markdown file per conversation bundled in a ZIP, or every conversation combined into a single Markdown file.
- **Limit** — up to 1000 conversations, taken in the order the list is currently sorted (newest first unless you've changed the sort).
- **Include events**, **Include tool calls**, **Include session metadata** — metadata adds cost, tokens, properties, and labels to each file. The session title is always included.
- **Compact long fields** — truncates long messages, tool inputs and results, errors, and event bodies over 4000 characters so the files stay readable. On by default.

The export honours the filters you already have applied, so narrow the list first and export second.

### Session detail

- **Conversation pane** — the human-readable transcript: user → agent turns, tool calls inlined.
- **Timeline pane** — the session's activity as a graph of turns, tool calls, and agent steps rather than a row per raw span. Hover a row for its timing; activate it to expand or navigate into the detail.
- **Metadata pane** — the session properties you set (user, plan, feature, etc.) plus inferred attributes.
- **Metrics panel** — the metrics scored for this session, both Brizz's own and any [external metrics](/docs/platform/external-metrics.md) you reported. Click one for its gauge, comment, and attributes.
- **Replay badge** — if you've configured a session-replay provider (FullStory, Mixpanel Session Replay, LogRocket), a one-click deep link to the recording appears here.
- **External link badges** — any links your code attached with `add_external_link` / `addExternalLink` (a Datadog trace, Sentry issue, dashboard, …) show up here as one-click badges. See [External links](/docs/instrument/external-links.md).

## How to act on it

1. **Use the conversation pane first, the timeline second.** The transcript tells you what happened; the timeline tells you *why* — open it only when the transcript looks wrong.
2. **Filter by feedback to find the worst sessions.** Negative-feedback sessions are the highest-signal training set you have.
3. **Save filters you use repeatedly.** "Last 24h, errors, service = checkout-agent" should be one click, not three.
4. **Cross-reference with Issues.** When a session belongs to an open issue, the issue badge appears on it — useful when triaging a single complaint to the broader pattern.
5. **Share links liberally.** Session URLs are stable; paste them in PR descriptions, Slack threads, and bug reports.

## See also

- [Sessions (instrumentation)](/docs/instrument/sessions.md) — how to capture and enrich sessions from your code.
- [User feedback](/docs/instrument/user-feedback.md) — what makes the negative/positive filter chips light up.
- [Issues](/docs/platform/issues.md) — the rollup of repeated problems across sessions.
- [Session review](/docs/platform/session-review.md) — queue the sessions a human should read, by rule.
- [Users](/docs/platform/users.md) — per-user rollups of the people behind these sessions.
- [External metrics](/docs/platform/external-metrics.md) — filter and slice sessions by scores your own system produced.
- [AI assistant](/docs/platform/ai-assistant.md) — ask about these sessions in natural language.
- [Custom dashboards](/docs/platform/custom-dashboards.md) — build your own boards of [custom charts](/docs/platform/custom-charts.md) that aggregate the session data on this page.


---

# External metrics

Bring your own scores into Brizz — eval results, ratings, latencies — and filter and slice sessions by them.

**External metrics** are numbers *your* system produces about an interaction: an LLM judge's score, a customer's star rating, a latency budget, a cost. You report them with [`record_metric`](/docs/instrument/record-metric.md), and Brizz treats them as first-class metrics alongside the ones it computes itself.

## What it is

Most teams already grade their own agent. An eval harness scores every answer for quality and groundedness; a human reviewer rates a sample each morning; a nightly job re-grades yesterday's conversations with a stronger judge. Those numbers usually live somewhere else — a spreadsheet, a warehouse table, a dashboard nobody opens next to the conversation that produced them.

An external metric puts that number on the session. Once it's there you can ask the question that actually matters: *show me every conversation my judge scored below 0.6* — and then read those conversations.

## Where they show up

**Session Details → Metrics.** Every metric reported for a session is badged in the details panel, next to the ones Brizz computes. `Answer Quality 0.34`, `Hallucination Risk 0`.

**The metric drawer.** Click a badge to open it. You get a gauge showing where the value sits on its scale, the `comment` you attached (an evaluator's rationale, a reviewer's note), and the metric's attributes as chips — which judge produced it, which rubric version, which variant the user saw.

**Sessions → filters.** Each metric you've reported becomes a numeric filter on the Sessions page, offering `>`, `≥`, `<`, `≤`, `=`, `≠`, and `exists`. Brizz builds the filter list per service from the metrics it has actually seen, so a metric appears in the dropdown as soon as you start reporting it — there's nothing to configure.

Because you tell Brizz the metric's scale (`min_value` / `max_value`) and its **polarity** — whether a rising value is good or bad — the gauges colour correctly without further instruction. A high `answer_quality` reads as healthy; a high `hallucination_risk` reads as a problem.

External metrics do **not** appear in the conversation transcript or on the Events page. They're measurements *about* a conversation, not things that happened *in* it.

## Re-scoring and the latest value

Reporting the same metric for a session again supersedes the previous value — the newest report wins. That's what makes offline evaluation work: a nightly job can re-grade a conversation with a better judge, and the session's score updates to reflect the correction rather than keeping the original.

Pass a `timestamp` when you re-score to record *when the thing you measured happened*, as opposed to when you got around to measuring it. Brizz still resolves "which value is current" by when the report arrived, so a backdated re-score correctly supersedes the original instead of being buried under it.

## Reporting them

See [Record metrics](/docs/instrument/record-metric.md) for the SDK reference — the fields, the validation rules, and how to attach a metric to a session out-of-band.

A few things worth knowing:

- **Values are numbers.** Categorical facts about the measurement go in `attributes` (which evaluator, which rubric), not in the value.
- **Names Brizz uses or has used are reserved.** `faithfulness`, `session_outcome`, and other Brizz-managed metric names can't be overwritten — pick your own name.
- **A metric needs a session.** It's anchored to a conversation; without one there's nothing to attach it to.

## Related

- [Record metrics](/docs/instrument/record-metric.md) — the SDK API.
- [Sessions](/docs/platform/sessions.md) — where you filter by them.
- [Custom events](/docs/instrument/custom-events.md) — for things that *happened*, rather than things you measured.


---

# Issues

Deduplicated problems detected across your agent's traffic — technical failures, capability gaps, behavioral problems — each with a status (open/tracked/ignored/resolved) and a priority (critical/high/medium/low).

The **Issues** page is the single place to triage what's going wrong with your agent. It groups similar problems — technical failures, requests the agent couldn't fulfil, repeated tool failures, behavioral gaps — into one row each, so you can see *what's broken and how often* without drowning in logs.

## What it is

An **issue** is a deduplicated problem. Brizz detects individual occurrences across your traffic — one technical error, one request the agent couldn't fulfil, one anomalous tool latency — and groups the ones describing the same underlying problem into a single issue, tracking frequency, first/last seen, affected users, and the sessions behind it.

Every issue carries an **issue type** — the taxonomy Brizz uses to describe what kind of problem it is:

- **Missing Capability** — the user asked for something the agent cannot do yet.
- **Missing Data** — the agent claims it lacks knowledge or data it should have been given.
- **Broken Capability** — the capability exists and the tool was called, but it failed technically and the agent declared it couldn't finish.
- **Technical Failure** — errors raised by the agent or its dependencies: error spans, HTTP 5xx, exceptions.
- **Behavioral** — the agent answers, but the way it behaves doesn't serve the user.
- **Performance** — latency and token-volume anomalies against the service's own baseline.
- **Security/Safety** — abusive or out-of-policy interactions.
- **Improvement** — an opportunity rather than a defect.

## How it's computed

Two stages:

1. **Detection.** Analyzers run over your telemetry in different ways: **rule-based** checks cover technical errors (OpenTelemetry error status, HTTP 5xx, exception attributes, failed tool calls) and deprecated model usage; **statistical** checks flag token and latency anomalies against the service's own 30-day baseline; and **embedding or model-based** checks cover what needs reading the conversation — missing capabilities, repeated near-identical tool calls, abuse.
2. **Clustering.** New detections are routed by error type, matched against issues that already exist, and grouped into a new issue when nothing matches. A pass over the generated issue *titles* then merges duplicates, which is what keeps near-identical issues from fanning out.

An issue's **mechanism** records which path produced it — `Automatic` for the clustering pipeline above, `Manual` for issues someone created by hand. It's available as a filter.

## How to read it in the dashboard

The Issues page is filterable at the top:

- **Status** — `Open`, `Tracked`, `Ignored`, `Resolved`. Adding any external link — a Jira, Linear, or Monday ticket, or a URL via **Add Link** — moves an issue to **Tracked**. It's a workflow state, not a live check: you can set it by hand, and removing the link doesn't move it back. `Open` and `Tracked` are the *active* statuses. Any status can move to any other, except `Open` while a Jira ticket is attached.
- **Priority** — `Critical`, `High`, `Medium`, `Low`. What to work on first, derived from how serious the problem is and how much of your traffic it affects, recalculated over a rolling window. Set it by hand and Brizz stops recalculating it.
- **Issue type** — the taxonomy above. Useful for routing: engineering takes Technical Failure and Broken Capability; product watches Missing Capability and Behavioral.
- **Trend** — `Regressed` (reopened at some point in the last 7 days), `Escalating` (occurrence rate at least doubled in the last 24h versus the prior 7-day average), `New` (first seen under 7 days ago), `Ongoing` (everything else). They're evaluated in that order, so an escalating issue that's also new reads as Escalating.
- **Assignee, external ticket, mechanism, labels** — plus **Tool**, which matches tools the issue's findings are about or were caused by. Journey, user, organization, satisfaction, and custom-property filters match issues by the sessions behind them.
- **Skill** — if your agent is built out of named skills, narrows to issues whose sessions activated a given skill. It only offers values once Brizz has seen skill activity for the service.
- **Search** — free-text match over issue titles and descriptions; title matches rank first.

Click any row to open the detail panel. You'll see:

- **Header** — title, issue type, priority, status, assignee, labels, and any linked external tickets. The title is editable on any issue; the description and issue type only on issues someone created by hand.
- **Sessions** — a five-session preview of the evidence, linked for one-click drill-in. Narrow it to sessions with or without tickets, keep the Issues page's active session filters, or apply a value from **Breakdown**; **View all** opens the full issue-filtered list.
- **Significance** — the affected share of all sessions, users, organizations, and relevant tool executions, followed by the same rate for each journey. The percentage is the raw rate; ordering and colour use a confidence-adjusted version, so a high rate over few sessions sorts lower and reads cooler. Tool executions stay neutral because calls within one session are not independent samples.
- **Breakdown** — where the issue's sessions concentrate, by journey, intent, custom properties, and model. It leads with dimensions whose strongest value is at least twice as common (or at most half as common) as the service baseline, covers a meaningful share of the issue, and has enough evidence to clear the confidence check; quieter dimensions stay behind **Show more**. Open a value's menu to filter the evidence in place, open matching sessions with or without the issue filter, or copy the value.
- **Activity** — the latest 200 status changes, priority overrides, assignments, and **Add Link** additions. Tickets created through the Jira, Linear, or Monday integrations are linked but not recorded here.

### Creating an issue by hand

Spot a problem Brizz didn't flag and you can raise it from the message itself, so the evidence stays anchored to it. **Create issue** takes a title, description, type, priority (defaults to Medium), and optional assignee; **Attach to existing** adds the message to any active hand-created issue for the service, not only your own.

Manual issues take every type except *Improvement* and behave like detected ones for status, assignment, and ticket links. The priority you set is never recalculated.

:::info
Manual issue creation is off by default. If you don't see the option on a message, [contact us](mailto:support@brizz.ai) to have it enabled — it isn't a setting an organization admin can change.
:::

### Issues overview

The aggregate companion to the list — "how bad is it overall, and where is it concentrated?"

- **Sessions with issues over time** — the share of sessions linked to a non-ignored issue. Recent buckets read low while clustering catches up, so don't call a drop a win too early.
- **Issue hotspots** — highest-volume values on one dimension, shaded by count so a low-volume but serious row stands out next to a high-volume routine one. Click a named, non-empty cell to open that slice; **Other** and the unlabelled catch-all aren't clickable.
- **Product-area map** — areas sized by open issues or affected sessions. The session sizing sums per-issue counts, so a session hit by several issues counts more than once.
- **New vs resolved** — currently-Open against currently-Resolved issues. Tracked and Ignored are in neither series and each side is capped, so read it as a trend.

## How to act on it

1. **Triage by priority, not by recency.** A Critical from last week beats a Low from this morning. Watch the Escalating trend badge separately — a Medium that just doubled in rate is often the more urgent signal.
2. **Drill into one example before generalizing.** Open the linked session and read the actual conversation — the dashboard summary is only a hint.
3. **For Technical Failure and Broken Capability, look at the code or the call.** Common causes are missing error handling around provider calls or tool execution, or a tool failing on auth, timeouts, or 5xx.
4. **For Missing Capability, Missing Data, and Behavioral, look at the prompt, the data, or the agent design.** These often need a product decision about what the agent should be able to do rather than a stack-trace fix.
5. **Resolve aggressively.** Mark issues `Resolved` as soon as you ship a fix — Brizz reopens the issue if the problem comes back, and flags it as `Regressed` for a week after that. If you'd rather track the fix in your own tracker, create a Jira, Linear, or Monday ticket from the issue; that moves it to `Tracked` and links the two.

## See also

- [Sessions](/docs/platform/sessions.md) — drill into the actual conversations contributing to an issue.
- [User feedback](/docs/instrument/user-feedback.md) — map feedback events to power the session feedback badges, filters, and charts. Feedback is session context; it doesn't itself create issues.
- [Events](/docs/platform/events.md) — browse the custom events you emit. Technical-error detection reads error spans and failed tool calls, not custom event payloads.
- [Insights](/docs/platform/insights.md) — AI-generated reads that often point at the issues here.
- [Labels](/docs/platform/labels.md) — the product-area and custom labels that organize issues.
- [Issue trackers](/docs/integrations/issue-trackers.md) — push an issue to Jira, Linear, or Monday.
- [AI assistant](/docs/platform/ai-assistant.md) — "Fix with your agent" starts from an issue.


---

# User intents

Semantic clusters of user prompts — what your users are actually asking for, grouped automatically.

The **User Intents** page tells you *why* people are talking to your agent — not the literal prompts, but the underlying goals. Brizz groups thousands of unique phrasings into a small set of intent clusters automatically, so you can spot what's popular, what's surging, and what your product doesn't yet support well.

## What it is

A **user intent** is a semantic cluster of similar prompts. "I want to cancel," "Stop my subscription," and "Turn off auto-renew" all map to one **Cancellation** intent. Brizz discovers these clusters from your data — you don't define them upfront, and there's no manual tagging.

Intents are organized hierarchically: broad parent intents break down into more specific child intents, so you can navigate from "Account management" → "Cancellation" → "Cancel due to price."

## How it's computed

The analytics pipeline runs intent clustering on the user-side messages of every session:

1. **Embedding** — each user prompt is embedded into a vector.
2. **Clustering** — vectors are grouped by similarity; small clusters merge into broader ones, large clusters split into specific children.
3. **Labeling** — Brizz uses an LLM to write a human-readable label for each cluster from its representative prompts.
4. **Volume tracking** — clusters are sorted and re-ranked over time as new sessions arrive.

The clustering job runs as a background workflow; intents typically appear within a few hours of new traffic.

## How to read it in the dashboard

Two views, same data:

- **Tree view** — the hierarchy. Start broad, click into child clusters to drill down. Best for exploring what categories of intent exist.
- **List view** — flat ranking by volume. Best for "what are the top 20 things users want right now."

Each cluster shows:

- **Volume** — how many prompts (and sessions) fall into the cluster over the selected window.
- **Trend** — week-over-week or day-over-day change. A surging intent often signals a new use case worth supporting.
- **Examples** — click in to see the actual prompts. The label is a summary; the examples tell you what users really said.

Use the **search** bar to test whether a phrase you're worried about forms its own cluster.

## How to act on it

1. **Prioritize roadmap by volume.** The top 10 intents by volume cover most of your traffic. If a high-volume intent corresponds to a product gap, that's your next feature.
2. **Watch trend, not just volume.** A small but fast-growing intent (e.g., a new integration question) often matters more than a stable popular one.
3. **Improve docs and prompts for confusing intents.** Some intents reveal users *don't know how to ask* — that's a docs/UX fix, not a model fix.
4. **Export representative prompts for fine-tuning or eval.** A clean cluster is a ready-made eval set or training corpus for a focused fine-tune.
5. **Cross-reference with Issues.** If an intent has a matching content issue, users are asking and your agent is failing — high leverage.

## See also

- [User journeys](/docs/platform/user-journeys.md) — what people *do* with your agent, vs. what they *ask for*.
- [Issues](/docs/platform/issues.md) — quality problems on the responses to those intents.
- [Identify users](/docs/instrument/identify-users.md) — intents become more useful when you can segment by user cohort.


---

# Traces

A paginated, low-level list of per-trace summaries with duration, cost, and models — for looking up a specific trace.

The **Traces** page lists per-trace summaries for a service. It's the lowest-level view Brizz offers and exists mainly for one job: you have a trace id from a log line or an error report, and you want the trace behind it.

:::info
Traces is off by default. If **Traces** isn't in your navigation, [contact us](mailto:support@brizz.ai) — it isn't a setting an organization admin can change. Most teams don't need it: [Sessions](/docs/platform/sessions.md) is the view built for reading agent behavior.
:::

## What it is

One row per trace. Each aggregates a trace's spans into a summary; what it doesn't do is group traces into a [session](/docs/introduction/concepts.md#session), which is what the Sessions page is for.

## How it's populated

From ingested telemetry, summarized per trace, with no quality-analysis step in between.

## How to read it in the dashboard

Pick a service, then use the date range and pagination. Each row shows timestamp, cumulative span duration (summed across spans, so parallel work can exceed elapsed wall-clock time), cost, and models used. Open a row for its span tree, which loads up to 5,000 spans.

:::warning
The search box filters only the rows already loaded on the current page — it does not query the backend. A trace that exists on another page reads as missing, so page through or narrow the date range instead of trusting an empty search.
:::

## How to act on it

1. **Come here with an id, not a question.** For exploring, [Sessions](/docs/platform/sessions.md) is faster — conversation, timeline, and metrics in one place.
2. **Rule out the page before blaming ingestion.** Search is page-local, so check the service, date range, and later pages first. Only then is a missing trace evidence of an ingestion problem — see [Troubleshooting](/docs/help/troubleshooting.md).

## Availability

Off by default, and enabling it is handled by Brizz rather than by an organization admin. The setting removes Traces from the navigation; it doesn't block the URL, so a direct link can still render for someone who has access.

## See also

- [Sessions](/docs/platform/sessions.md) — the conversation-level view, and the one to reach for by default.
- [Core concepts](/docs/introduction/concepts.md) — how traces, spans, and sessions relate.
- [Troubleshooting](/docs/help/troubleshooting.md) — when a trace you expected never arrives.


---

# User journeys

Common paths users take through your agent — aggregated flow patterns, not single sessions.

The **User Journeys** page zooms out from individual sessions to show the *common paths* users take through your agent. It answers product questions: what's the typical flow? Where do people drop off? Which sequences end well, and which don't?

## What it is

A **journey** is an aggregated pattern — a sequence of user actions, agent responses, or intent transitions — that many sessions follow. Sessions show you one conversation; journeys show you the shape of all conversations.

Journeys are useful when you want to talk about behavior at the population level: "30% of new users start with intent X, and half of them never reach intent Y."

## How it's computed

Journey analysis is heavy, so it runs as a background workflow over a chosen time window (24h, 7d, 30d). The pipeline:

1. **Builds a sequence per session** — the ordered list of user turns, agent turns, or matched intents (depending on the journey type).
2. **Aligns and clusters** — sequences that share subsequences are grouped using sequence alignment + clustering.
3. **Summarizes each pattern** — Brizz writes a human-readable label and counts how many sessions follow it.

Because the job is expensive, journeys don't update in real time — kick off a compute when you want a fresh view.

## How to read it in the dashboard

The Journeys page is organized as a list of journey cards, ranked by the number of sessions that follow each pattern:

- **Steps** — the ordered sequence of intent labels or actions in this journey.
- **Volume** — how many sessions matched.
- **Outcome distribution** — for journeys where outcomes are defined (success/failure/dropoff), the split across them.
- **Sample sessions** — open a sample to see the actual conversation behind the pattern.

Pick a time range that matches your question. A 7-day window is the right default; 24h is too noisy, 30d smooths out recent regressions.

## How to act on it

1. **Find the "happy path" first.** The highest-volume journey with a successful outcome is your baseline — design and content should be optimized for it.
2. **Look at dropoff points, not endpoints.** Sessions ending mid-journey are a UX problem; sessions reaching the end with a bad outcome are a content problem.
3. **Compare two windows.** A journey that vanished after a deploy means you broke a flow; a new pattern that appeared often signals an unintended use case.
4. **Cross-check with Intents.** A journey that always starts with a specific intent and ends badly tells you exactly which prompt to fix.
5. **Don't optimize for rare paths.** A journey followed by 30 sessions out of 10,000 is a curiosity, not a roadmap item.

## See also

- [User intents](/docs/platform/user-intents.md) — the building blocks each journey is composed of.
- [Sessions](/docs/platform/sessions.md) — drill into the actual conversations behind a journey.
- [Issues](/docs/platform/issues.md) — when a journey reliably ends badly, the matching content/product issue is where to track the fix.


---

# Events

Browse and search the events your agent emitted — custom events and log records, with their body and attributes, and a link to the session where present.

The **Events** page is where you check what your instrumentation actually sent. Everything you emit with [custom events](/docs/instrument/custom-events.md), plus log records your agent produced, lands here.

## What it is

A flat, searchable list of one service's event stream — what you want when you're verifying instrumentation rather than reading a conversation. [Sessions](/docs/platform/sessions.md) shows the same events in conversation context.

## How it's populated

Directly from your telemetry, with no configuration and no analysis step in between. Arrival is asynchronous, so allow a short delay before concluding something is missing.

## How to read it in the dashboard

Pick a service first; the page is empty until you do.

- **Search by event name** — also rescopes both charts. Log records with no event name stay under an empty name and won't match.
- **Stats bar** — total events and *unique* event names. The unique count should be small; a jump usually means a dynamic value got baked into a name.
- **Events over time** — volume per bucket, narrowed to the searched name.
- **Event distribution** — the **top five** names, with percentages over just those five.
- **Event table** — timestamp, name, and session id. Sorting applies to the current page. Expand a row for the parsed body properties and the event's own attributes; a plain-text body has nothing to parse, and resource attributes aren't shown. Session id is optional, so some rows have no session to open.

## How to act on it

1. **Verify new instrumentation here first.** After adding a `record_event` call, search for its name — but check the service, date range, and name filter before concluding it never arrived.
2. **Watch the unique-event count.** Ids or timestamps interpolated into a name show up as runaway cardinality and make the event useless to aggregate on.
3. **Check attributes before building on them.** Confirm names and value shapes, then use the supported ones as [session filters](/docs/platform/sessions.md). Events aren't a [custom charts](/docs/platform/custom-charts.md) data source.

## Availability

On by default. If **Events** isn't in your navigation, [contact us](mailto:support@brizz.ai).

## See also

- [Custom events](/docs/instrument/custom-events.md) — how to emit events, and the naming conventions to follow.
- [Services & configuration](/docs/admin/services-and-configuration.md) — the per-service Events tab, where events map to Brizz system events.
- [Sessions](/docs/platform/sessions.md) — session-linked events in conversation context.


---

# Tools

Per-tool dashboard — call volume, run time, error rate, and the issues, satisfaction, and estimated token volume of the sessions that use each tool.

The **Tools** dashboard answers "which of my agent's tools is the problem?" Pick one tool and every chart rescopes to it.

## What it is

A single-tool view. The selector lists tools observed for the service, ranked by estimated token volume and capped at 300, opening on the heaviest.

## How it's populated

From tool-call spans your SDK or framework emits — no configuration needed, though a tool needs named, completed calls in the selected range to appear.

## How to read it in the dashboard

**Some charts measure the tool; others measure the sessions that used it.** This is the thing to get right here:

- **Call-level** — Call volume, Avg run time, Error rate, and Est. token breakdown count *this tool's own calls*. Error rate is the share of its calls that errored, not of affected sessions.
- **Session-scoped** — Top journeys, Issues, User satisfaction, Task completion, Task outcome, and Cache tokens measure *the sessions that used it*. A high issue count means co-occurrence, not causation.

The four sections:

- **Top issues & journeys** — the highest-*priority* issues in sessions using this tool (not the most strongly associated), plus a treemap of journeys sized by session count.
- **Usage & reliability** — Call volume, Avg run time, Error rate, and Issues over time. The last counts *distinct issues* at the bucket where they were first detected.
- **Quality** — User satisfaction, Task completion, and Task outcome for those sessions.
- **Token volume** — Input (call arguments), Output (the result), and Schema (the definition's size, assigned to each observed call on the assumption it's sent every time), plus cache tokens.

**Total vs per-session** divides by the sessions that used the tool — never your whole traffic. Numerators come from different clocks, so read it as a signal, not a precise ratio.

Global session filters and the time range apply to the charts. Change-event markers are the exception: they ignore session filters, so they line up a jump with a change without proving it caused one.

## How to act on it

1. **Look at schema volume first.** If your provider re-sends the definition each call, a verbose one is a fixed tax on every turn mentioning the tool.
2. **Read error rate against call volume.** The same rate matters far more on your busiest tool than on one called twice a day.
3. **Use per-session to separate growth from regression.** Rising calls *per session* means the agent reaches for it more often within a session — worth checking for a retry loop.
4. **Follow avg run time into latency.** A slower tool is a candidate cause for slower sessions, though parallel work means it won't always show end-to-end.
5. **Open the issues, don't just count them.** The panel is session-scoped, so confirm the tool is implicated first.

## Availability

On by default. If **Tools** isn't in your navigation, [contact us](mailto:support@brizz.ai).

## See also

- [Cost & usage](/docs/platform/cost.md) — actual spend, in money, across models.
- [Issues](/docs/platform/issues.md) — the full triage view for the issues surfaced here.
- [Latency & performance](/docs/platform/latency.md) — where a slow tool shows up end-to-end.


---

# Cost & usage

The Cost dashboard — LLM spend over time, broken down by model, user, organization, and billing component, plus the most expensive sessions and per-tool token volume.

The **Cost** dashboard tracks where your LLM spend goes and catches runaway usage early. For the arithmetic behind each number, see [Cost calculation](/docs/platform/cost-calculation.md).

## How it's populated

Brizz uses the cost the provider reported when there is one. Otherwise it prices the call itself, which needs the model and provider recognized and both rates and token counts available — if any is missing, the call isn't priced. The tool and prompt-size charts measure something different: estimated **token volume**, not money.

## How to read it in the dashboard

- **Cost over time** — spend against session count. Spend rising while sessions stay flat points at a per-session regression. The two use different clocks (spend by call time, sessions by ingestion time), so treat a divergence as a lead, not a measurement.
- **Cost by model** — where a model swap shows up.
- **Cost by user** / **Cost by organization** — top spenders, for attributing cost to a customer or team.
- **Cost by component** — input, cache read, cache write, output, and tools. **Unattributed** fills the gap when those come to less than the total.
- **Cache efficiency** — the share of input tokens served from cache. Cached input bills less, so a low share is worth investigating; the chart shows the current share, not how much of the rest could be cached.
- **Most expensive sessions** — sessions whose *start* is in range, ranked by full cost. A session that began earlier won't appear even if its spend landed inside the window.
- **Tools by token contribution** and **Tool size over time** — estimated tokens per tool, not per-tool spend.
- **System prompt size over time** — average estimated system-prompt tokens per agent. Markers show when the prompt changed; they carry the change, not a release identity.

Session filters and the time range scope the charts — except change-event markers, which follow only the service and date range.

## How to act on it

1. **Look at cache efficiency early.** Moving input into the prompt cache is often cheaper and faster than shortening prompts.
2. **Split growth from regression.** Flat sessions with rising spend points at per-session usage — a longer prompt, a chattier tool, a bigger model. Remember the clock difference: delayed ingestion produces the same shape.
3. **Line up a jump with a change event.** A step in the prompt-size chart often coincides with a step in the bill — a correlation worth chasing.
4. **Check schema tokens on the busiest tools.** If your provider re-sends definitions each call, trimming one pays out on every turn. See [Tools](/docs/platform/tools.md).
5. **Open the expensive sessions.** Outliers are often a retry loop or a runaway agent rather than steady-state cost.

## Availability

On by default. If **Cost** isn't in your navigation, [contact us](mailto:support@brizz.ai). Cost by organization needs [identified organizations](/docs/instrument/identify-organizations.md).

## See also

- [Cost calculation](/docs/platform/cost-calculation.md) — billing classes, rate sources, and how a session's cost is derived.
- [Tools](/docs/platform/tools.md) — per-tool token volume, including estimated schema overhead.
- [Identify users](/docs/instrument/identify-users.md) — what makes the per-user breakdown work.


---

# Cost calculation

How Brizz computes and displays the cost of each session.

## How Brizz computes cost

Brizz computes cost per LLM call. For each call it multiplies the token count for each billing class by that class's per-token rate, then sums across every call in the session:

```
session cost = Σ (tokens_class × rate_class)   for each LLM call in the session
```

The billing classes are:

| Class | What it counts |
|---|---|
| **Input** | Fresh (non-cached) prompt tokens |
| **Cache read** | Prompt tokens served from the provider's prompt cache |
| **Cache write** | Prompt tokens written into the provider's prompt cache |
| **Output** | Completion tokens generated by the model |
| **Tools** | Flat per-call surcharge for tool/function calling, when billed separately |

## Where rates come from

Rates come from [LiteLLM's model price list](https://github.com/BerriAI/litellm), refreshed periodically. For each call Brizz uses the best signal available:

1. A cost the provider returned in its response (most accurate), or
2. A cost computed from the token counts and the LiteLLM rate for that model.

Costs are **estimates** and may differ slightly from the invoice your provider issues.

## Provider and region premiums

The same model can be billed at different rates depending on how it's reached. The clearest example is Amazon Bedrock: its region-pinned profiles (`eu.`, `us.`, `apac.`, …) are priced above the `global.` profile and the base provider rate for the *identical* model.

When a model is reached through a routing or locality prefix and that profile's rate differs from the model's base-family rate, Brizz surfaces the difference as a **premium** (`+X%`) — or, if billed below base, a **discount** (`−X%`). It's computed server-side at billing time against the same pricing snapshot used to bill the call, so it always reconciles with the displayed cost. A model-version price change is *not* treated as a premium — only a routing/locality difference is.

**What Brizz detects today:** Amazon Bedrock region-pinned profiles. For example, `eu.anthropic.claude-sonnet-4-6` shows **+10%**, while the same model via the `global.` profile or the base provider API shows no premium.

**What Brizz does not detect yet:** premiums charged through signals that aren't visible in the model ID — for example Azure data-zone vs. regional deployments, Vertex regional endpoints, or OpenAI/Anthropic service tiers (batch, priority). Those calls are still billed at Brizz's rate for the model, but the *reason* for the difference isn't named.

A premium is computed only for sessions ingested after this feature shipped; older sessions show no premium line.

## Reading the breakdown panel

Open the panel from any cost figure — it's titled **Cost breakdown**. Each row reads left-to-right as **tokens × rate = cost**, where the rate is the per-million-token rate actually billed for that class, ending in a **Total**.

- **"—"** in a Tokens or Rate cell means there's no token-level data for that class (e.g. a flat tool surcharge has no per-token rate).
- **"Breakdown not available"** means the cost came straight from the provider response without a per-class split. The total is still accurate.
- **Single vs. multiple models:** a single-model session shows one breakdown. A multi-model session shows one card per model — model, provider, any premium, and a **Subtotal** — with a bold **Session total** at the bottom.

## Caveats

- Costs shown are **estimates** derived from token counts and rate tables; they may differ from your provider's invoice (rounding, promotional rates, committed-use discounts).
- Cache read and cache write are distinct billing classes at different rates. If your provider doesn't report cache token counts, those rows won't appear.

## See also

- [Cost & usage](/docs/platform/cost.md) — the dashboard that reports the spend computed here.
- [Custom charts](/docs/platform/custom-charts.md) — chart cost over time, by model, or per user with the cost metric described here.
- [Custom dashboards](/docs/platform/custom-dashboards.md) — collect cost charts into a board your team reviews.
- [System performance](/docs/platform/performance.md) — cost tracked alongside session volumes and error rates.


---

# Custom dashboards

Build your own dashboards — named boards of custom charts, scoped to a trace service and shared with your team.

A **custom dashboard** is a board you build yourself: a named collection of [custom charts](/docs/platform/custom-charts.md) laid out on a grid, scoped to one of your trace services. Use it when the built-in pages don't frame your data the way your team needs — a board for cost, one for a specific user journey, one for the metrics you review in your weekly standup.

This page is about *building and arranging* dashboards. For the charts that go on them — how a chart is defined, the chart types, metrics, and Slack digests — see [Custom charts](/docs/platform/custom-charts.md).

## What it is

A dashboard is a saved layout, not a saved snapshot. Each chart on it is a live query (see [Custom charts](/docs/platform/custom-charts.md)), so the whole board re-reads your telemetry every time it opens — there's nothing to refresh manually.

Dashboards have three defining properties:

- **Scoped to a trace service.** A dashboard belongs to one service. Switch services and you see that service's dashboards.
- **Named, and there can be many.** You're not limited to one board per service — create as many as you like and give each a name and an icon so they're easy to tell apart.
- **Shared with your team.** Dashboards are tenant-wide, not private. Everyone with access to the tenant sees the same set of dashboards, and any member can edit them. There are no personal, per-user dashboards.

## How to create a dashboard

Create a new dashboard, and set:

- **A name and icon** — what your team will see in the switcher.
- **A starting point** — either a **template** (a board pre-filled with a themed set of charts) or a **blank** board you fill in yourself.
- **A focus filter** (optional) — a cohort that every chart on the board is narrowed to. See [Focus filter](#focus-filter) below.

A new dashboard opens in **draft** state. Arrange it, add and tweak charts, then **save** — nothing is shared with your team until you do. Discard the draft and the board is never created.

## Templates

A template is a blank dashboard's head start: a themed board that already has a coherent set of charts on it, which you then keep, remove, or edit. Templates are organized around the questions teams most often ask:

- **Tool focus** — how your agent's tools are used and where they fail.
- **Intent focus** — what users are trying to do and how those intents trend.
- **Journey focus** — how conversations flow and where they drop off.
- **Issue focus** — what's breaking and how issue volume moves over time.
- **Blank** — no charts; start from an empty grid.

Templates are a starting layout, not a fixed one. Once created, a dashboard is fully yours to change — the template only decides which charts it opens with.

## Focus filter

A **focus filter** narrows the *entire dashboard* to one cohort — a single tool, intent, journey, issue type, or any filterable dimension. Set it once and every chart on the board answers its question *for that cohort only*, without you editing each chart.

This is what makes a template like "Tool focus" reusable: point the focus filter at a specific tool and the whole board becomes a report about that tool. Change the focus and the same charts re-scope to the new cohort.

## Arranging the grid

Charts live on a drag-and-resize grid.

- **Drag** a chart by its header to move it; **drag its edge** to resize.
- The layout is **responsive** — Brizz remembers your arrangement per screen size, so a board you tune on a wide monitor still reads well on a laptop.
- Layout changes are part of the **draft** — rearrange freely, then **save** to publish the new layout to your team. Until you save, only you see the changes.

## Cross-filter drill-down

Charts on a dashboard are interactive. Clicking into a data point — a bar, a slice, a row — drills the view down to that segment, so a click on "the checkout tool" or "the refund intent" narrows what you're looking at without opening the chart builder. It's the fast path from "something looks off in this chart" to "show me exactly those sessions."

## Date range

A dashboard has a single **date range** that applies to every chart on it. Change the range and the whole board re-queries for that window. Individual charts still express their own time bucketing (see [time series in Custom charts](/docs/platform/custom-charts.md#time-series-and-bucketing)), but the outer window is shared.

## Sharing

Every dashboard has a shareable **URL**. The link reproduces the *current view* — the dashboard, its focus filter, and its date range — so a teammate opens exactly what you're looking at.

Two things to know about a shared link:

- **It's a view, not a snapshot.** The recipient sees live data for that view, not a frozen copy of your numbers.
- **It doesn't grant access or edit rights.** The recipient still needs access to the tenant to open it. Sharing a URL is a pointer, not a permission grant.

## Managing dashboards

Manage an existing dashboard from its header:

- **Rename** it by clicking the dashboard title and typing a new name.
- **Change its icon** or **delete** the dashboard from its settings menu.

There's no one-click "duplicate dashboard" — duplication works at the [chart level](/docs/platform/custom-charts.md#editing-and-duplicating-charts) instead. To spin up a themed variant of a board quickly, start a new dashboard from a [template](#templates).

Because dashboards are tenant-shared, these actions affect **everyone** on your tenant: rename or delete a board and it changes for the whole team, not just for you.

## Availability

Custom dashboards are gated by a tenant feature setting and a plan tier. If you don't see the option to create dashboards, the feature isn't enabled for your tenant yet — contact your Brizz admin or reach out to Brizz. Where the feature is enabled, **any member of the tenant can create and edit** dashboards; there's no separate dashboard-editor role.

## See also

- [Custom charts](/docs/platform/custom-charts.md) — how to build the charts that go on a dashboard.
- [Cost calculation](/docs/platform/cost-calculation.md) — the cost metric behind cost charts.
- [Sessions](/docs/platform/sessions.md) — the underlying data most charts aggregate.


---

# Custom charts

Build charts over your telemetry — pick a dataset, a metric, a chart type, and how to group and filter it.

A **custom chart** is a chart you define yourself: choose what to measure, how to slice it, and how to draw it, and Brizz renders it against your telemetry. Charts live on [custom dashboards](/docs/platform/custom-dashboards.md) — this page is about the charts themselves.

## What it is

A chart is a **saved query, not saved data.** You describe *what to ask* — a metric, a way to group it, some filters — and Brizz runs that query against your telemetry every time the chart is shown. Change your date range or send new data and the chart reflects it immediately; there's no snapshot to regenerate.

That's the mental model to keep: you're building a question, and the chart is its live answer.

## Three ways to add a chart

When you add a block to a dashboard you can:

1. **Build a custom chart** — define a new query from scratch in the chart builder (below). This is the main path.
2. **Add from the library** — start from a ready-made chart instead of a blank one, then tweak it. A fast way to get a common view without wiring it up yourself.
3. **Add a text block** — a Markdown block for a title, a note, or a section header. Not a query — just text to give the board structure.

## The chart builder

The builder walks you from *what data* to *how to draw it*. You don't have to touch every step — sensible defaults fill in as you go.

1. **Dataset** — pick what you're measuring over: **sessions**, **users**, or **organizations** (see [Datasets](#datasets)).
2. **Chart type** — pick how it's drawn: a line, a bar chart, a number card, a table, and so on (see [Chart types](#chart-types)).
3. **Metric and aggregation** — pick the number (cost, duration, tokens, a count, a rate, …) and how to roll it up (sum, average, a percentile, …).
4. **Group by** — pick how to break the metric out: by model, over time, by outcome, by intent, and more (see [Group by and top-N](#group-by-and-top-n)).
5. **Filter** — narrow to the rows you care about before the metric is computed.
6. **Advanced** — the optional knobs: `having`, ordering and limits, time-series fill, thresholds, and formulas (below).

Throughout, the builder's **pickers show what's actually available for your choices** — the metric list changes with the dataset, the group-by options reflect what Brizz knows about your data. Trust the picker over any fixed list: it's the live source of truth for what you can chart.

## Datasets

Every chart is built on one of three datasets. Pick the one whose "one row" matches the question you're asking:

- **Sessions** — one row per conversation. Answers "how are individual sessions doing?" — cost per session, duration, error rates, outcomes, satisfaction.
- **Users** — one row per end user. Answers "how are my users doing?" — activity, retention-shaped counts, per-user rollups.
- **Organizations** — one row per organization (the accounts your users belong to). Answers the same kinds of questions at the account level.

There's no raw SQL — the dataset plus the builder's pickers define what's queryable, which keeps every chart tenant-scoped and safe.

## Chart types

The **type selector** offers a spread of visualizations; pick the family that fits the shape of your answer:

- **Time series** (line, area) — a metric over time.
- **Bar** (bar, stacked bar) — compare a metric across categories, optionally split into sub-segments.
- **Pie / donut** — parts of a whole.
- **Table** — the raw grouped rows, with columns.
- **Number card** — a single headline figure.
- **Gauge** — one value against a target or range.
- **Scatter** — two metrics plotted against each other.

Not every type fits every query — the selector reflects what makes sense for the metric and grouping you've chosen.

## Group by and top-N

**Group by** is how a metric becomes a chart with more than one value. Brizz offers grouping by the dimensions it understands about your data — for example by **model**, by **time bucket**, by **outcome**, by **satisfaction**, by **journey**, by **intent**, by **issue type**, or by **tool** — plus dynamic dimensions derived from your own data, such as a **custom property**, a **label**, or an **N-day** window.

When a grouping has many values, use **top-N** to keep the chart readable — chart the largest N groups and let the rest fall away. As with everything else, the available group-by options come from the picker, which reflects your data rather than a fixed menu.

## Filters vs. having

Two ways to narrow a chart — they act at different stages:

- **Filters** apply **before** aggregation. They pick which rows go into the metric ("only sessions from the checkout service", "only paid users"). Use a filter to define the population you're measuring.
- **Having** applies **after** aggregation. It filters the *grouped results* by their computed value ("only models where average cost > $0.10", "only intents with more than 100 sessions"). Use `having` to hide small or uninteresting groups.

Reach for a filter to change *what you're counting*, and `having` to change *which results you keep*.

## Time series and bucketing

For a time-series chart, Brizz buckets your data into time intervals and plots one point per bucket. Empty buckets are **filled** so the line is continuous rather than skipping gaps — a quiet hour shows up as a real zero, not a missing point, which keeps trends honest. The dashboard's [date range](/docs/platform/custom-dashboards.md#date-range) sets the outer window; the chart's bucketing sets the granularity within it.

## Thresholds and formulas

Two ways to make a chart say more:

- **Thresholds** — draw a reference line (a target, an SLO, a budget) so "good vs. bad" is visible at a glance, and gauges color against it.
- **Formulas** — derive a value from other metrics (a ratio, a rate, a cost-per-something) rather than charting a single raw metric.

To read a chart *period-over-period* — this window versus the one before it — subscribe it to a [Slack digest](#slack-chart-digest), which compares the two windows for you.

## Editing and duplicating charts

Open any chart's menu to **edit** it — you re-enter the builder with its current definition loaded, so you can adjust the metric, grouping, or type in place. **Duplicate** a chart to spin off a variant (same query, one knob changed) without rebuilding it. Chart edits, like layout changes, are part of the dashboard **draft** until you save.

## Slack chart digest

Any chart can be turned into a recurring **Slack digest** — subscribe a chart and Brizz renders it as an image and posts it to a Slack channel on a schedule:

- **Cadence** — **daily** or **weekly**.
- **Optional AI summary** — a short written read of what the chart shows and what moved, alongside the numbers.
- **Period-over-period shifts** — each digest compares the current window against the previous one and calls out the notable movers, so the signal isn't buried.

This is per **chart**, not per dashboard — there's no whole-dashboard scheduled export; you subscribe the specific charts worth a recurring nudge. Setting one up requires the [Slack integration](/docs/integrations/slack.md) to be connected.

## Limits

A chart returns a **capped number of rows** so a runaway grouping can't pull an unbounded result set — the builder shows the current default and ceiling when you set a limit, and top-N is the intended tool for high-cardinality groupings. If a chart looks truncated, tighten it with a filter, a smaller top-N, or a coarser grouping rather than trying to raise the cap.

## See also

- [Custom dashboards](/docs/platform/custom-dashboards.md) — the boards these charts live on.
- [Cost calculation](/docs/platform/cost-calculation.md) — how the cost metric behind cost charts is computed.
- [Slack integration](/docs/integrations/slack.md) — connect Slack to receive chart digests.
- [Labels](/docs/platform/labels.md) — group any chart by a label.
- [Sessions](/docs/platform/sessions.md) — the session data most charts aggregate.


---

# Overview

Your tenant's home page — a single-screen read on agent health, top problems, and emerging behavior.

The **Overview** is the landing page for your tenant: one screen that answers "how is my agent doing right now?" before you drill into any single page. It stitches the headline numbers, the worst problems, and the newest behavioral patterns into panels you can scan in a few seconds.

## What it is

The Overview is a page of read-only panels, each summarizing one area of analysis. It's built to be glanced at — every panel links through to the full view behind it, so the Overview is where you start and the specialized pages are where you finish.

## What each panel tells you

- **KPI strip** — the headline metrics across the top: the top-line counts and rates that frame everything below.
- **Agent Health Score** — a single rolled-up read on how well your agent is performing, so you can tell "good week / bad week" at a glance.
- **Top Issues** — the highest-priority problems detected across your traffic, a shortlist of the full [Issues](/docs/platform/issues.md) triage view.
- **Failure Breakdown** — how failures split across categories, so you can see whether the pain is runtime, content, or product.
- **Journey treemap** — the common paths users take, sized by volume and annotated with trend — a compact view of [User journeys](/docs/platform/user-journeys.md).
- **Emerging Intents** and **Emerging Journeys** — what's newly surging in what users ask for and how they move through your agent, the leading edge of [User intents](/docs/platform/user-intents.md) and journeys.
- **Live users** — how many users are active right now.
- **Platform-status footer** — a quick health read on the platform itself, at the bottom of the page.

Each panel is a summary. Treat a number that looks off as a prompt to click through, not a conclusion.

## Getting data to appear

A brand-new tenant sees an **onboarding empty state** instead of populated panels — there's no data to summarize yet. Send your first sessions (see [Sessions](/docs/platform/sessions.md)) and the panels begin to fill.

Some panels stay **locked behind an overlay** until your tenant has enough sessions to make the summary meaningful — analysis like journeys and emerging patterns needs a minimum volume before it produces a stable read. Cross that session threshold and the locked panels populate on their own; there's nothing to switch on.

## How to act on it

1. **Read top-to-bottom, then click through.** The Health Score and KPI strip tell you *whether* to worry; the panels below tell you *where*; the linked pages tell you *why*.
2. **Start triage from Top Issues.** It's the shortlist — open the full [Issues](/docs/platform/issues.md) page when you need the rest.
3. **Watch the emerging panels for surprises.** A new intent or journey that appears here often signals an unplanned use case worth a closer look.
4. **Use the Overview as a standup screen.** It's the fastest shared read on agent health for a team check-in.

## Availability

Any member of the tenant can view the Overview. The Journeys and Intents panels are plan-gated — if they show as locked, your plan doesn't include them yet.

## See also

- [Sessions](/docs/platform/sessions.md) — drill into the conversations behind the numbers.
- [Issues](/docs/platform/issues.md) — the full triage view behind the Top Issues panel.
- [User journeys](/docs/platform/user-journeys.md) — the journeys behind the treemap.
- [User intents](/docs/platform/user-intents.md) — the intents behind the Emerging Intents panel.
- [Custom dashboards](/docs/platform/custom-dashboards.md) — build your own board when the Overview doesn't frame data your way.


---

# Users

Per-user analytics — one row per identified end user, with activity, cost, and session metrics.

The **Users** page is a table of your agent's end users, one row each, with the activity, cost, and session metrics that tell you who's using your agent and how much. It's where a question like "who are my heaviest users, and what are they costing me?" gets answered.

## What it is

The Users page is a per-user analytics table. Each row is one identified end user; the columns are that user's rolled-up metrics — how active they are, how many sessions they've run, what they've cost. Sort, filter, and open any user for the detail behind their row.

## How it's populated

The table is populated **only when you identify your users from the SDK**. A user becomes a row when your instrumentation attaches an identity to their sessions; without identify calls, Brizz has no user to key on and the page stays empty. See [Identify users](/docs/instrument/identify-users.md) for the SDK calls that populate it.

## Reading the table

Each row rolls a single user's sessions up into activity, cost, and session metrics. The table sorts by any column and filters at the top, so you can go from "everyone" to "the ten users who cost the most this week" in a couple of clicks.

Beyond the built-in columns, you can add **custom-property columns** — surface any property you attach to your users as its own column, so the attributes that matter to your product sit alongside the standard metrics. The column picker reflects the properties Brizz has actually seen on your users, so trust it over any fixed list.

## The user details drawer

Click any row to open the **details drawer** for that user — a focused view of one user's activity, metrics, and recent sessions without leaving the table. Use it to go from a row that looks unusual to the sessions behind it.

## Cohorts and saved filters

- **Saved filters** — narrow the table to a population you care about ("paid users, last 30 days") and save the filter so it's one click next time.
- **Cohorts** — build a group of users from a set of conditions and save it as a named **cohort**, so you can return to the same population and reuse it.

## Export

Export the current table to **CSV** to pull the user list — with whatever filters and columns you've set — into a spreadsheet or another tool.

## Availability

Any member of the tenant can view the Users page. Your tenant needs the users feature enabled for the page to appear in the sidebar.

## See also

- [Identify users](/docs/instrument/identify-users.md) — the SDK calls that populate this table.
- [Organizations](/docs/platform/organizations.md) — the same analysis rolled up to the account level.
- [Leaderboard](/docs/platform/leaderboard.md) — rank users against each other.
- [Sessions](/docs/platform/sessions.md) — the sessions behind each user's activity.


---

# Organizations

Per-organization analytics for B2B agents — engagement, retention, and cost rolled up per account.

The **Organizations** page rolls your analytics up from individual users to the accounts they belong to. For a B2B agent, this is where "how is each customer doing?" gets answered — engagement, retention, and cost per organization, one row each.

## What it is

The Organizations page is a per-organization analytics table. Each row is one organization; the columns are that account's rolled-up engagement, retention, and cost metrics. Sort, filter, and open any organization for the detail behind its row.

## Setup

Organizations are **derived from an org-id property you configure first** — a one-time setup step. You tell Brizz which of your user properties identifies an organization, and Brizz groups users by that property into accounts. Until you map an org-id property, there's nothing to group by and the page has no rows. The property itself is attached from the SDK; see [Identify users](/docs/instrument/identify-users.md).

## Reading the table

Each row aggregates every user in one organization into account-level engagement, retention, and cost. Sort by any column and filter at the top to move from "all accounts" to the slice you care about — your largest accounts, your least engaged, your most expensive.

## The organization details drawer

Click any row to open the **details drawer** for that organization — a focused view of one account's metrics and activity. Use it to go from an account that stands out in the table to what's driving it.

## Filters and export

- **Filters** — narrow the table to the accounts you care about at the top of the page.
- **CSV export** — pull the current table, with your filters applied, into a spreadsheet or another tool.

## Availability

Any member of the tenant can view the Organizations page. Your tenant needs organizations enabled. The redacted-filter capability is plan-gated — if it's unavailable, your plan doesn't include it yet.

## See also

- [Identify users](/docs/instrument/identify-users.md) — attach the org-id property that powers this page.
- [Users](/docs/platform/users.md) — the same analysis at the individual-user level.
- [Leaderboard](/docs/platform/leaderboard.md) — rank organizations against each other.


---

# System performance

A system-health view of your agent — session volumes, error rates, and cost over time.

The **System performance** tab is the health-monitoring view of your agent — session volumes, error rates, and cost, tracked so you can tell whether the system as a whole is behaving. It's the "are we up and steady?" view, distinct from the deeper quality analysis on the other pages. Reach it at `/dashboard?tab=performance`.

## What it is

System performance is a dashboard tab focused on operational health rather than conversation quality. It answers questions about throughput and reliability: how much traffic is flowing, how much of it is erroring, and what it's costing — all over your selected window.

## What it tracks

- **Session volumes** — how many sessions are flowing through your agent over time.
- **Error rates** — how much of that traffic is failing.
- **Cost** — what the traffic is costing (computed as described in [Cost calculation](/docs/platform/cost-calculation.md)).

**KPI cards** across the top carry the headline figures; the charts below break each one out over time.

## Filters and segmentation

The tab shares the dashboard's **global filter bar and segmentation controls**, so any filter or segment you set applies here the same way it applies across the dashboard. Narrow to a service, a time range, or a segment and every metric on the tab re-reads for that slice.

## How to act on it

1. **Watch the trend, not the instant.** A spike in error rate or cost matters more as a *change* than as an absolute number — compare against the preceding window.
2. **Segment to localize a problem.** When a rate moves, use segmentation to find which slice of traffic moved it before you go digging.
3. **Pair volume with latency.** Rising volume with steady latency is healthy growth; rising volume with climbing latency is a capacity problem — cross-check [Latency & performance](/docs/platform/latency.md).

## Availability

Any member of the tenant can view this tab. It's on by default — if you don't see it, your tenant has turned it off.

## See also

- [Latency & performance](/docs/platform/latency.md) — response-time detail to pair with these volume and error metrics.
- [Cost calculation](/docs/platform/cost-calculation.md) — how the cost figure on this tab is computed.
- [Overview](/docs/platform/overview.md) — the tenant-wide health summary.


---

# Latency & performance

Where the time goes in a turn — response times, time-to-first-response, and turns per session.

The **Latency & performance** tab is about speed: how long your agent takes to respond, where that time goes within a turn, and how many turns a conversation runs. It's the view you open when the complaint is "the agent feels slow." Reach it at `/dashboard?tab=latency`.

## What it is

This tab breaks down your agent's timing. Where the [System performance](/docs/platform/performance.md) tab tells you *how much* traffic and *how many* errors, this one tells you *how fast* — the response-time picture across your traffic.

## The metrics

- **Time-to-first-response (TTFR)** — how long until the user sees the first token or response. This is the number users actually feel; a good TTFR keeps a conversation feeling responsive even when the full answer takes longer.
- **Total trace duration** — how long the whole turn takes end to end, across every LLM and tool call. The gap between TTFR and total duration is the work that happens *after* the user starts seeing output.
- **Turns per session** — how many back-and-forth exchanges a conversation runs. Read alongside the timing metrics, it tells you whether sessions are long because they're productive or because the agent isn't resolving things quickly.

## The timing diagram

A **timing diagram** visualizes where the time goes across a single turn — laying the phases of a turn out in sequence so you can see which part dominates. Use it to tell a slow *model* apart from a slow *tool call* apart from time spent between steps, without reading a raw span timeline.

## How to act on it

1. **Optimize TTFR first.** It's the latency users perceive. Getting the first token out sooner often beats making the whole turn faster.
2. **Decompose before you optimize.** Use the timing diagram to find the dominant phase — there's no point speeding up the model if a tool call owns most of the turn.
3. **Read turns-per-session as a quality signal.** Climbing turn counts can mean users are fighting to get an answer, not that they're more engaged.
4. **Drill into a slow session.** When a metric looks wrong, open the session and read its per-span timeline for the specific culprit — see [Sessions](/docs/platform/sessions.md).

## Availability

Any member of the tenant can view this tab. It's on by default — if you don't see it, your tenant has turned it off.

## See also

- [System performance](/docs/platform/performance.md) — volumes, error rates, and cost alongside these latency metrics.
- [Sessions](/docs/platform/sessions.md) — open a slow session and read its per-span timeline.


---

# Leaderboard

Rank users, organizations, and segments by activity, cost, satisfaction, outcomes, and more.

The **Leaderboard** tab ranks the entities in your data — users, organizations, and segments — so you can see who's at the top and who's at the bottom on the metric you care about. It's your "who are my best and worst accounts?" view. Reach it at `/dashboard?tab=leaderboard`.

## What it is

The Leaderboard is an **entity ranking**. It orders users, organizations, or segments by a chosen metric and shows you both ends — the leaders and the laggards. This is a ranking of *who*, not a comparison of models or prompts: it tells you which accounts and segments are doing best and worst, not which agent version performs better.

## What it ranks

Pick the kind of entity to rank:

- **Users** — individual end users (see [Users](/docs/platform/users.md)).
- **Organizations** — accounts (see [Organizations](/docs/platform/organizations.md)).
- **Segments** — groups defined by a shared attribute.

Each ranking shows both **top and bottom** — the strongest and the weakest on the selected metric — because the bottom of the list is often where the actionable problems are.

## The metrics

Rank by any of a spread of metrics, including:

- **Active days** — how many distinct days the entity showed up.
- **Sessions** and **messages** — how much they used the agent.
- **Cost** — what they cost.
- **Satisfaction** — how happy they were.
- **Outcome / task-completion rate** — how often their sessions ended successfully.
- **Issue count** — how many problems they hit.

The metric picker is the source of truth for what's rankable — trust it over any fixed list here.

## How to act on it

1. **Read both ends.** The top tells you who your power users and healthiest accounts are; the bottom tells you where churn and frustration are concentrated.
2. **Rank by outcome or satisfaction, not just volume.** A high-volume account with a low completion rate is a support risk hiding behind a big number.
3. **Cross-check a low ranker in its detail table.** When an account sits at the bottom, open it in [Users](/docs/platform/users.md) or [Organizations](/docs/platform/organizations.md) to see what's dragging it down.

## Availability

Any member of the tenant can view this tab. It's on by default — if you don't see it, your tenant has turned it off.

## See also

- [Users](/docs/platform/users.md) — the per-user table behind user rankings.
- [Organizations](/docs/platform/organizations.md) — the per-organization table behind org rankings.


---

# Insights

AI-generated insight cards over your own data, with a quality-trend list per service.

The **Insights** page surfaces what Brizz notices in your data on its own — a feed of AI-generated insight cards, alongside a quality-trend list for the service you're looking at. It's the "what should I know that I didn't think to ask?" view.

## What it is

Insights is a page of **AI-generated cards**. Where the other pages answer questions you pose, Insights raises observations you didn't — patterns, shifts, and notable findings Brizz pulled out of your traffic and wrote up for you to read.

## How insights are generated

Insights are generated by **AI over your own data**. Brizz analyzes the sessions, issues, and metrics for the selected service and writes up what stands out as short, readable cards. They're derived from your actual traffic, not a fixed template — so what shows up reflects what's happening in your agent.

## Reading cards and trends

- **Insight cards** — each card is one observation, written to be read at a glance. Treat a card as a lead to follow, not a final verdict; click through to the underlying page to confirm and dig in.
- **Quality-trend list** — for the selected service, a list of how quality is trending, so the headline cards sit alongside the direction things are moving.

Insights are scoped to the **selected service** — switch services to see that service's insights and trends.

## Archiving

When you've read a card and don't need it in your feed, **archive** it. Archived insights move out of the main view into a separate **archived view**, so your active feed stays focused on what's current while nothing is lost — open the archived view any time to revisit what you've cleared.

## How to act on it

1. **Skim the feed, then verify.** A card is a starting point; open the page behind it before you act on the observation.
2. **Archive as you go.** Clear cards you've handled so the feed reflects what still needs attention.
3. **Ask a follow-up.** When a card raises a question, take it to the [AI assistant](/docs/platform/ai-assistant.md) to dig into the specifics.

## Availability

Any member of the tenant can view Insights. Your tenant needs the insights feature enabled.

## See also

- [Issues](/docs/platform/issues.md) — the problems insights often point you toward.
- [AI assistant](/docs/platform/ai-assistant.md) — ask follow-up questions about what an insight surfaced.
- [Overview](/docs/platform/overview.md) — where headline insights also surface.


---

# AI assistant

Ask Brizz in plain language — about your own telemetry and about the product itself — and get answers with inline charts, clickable entity cards, and downloadable CSV or JSON reports.

The **AI assistant** is Brizz's in-product agent. Ask it a question in plain language and it answers from your data — sessions, issues, events, users, journeys, organizations, intents, cost — or from the product docs when you ask how something works. It's the fastest way to get an answer without knowing which page to open first.

## What it is

A conversational assistant that reasons over two things:

- **Your own telemetry.** It can answer questions about your sessions, issues, events, users, journeys, organizations, intents, and cost — the same data the rest of the dashboard is built on.
- **Brizz itself.** It can answer product and how-to questions from the documentation, so "how do I build a custom chart?" gets a real answer, not a shrug.

You don't pick between the two — ask your question and the assistant works out whether it's about your data or about the product.

## What you can ask

The assistant is built for the questions you'd otherwise click through several pages to answer. The suggested prompts it offers are the best guide to what it's good at. A few illustrative examples:

- "Show me failed sessions."
- "What are my top issues?"
- "Top user intents."
- "Who are my most active users?"
- "How much did I spend this week?"
- "How did my error rate change week over week?"
- "Export the last 20 sessions as a CSV."

Treat these as a starting shape, not a fixed menu — phrase your own question naturally and the assistant will map it to the data it has. When you're not sure what to ask, the in-product suggestions are the live source of truth for what it can do.

## Where to find it

There are three ways in, each suited to a different moment:

- **Full-page view.** A dedicated page at `/agent` — the place to go for a longer back-and-forth or when the assistant is your starting point.
- **Global side panel.** A floating action button (FAB) is available across the dashboard; open it and the assistant slides in as a side panel, so you can ask a question without leaving the page you're on.
- **Fix with your agent.** Launched from an issue, this opens the assistant already scoped to that issue — you skip straight to "help me understand this one" instead of describing it from scratch.

## Charts and entity cards in answers

Answers aren't just text. Where it helps, the assistant renders results inline:

- **Generated charts.** A question about a trend or a breakdown comes back with a chart drawn right in the conversation, so you see the shape of the answer, not just a number.
- **Entity cards.** When an answer points at a specific thing — a session, an issue, a journey, an organization, or an intent — the assistant shows it as a clickable card that deep-links into the relevant page. Read the summary in chat, then click through to the full record when you want the detail.

## Downloadable reports

When you want results as a file to keep or share — not just an answer in the chat — ask the assistant to create a report. It saves your query and gives you a **Download** button for a **CSV** or **JSON** file.

- **Just ask for it.** "Export the last 20 sessions as a CSV," "give me a JSON report of my top intents this month" — the assistant works out the query, previews it back to you for approval, and saves it as a report. Yes, the assistant can create reports for you; you don't need to export anything by hand.
- **Choose the format.** Ask for CSV or JSON, or the assistant asks you to pick before it runs the query.
- **A button, not a link.** Clicking Download re-runs the saved query against your data on the spot and streams the result straight to you — there's no separate file sitting in storage and no link to copy or share. It's authenticated to your logged-in session, so it never expires; click it again any time for a fresh export.
- **Come back to it later.** Your reports are saved to your account, so in a future conversation you can ask the assistant to find a report you made before, download it again, or update it with a new query.

## When it asks you to clarify

If your request is ambiguous, the assistant asks a clarifying question rather than guessing. "Show me the slow ones" might prompt "slow by latency, or by number of turns?" This is by design — a quick clarification up front beats a confident answer to the wrong question. Answer it and the assistant continues with the narrower request.

## Tips for good answers

- **Say the time window.** "This week," "last 30 days," "since Monday" — a bounded range gives a sharper answer than an open-ended one.
- **Name the entity type when you know it.** "Top issues," "most active users," "which intents" — pointing at sessions vs. users vs. issues helps the assistant frame the query.
- **Follow up in the same thread.** The assistant keeps context, so "now just the checkout service" or "break that down by model" refines the previous answer instead of starting over.
- **Answer its clarifying question rather than rephrasing from scratch.** When it asks, the fastest path forward is to reply to the question it asked.
- **Click through to verify.** Use the entity cards to open the underlying session or issue when the answer matters — the chat summary is a hint, the record is the truth.

## Availability

The AI assistant is available to any member of the tenant. Your tenant needs the assistant enabled and a plan that includes it. **Fix with your agent** is gated separately — a tenant can have the assistant without that entry point, or the other way around. If you don't see the assistant or the Fix with your agent action, the corresponding capability isn't enabled for your tenant yet.

## See also

- [Sessions](/docs/platform/sessions.md) — the session data the assistant reasons over.
- [Issues](/docs/platform/issues.md) — where "Fix with your agent" starts.
- [Custom charts](/docs/platform/custom-charts.md) — build persistent versions of the charts the assistant generates inline.


---

# Labels

Categorize sessions and other entities — manually, by rule, or with AI — then filter and group your analytics by them.

**Labels** are how you categorize your data in Brizz. Attach a label to a session (or another entity) and it becomes a dimension you can filter and group by everywhere else — a way to carry your own vocabulary into the analytics, not just the fields the SDK sent.

## What it is

A label marks an entity as belonging to some category you care about — a plan tier, a customer segment, a review status, a product area. Once applied, a label isn't just a tag on one record; it's a dimension you can slice the whole product by.

Labels come in two shapes: a **plain label** and a **label group**.

## Labels vs. label groups

- **Plain label** — a single category applied on its own. An entity either has it or doesn't.
- **Label group** — a label with a set of **allowed values**. A `plan` group might allow `free`, `pro`, and `enterprise`; a `review-status` group might allow `pending`, `approved`, and `rejected`. Each value can carry its own **color** and **description**, so the group reads clearly wherever it's shown.

Reach for a group when you want related categories managed together; reach for a plain label when it's a standalone flag. Groups can be mutually exclusive, but an [AI classification rule](#labeling-rules) can assign several values to the same session when it's configured to allow multiple values — the **Single value** checkbox in the labeling-rule editor, or **Single category per session** in the classification editor.

## How labels get applied

A label reaches an entity in one of three ways:

- **Manually** — someone applies the label directly to an entity.
- **By a labeling rule** — a saved rule applies the label automatically as data arrives.
- **By AI** — "auto-assign with AI" classifies entities and applies the label for you, so you don't have to write the matching logic by hand.

The three paths coexist. A label can be applied by hand to one session and by a rule or by AI across thousands of others.

## Labeling rules

Labeling **rules** are how you automate application instead of tagging entities one at a time. A rule targets a label and decides what gets it. Rules come in two kinds:

- **Manual matching** — a rule you define with explicit matching criteria.
- **AI classification** — a rule that hands classification to the AI, which decides which entities match and applies the label.

Rules are an admin capability — see [Availability](#availability). The rule editor is the in-product source of truth for what each rule kind can target and how it's configured.

## Using labels

Once a label exists and is applied, it becomes a dimension across the analytics:

- **Filter** by a label to narrow any view to the entities that carry it.
- **Group by** a label to break a metric out along your own categories — for example, group a [custom chart](/docs/platform/custom-charts.md) by a label to compare a metric across its values. For a multi-value classification, one session can count toward several values, so per-value counts can overlap.

### System labels

Brizz ships some **system labels** of its own. The clearest example is **`product-area`**, which organizes the Issues view — it drives the product-area treemap on the Issues Overview. System labels exist alongside your own, and you can filter and group by them just the same. The difference is ownership: system labels are **read-only** to your tenant. You use them; you don't edit them.

## Availability

**Viewing** labels is available to any member — the `/labels` page is read-only for members, so anyone can see which labels exist and how they're defined. **Managing** labels (creating and editing them) requires admin or higher. **AI auto-assign** additionally needs a plan that includes AI labels. **Managing labeling rules** requires admin or higher. If you can see labels but can't change them, your role is view-only for this area.

## See also

- [Labeler instances](/docs/admin/labeler-instances.md) — configure the automatic labelers that apply labels for you.
- [Notifications](/docs/platform/notifications.md) — a Label Digest delivers label trends on a schedule.
- [Custom charts](/docs/platform/custom-charts.md) — group any chart by a label.
- [Issues](/docs/platform/issues.md) — the product-area system label organizes the Issues view.


---

# Notifications

Deliver digests, alerts, and AI summaries to Slack or Email — built through a wizard, delivered on a schedule.

**Notifications** push what's happening in Brizz out to where your team already works. Instead of remembering to check the dashboard, you set up a notification once and Brizz delivers digests, alerts, and AI-written summaries to Slack or Email on a schedule.

## What it is

A notification is a standing subscription: pick what you want to hear about, where it should go, and how often, and Brizz sends it without further prompting. Some notifications react to events as they happen (an alert); most run on a cadence you set (a digest or briefing). Either way, the point is the same — the signal comes to you.

## The notification wizard

Notifications are built through a short wizard that walks you from *what* to *where* to *how often*:

1. **Type** — pick what kind of notification this is (see [Notification types](#notification-types)).
2. **Route** — choose the delivery service and channel it goes to (a Slack channel or an email destination).
3. **Configure** — set the type-specific options and the schedule.
4. **Review** — confirm everything, then create it.

The wizard's options change with the type you pick, so the steps you see reflect what that notification actually needs. Trust the wizard over any fixed list of settings.

## Notification types

Brizz offers several notification types, each answering a different question:

- **Alert** — fires when something notable happens: a **new issue** appears or a **spike** is detected. Use it when you want to know the moment a problem shows up, not at the end of the day.
- **Periodic Report** — a **daily or weekly digest** that summarizes the period and compares it against the last one, so you see **change vs. the previous period**, not just current totals.
- **Emerging Information** — surfaces what's **trending**: intents, journeys, and labels that are on the move. It runs **daily at 10:00 UTC**.
- **AI Daily TLDR** — an **AI-written briefing** that reads your recent activity and summarizes what matters in prose, so the highlights arrive already interpreted.
- **Label Digest** — a scheduled digest of **label activity**, so you can track how your [labels](/docs/platform/labels.md) are trending over time.
- **Chart Digest** — a recurring delivery of a specific [custom chart](/docs/platform/custom-charts.md) as an image, with optional AI summary and period-over-period shifts. It's set up from the chart itself and documented in [Custom charts](/docs/platform/custom-charts.md#slack-chart-digest) — see there for the details.

## Channels and scheduling

Every notification routes to a **channel** on a delivery service:

- **Slack** — posts to a Slack channel. This requires the [Slack integration](/docs/integrations/slack.md) to be connected first.
- **Email** — sends to an email destination.

Scheduled notifications share the same set of timing controls:

- **Cadence** — how often it runs (for example, daily or weekly, depending on the type).
- **Time of day** — when it runs, expressed in **UTC**. Convert from your local time when you set it.
- **Excluded days** — days to skip, so a daily digest doesn't fire on, say, weekends.

Some types set part of the schedule for you — Emerging Information, for instance, runs daily at a fixed time — while others leave the cadence up to you.

## Availability

Managing notifications requires **admin**. Your tenant also needs a plan that includes notifications. Two types carry an extra requirement:

- **Chart Digest** additionally needs custom dashboards and charts enabled.
- **Label Digest** additionally needs the label-digest feature enabled.

If a type doesn't appear in the wizard, the capability it depends on isn't enabled for your tenant yet.

## See also

- [Slack integration](/docs/integrations/slack.md) — connect Slack so notifications can post to a channel.
- [Custom charts](/docs/platform/custom-charts.md) — the Chart Digest that delivers a chart on a schedule.
- [Labels](/docs/platform/labels.md) — the labels a Label Digest and Emerging Information track.


---

# Session review

A human-review queue for sessions: rules flag the sessions worth a look, and your team works through them as open, ignored, or done.

**Session review** is a work queue for humans. Rules decide which sessions someone should read; matching sessions land in a queue your team works through, instead of being spotted by whoever happened to be browsing.

:::info
Session review is off by default, and the planner that fills the queue also requires **Tasks** to be enabled. If the page is missing, or the queue stays empty with enabled rules in place, [contact us](mailto:support@brizz.ai) — neither is a setting an organization admin can change.
:::

## What it is

**Rules** decide which sessions get flagged. The **queue** is where they wait for a person. It's deliberately separate from [Issues](/docs/platform/issues.md): an issue is a problem Brizz detected and clustered, while a review is a session a human asked to look at — a healthy session can still be worth reviewing.

## How it's populated

A rule is a set of filter conditions over sessions, drawn from a subset of the [Sessions](/docs/platform/sessions.md) filters. A planner runs periodically and applies every enabled rule across a recent lookback window, so items arrive in batches rather than the moment a session finishes. A session matched by several rules is queued once, and each run adds at most 1,000 matches per rule. Rules can be disabled without deleting them.

## How to read it in the dashboard

Three tabs: **Open** (waiting), **Ignored** (looked at, set aside), and **Done**. You can mark an open item done or ignore it, and reopen a closed one — no decision is final.

Each rule carries a name, an optional description, its conditions, and an enabled switch. Managing rules requires **admin** or higher; anyone with page access can work the queue.

## How to act on it

1. **Write rules for sessions you'd read anyway.** "Negative feedback" or "errors on the new model" are good; "all sessions" recreates the Sessions page with extra steps. A rule isn't scoped to one service, so put the narrowing in the conditions.
2. **Keep Open drainable.** A queue nobody can finish stops being read — tighten the rule rather than working harder.
3. **Ignore deliberately.** Lots of ignores on one rule means it's over-matching.
4. **Raise an issue when a review finds a pattern.** The queue is for individual sessions; a recurring problem belongs in [Issues](/docs/platform/issues.md).

## Tasks

The **Tasks** page is the landing page for "what needs my attention", combining pending reviews with open issues seen in the last week. Both lists are capped, so it's a shortlist rather than a full inventory. Depending on your navigation layout it may replace the Issues entry or sit beside it.

## Availability

Off by default for both the queue and Tasks, and enabled by Brizz rather than by an organization admin. Creating and editing rules requires **admin** or higher.

## See also

- [Sessions](/docs/platform/sessions.md) — the filters a rule is built from, and where a flagged session opens.
- [Issues](/docs/platform/issues.md) — the detected-and-clustered counterpart to a human queue.


---

# Slack Integration

Connect Brizz to Slack for real-time alerts and notifications

Connect Brizz to your Slack workspace to receive real-time alerts and notifications about your AI agents directly in Slack.

## Why Connect Slack?

Integrating Slack with Brizz enables:

- **Daily Activity Reports**: Receive daily summaries of your agent's performance
- **Weekly Summaries**: Get weekly insights delivered to your team
- **Emerging Intent Alerts**: Be notified when new user intents are detected
- **Issue Notifications**: Stay informed about critical issues as they happen

## How to Connect

1. Navigate to **Settings > Integrations** in the Brizz dashboard
2. Click **Connect Slack**
3. Authorize Brizz in your Slack workspace
4. Select the channel where you want to receive notifications

### Upgrading an existing install

When Brizz adds new bot scopes (for example, the AI agent features coming soon), Slack does not auto-prompt installed workspaces. A workspace admin must re-authorize Brizz once to grant the new permissions. If your workspace needs an upgrade, the integration card will show an **Update required** state with a **Reconnect Slack** button — one click completes the flow and preserves all of your existing channel settings.

## App Directory Listing

### Short Description
Real-time insights and alerts for your AI agents.

### Long Description
**Brizz is the analytics layer for AI agents, turning every conversation into actionable insights to improve performance and drive product decisions.**

The Brizz app for Slack brings these critical insights directly to where your team collaborates, ensuring you never miss a beat with your AI agent's behavior.

**How it works**
Once connected, Brizz monitors your AI agents in real-time and delivers high-signal notifications to your chosen channels. You can customize which alerts you receive to keep your product and engineering teams aligned.

**Key Features:**
*   **🚨 Real-time Issue Alerts**: Get instantly notified when your agent encounters critical errors or anomalies.
*   **📊 Daily & Weekly Summaries**: Receive automated reports on agent performance, conversation volume, and success rates.
*   **💡 Emerging Intents**: Be the first to know when users start asking about new topics or features your agent doesn't handle yet.
*   **🔗 Rich Link Previews**: Paste a link to a Brizz session or trace, and see a detailed preview including user intent, duration, and status without leaving Slack.
*   **⚡️ Quick Commands**: Use `/brizz status` to check system health instantly.

Connect Brizz to Slack today to accelerate feedback loops and make your AI agents more successful.

## Required Permissions

When you connect Slack, Brizz requests the following permissions:

| Permission          | Why We Need It                                                                                      |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| `channels:join`     | Automatically join public channels when you configure notifications, so the bot can post without manual `/invite` |
| `chat:write`        | Send daily reports, weekly summaries, emerging intent alerts, and issue notifications to channels   |
| `commands`          | Enable the `/brizz` slash command so your team can check status and access help directly in Slack   |
| `team:read`         | Identify your Slack workspace to ensure notifications are delivered to the correct organization     |
| `users:read`        | Display user names in notifications and activity reports for better context and identification      |
| `users:read.email`  | Match Slack users with their Brizz accounts to associate your workspace with your organization      |
| `app_mentions:read` | Allow Brizz to respond when mentioned in channels for interactive queries and commands (future)     |
| `incoming-webhook`  | Deliver webhook-based notifications for real-time alerts and automated messages to your channels    |
| `channels:read`     | List available public channels in your workspace so you can select where to receive notifications   |
| `groups:read`       | List private channels in your workspace so you can configure notification delivery to private groups|
| `links:read`        | Receive link_shared events when Brizz dashboard URLs are posted in Slack for rich link previews     |
| `links:write`       | Unfurl Brizz dashboard links with rich previews showing session, intent, and journey details         |

### Coming soon

These scopes power the new **Brizz AI agent in Slack** and will be requested once Slack approves the expanded app manifest. No action is needed today — when they ship, the integration card will prompt for a one-click reconnect.

| Permission          | Why We Need It                                                                                      |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| `assistant:write`   | Power the Brizz AI agent container — thread titles, status, and suggested prompts                  |
| `im:history`        | Read direct messages sent to the Brizz agent so it can reply with analytics insights               |

## Slash Commands

Once connected, you can use these commands in Slack:

- `/brizz` or `/brizz help` - Show available commands
- `/brizz status` - Check Brizz connection status

## Managing the Integration

To disconnect or reconfigure the Slack integration:

1. Go to **Settings > Integrations**
2. Find the Slack integration card
3. Click **Disconnect** or **Configure**

## See also

- [Jira](/docs/integrations/jira.md) — the other outbound issue integration.
- [Issues](/docs/platform/issues.md) — the source of what Slack alerts are notifying you about.


---

# Issue trackers

Connect Jira, Linear, or Monday so a Brizz issue becomes a ticket in the tracker your team already works from.

Brizz creates tickets in three issue trackers — **Jira**, **Linear**, and **Monday** — so a problem Brizz detected becomes a ticket in the tool your team plans work in, without retyping it. GitHub can also be connected, but it has no ticket actions on an issue yet; add a GitHub URL with **Add Link** instead.

## What connecting gets you

- **Create a ticket from a Brizz issue.** The ticket carries the issue's title and description. How much else travels with it varies by provider — Jira and Monday embed session context and a link back, while the Linear body ends with a generic link to Brizz rather than a deep link to the issue.
- **Attach an existing ticket.** If someone already filed it, link the two rather than creating a duplicate.
- **See the link from Brizz.** Linked tickets show as badges on the issue. Jira and Linear links also show the ticket's current status; a Monday link shows the item name only.
- **Automatic status handoff.** Linking a ticket — or adding any external link — moves the Brizz issue to **Tracked**, so your open queue reflects what's actually been picked up. See [Issues](/docs/platform/issues.md) for what the statuses mean.

## Connecting

1. Go to **Settings → Integrations**.
2. Find the tracker's card and click **Connect**.
3. Authorize Brizz in the provider's own consent screen, then return to Brizz.

Jira, Linear, and Monday connect over OAuth. When none of them is connected, the **Add Link** control on an issue also offers to set one up — it takes you to Settings to finish, and doesn't return you to the issue you started from. See [Jira](/docs/integrations/jira.md) for that integration's permission list and project setup.

To disconnect, use the same card and click **Disconnect**. Tickets you already created stay in the tracker; you just stop being able to create new ones, or to read Jira and Linear status from Brizz.

## How to act on it

1. **Connect the tracker your team actually plans in.** The value is that Tracked reflects reality — if the tickets live somewhere your team doesn't look, it doesn't.
2. **Attach rather than create when a ticket exists.** Two tickets for one issue split the discussion and neither shows the whole picture.
3. **Let Tracked do the triage bookkeeping.** Once linked, the issue leaves your untriaged queue automatically; you don't have to remember to change the status.

## Availability

Connecting and disconnecting integrations is an **Organization Admin** action, and it also depends on your plan — a connection attempt on a plan that doesn't include the integration is refused with an upgrade prompt.

## See also

- [Jira](/docs/integrations/jira.md) — the Jira integration in detail, including required permissions.
- [Issues](/docs/platform/issues.md) — what tickets get created from, and what the Tracked status means.
- [Slack](/docs/integrations/slack.md) — notifications rather than ticketing.
- [External links](/docs/instrument/external-links.md) — attach arbitrary links to sessions from your own code.


---

# Jira Integration

Connect Brizz to Jira to create and track issues found by Brizz directly from your Jira workspace.

Connect Brizz to your Jira Cloud workspace to create and track Jira tickets directly from Brizz issues.

## Why Connect Jira?

Integrating Jira with Brizz enables:

- **Create Issues in Jira from Issues in Brizz**: Turn Brizz issues into trackable Jira tickets with a single click
- **Read Linked Issues**: View Jira ticket status directly from the Brizz dashboard
- **Streamlined Workflows**: Bridge the gap between AI observability and your existing issue tracking

## How to Connect

1. Navigate to **Settings > Integrations** in the Brizz dashboard
2. Click **Connect Jira**
3. You'll be redirected to Atlassian to authorize Brizz
4. Select the Jira site you want to connect (if you have multiple)
5. Return to Brizz to complete the setup

## Required Permissions

When you connect Jira, Brizz requests the following permissions:

| Permission        | Why We Need It                                                                 |
| ----------------- | ------------------------------------------------------------------------------ |
| `read:jira-work`  | Read linked issues, projects, and boards to display Jira ticket status in Brizz |
| `read:jira-user`  | Read user information for displaying assignees and reporters on linked issues  |
| `write:jira-work` | Create Jira issues from Brizz issues with full context and details             |
| `manage:jira-webhook` | Register the webhooks that mirror Jira ticket status changes back to Brizz |
| `offline_access`  | Maintain the connection without requiring you to re-authorize frequently       |

## Creating Jira Issues

Once connected, you can create Jira issues from any Brizz issue:

1. Navigate to the **Issues** page in Brizz
2. Click on an issue to view its details
3. Click **Create Jira Issue**
4. Select the Jira project and issue type
5. Review and customize the issue details
6. Click **Create** to create the ticket in Jira

The Jira issue will include:
- Issue title and description from Brizz
- Link back to the Brizz issue for full context
- The affected sessions, linked back to Brizz

## Managing the Integration

To disconnect or reconfigure the Jira integration:

1. Go to **Settings > Integrations**
2. Find the Jira integration card
3. Click **Disconnect** to remove the integration

After disconnecting, any previously created Jira issues will remain in Jira, but you won't be able to create new issues or see linked ticket status in Brizz.

## See also

- [Issue trackers](/docs/integrations/issue-trackers.md) — Linear and Monday also create and attach tickets, with their own flows.
- [Slack](/docs/integrations/slack.md) — notifications rather than ticketing.
- [Issues](/docs/platform/issues.md) — the source of what Jira tickets get created from.


---

# Segment Integration

Send Segment events to Brizz for observability alongside your AI agent traces

[Segment](https://segment.com) is a Customer Data Platform (CDP) that collects analytics events from your apps and routes them to hundreds of tools. It provides a single API for tracking user actions — [track](https://segment.com/docs/connections/spec/track/), [identify](https://segment.com/docs/connections/spec/identify/), [page](https://segment.com/docs/connections/spec/page/), and [group](https://segment.com/docs/connections/spec/group/) calls — and delivers them to any connected destination.

By connecting Segment to Brizz, your product analytics events appear alongside AI agent traces in the same session timeline, giving you a complete picture of what your users do and how your agents respond.

## Why Connect Segment?

Integrating Segment with Brizz enables:

- **Unified Observability**: See Segment events alongside OpenTelemetry traces in the same session timeline
- **User Journey Context**: Attach product events (sign-ups, purchases, feature usage) to AI agent sessions
- **No Code Changes Required**: If you already use Segment, events flow to Brizz automatically via a destination

## Two ways to connect

Both routes deliver the same events to the same place — pick one.

| | [Destination Function](#setup-custom-destination-function) | [Webhook destination](#setup-webhook-destination) |
|---|---|---|
| What you configure | A JavaScript function you paste into Segment | Segment's built-in Webhooks destination, pointed at a URL Brizz gives you |
| Where the URL comes from | You supply Brizz's ingestion endpoint | Brizz generates it when you create the webhook |
| Authentication | A Brizz telemetry key, in the function's settings | An HMAC shared secret, or a Brizz telemetry key |
| Best for | Full control over the payload, custom transforms | Getting connected without writing code |

:::info
These are **inbound** webhooks — Segment pushing events *into* Brizz. They're unrelated to [outbound webhooks](/docs/api/webhooks.md), where Brizz calls an endpoint *you* own to tell you a session matured. Same word, opposite direction, separate configuration.
:::

## Setup: Custom Destination Function

A [Destination Function](https://segment.com/docs/connections/functions/destination-functions/) is a JavaScript function that runs inside Segment and forwards events to an external API.

### 1. Create a Destination Function

Navigate to **Catalog > Functions** in Segment.

![Navigate to Functions in the Segment catalog](/docs/guides/integration/segment/catalog_function.png)

Click **New Function** and select **Destination**.

![Create a new destination function](/docs/guides/integration/segment/new_function.png)

### 2. Add the Function Code

Replace the default code with the Brizz destination function below. Copy the entire code block and paste it into the function editor.

The function handles all four Segment event types (track, identify, page, group) and supports [batching](https://segment.com/docs/connections/functions/destination-functions/#batching-the-destination-function) for efficient delivery.

```javascript
// Brizz – Segment Custom Function (Destination)
// Settings: apiKey, serviceName (optional), environment (optional), baseUrl (optional)

const DEFAULT_BASE_URL = 'https://telemetry.brizz.dev'

// ── Helpers ──────────────────────────────────────────────────────────────────

const SEVERITY_MAP = {
  trace: 1, debug: 5, info: 9, warn: 13, warning: 13, error: 17, fatal: 21, critical: 21
}

function brizzAliases(field) {
  const parts = field.split('_')
  const camel = 'brizz' + parts.map(p => p[0].toUpperCase() + p.slice(1)).join('')
  const dot = 'brizz.' + parts.join('.')
  const underscore = 'brizz.' + field
  return [...new Set([camel, dot, underscore])]
}

function lookupBrizzField(props, field) {
  for (const key of brizzAliases(field)) {
    if (typeof props[key] === 'string' && props[key]) return props[key]
  }
  return undefined
}

function lookupBrizzSeverity(props) {
  for (const key of brizzAliases('severity_number')) {
    const val = props[key]
    if (typeof val === 'number' && val >= 0 && val <= 24) return val
  }
  const level = lookupBrizzField(props, 'severity')
  if (typeof level === 'string' && SEVERITY_MAP[level.toLowerCase()] !== undefined) {
    return SEVERITY_MAP[level.toLowerCase()]
  }
  return undefined
}

function flattenObject(obj, prefix) {
  const result = {}
  for (const [key, value] of Object.entries(obj)) {
    const fullKey = prefix ? `${prefix}.${key}` : key
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      Object.assign(result, flattenObject(value, fullKey))
    } else {
      result[fullKey] = value
    }
  }
  return result
}

function buildBrizzEvent(event, settings, eventName, eventType, body) {
  const attributes = flattenObject(event.context || {})

  if (event.userId) attributes['brizz.user_id'] = event.userId
  if (event.anonymousId) attributes['segment.anonymous_id'] = event.anonymousId
  if (event.messageId) attributes['segment.message_id'] = event.messageId
  attributes['segment.event_type'] = eventType

  const props = event.properties || event.traits || {}
  const severityNumber = lookupBrizzSeverity(props)

  const cleanBody = body && typeof body === 'object' && !Array.isArray(body)
    ? Object.fromEntries(Object.entries(body).filter(([k]) => !k.startsWith('brizz')))
    : body

  const result = {
    name: eventName,
    service_name: settings.serviceName || lookupBrizzField(props, 'service_name') || 'unknown',
    session_id: lookupBrizzField(props, 'session_id') || '',
    timestamp: event.timestamp || new Date().toISOString(),
    source: 'segment',
    environment: settings.environment || lookupBrizzField(props, 'environment'),
    attributes,
    body: cleanBody
  }

  if (severityNumber !== undefined) {
    result.severity_number = severityNumber
  }

  return result
}

async function sendToBrizz(brizzEvent, settings) {
  const baseUrl = settings.baseUrl || DEFAULT_BASE_URL
  const endpoint = `${baseUrl}/raw/events`

  let response
  try {
    response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${settings.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(brizzEvent)
    })
  } catch (error) {
    const message =
      error && typeof error.message === 'string' ? error.message : String(error)
    throw new RetryError(`Network error while calling ${endpoint}: ${message}`)
  }

  if (response.status >= 500 || response.status === 429) {
    throw new RetryError(`Failed with ${response.status}`)
  }

  if (response.status === 401 || response.status === 403) {
    throw new ValidationError('Invalid Brizz API key (401 Unauthorized)')
  }

  if (response.status >= 400) {
    const body = await response.text()
    throw new ValidationError(`Request failed with ${response.status}: ${body}`)
  }
}

function mapEvent(event, settings) {
  switch (event.type) {
    case 'track':
      return buildBrizzEvent(event, settings, event.event, 'track', event.properties || {})
    case 'identify':
      return buildBrizzEvent(event, settings, 'identify', 'identify', event.traits || {})
    case 'group':
      return buildBrizzEvent(event, settings, 'group', 'group', { group_id: event.groupId, ...(event.traits || {}) })
    case 'page': {
      const pageName = event.name ? `page.${event.name}` : 'page_view'
      return buildBrizzEvent(event, settings, pageName, 'page', event.properties || {})
    }
    default:
      return null
  }
}

// ── Event Handlers ───────────────────────────────────────────────────────────

async function onBatch(events, settings) {
  const brizzEvents = events.map(e => mapEvent(e, settings)).filter(Boolean)
  if (brizzEvents.length === 0) return
  await sendToBrizz(brizzEvents, settings)
}

async function onTrack(event, settings) {
  await sendToBrizz(mapEvent(event, settings), settings)
}

async function onIdentify(event, settings) {
  await sendToBrizz(mapEvent(event, settings), settings)
}

async function onGroup(event, settings) {
  await sendToBrizz(mapEvent(event, settings), settings)
}

async function onPage(event, settings) {
  await sendToBrizz(mapEvent(event, settings), settings)
}

async function onScreen(event, settings) {
  throw new EventNotSupported(
    'screen events are not currently mapped to Brizz telemetry. Consider using page events instead.'
  )
}

async function onAlias(event, settings) {
  throw new EventNotSupported(
    'alias events are not currently mapped to Brizz telemetry. Consider using identify events instead.'
  )
}

async function onDelete(event, settings) {
  throw new EventNotSupported(
    'delete events are not currently mapped to Brizz telemetry. Handle user deletion via your data privacy process outside of Brizz.'
  )
}
```

### 3. Configure Settings

Click the **Settings** tab and add the following fields:

![Function settings tab](/docs/guides/integration/segment/settings.png)

| Setting         | Type   | Required | Description                                                                 |
| --------------- | ------ | -------- | --------------------------------------------------------------------------- |
| `apiKey`        | String | **Yes**  | Your Brizz API key (found in **Settings > API Keys** in the Brizz dashboard) |
| `serviceName`   | String | No       | Static application name. Can also be sent per-event via `brizzServiceName`.  |
| `environment`   | String | No       | Deployment environment (e.g. `production`). Can also be sent via `brizzEnvironment`. |
| `baseUrl`       | String | No       | Override the telemetry endpoint. Defaults to `https://telemetry.brizz.dev`. |

Add the **API Key** setting:

![Add API Key setting](/docs/guides/integration/segment/add_settings_api_key.png)

Optionally add **Service Name**:

![Add Service Name setting](/docs/guides/integration/segment/add_settings_service_name.png)

Review your final settings:

![Final settings overview](/docs/guides/integration/segment/final_settings.png)

### 4. Name and Create the Function

Give your function a name (e.g. "Brizz") and click **Create Function**.

![Name and create the function](/docs/guides/integration/segment/configure_function.png)

### 5. Connect a Source

Back in the Segment catalog, find your new Brizz destination and click **Connect Destination**.

![Connect destination from catalog](/docs/guides/integration/segment/connect_destination.png)

Select the source you want to send events from:

![Select source](/docs/guides/integration/segment/connect_source.png)

### 6. Enable the Destination

Fill in the destination settings (API key, etc.) and **enable** the destination using the toggle.

![Destination settings and enable toggle](/docs/guides/integration/segment/destination_settings.png)

### 7. Verify Events

Open the **Event Tester** tab to confirm events are flowing. It may take a couple of minutes on the first run.

![Event tester showing successful delivery](/docs/guides/integration/segment/event_tester.png)

Verify that events show as **successfully delivered**:

![Final verification of event delivery](/docs/guides/integration/segment/final_verify.png)

Once delivered, events appear in the Brizz session timeline:

![Events visible in a Brizz session](/docs/guides/integration/segment/brizz_session.png)

---

## Setup: Webhook destination

Instead of running a function, you can point Segment's built-in **Webhooks** destination at an endpoint Brizz generates for you. Brizz walks you through this in the product, with screenshots for each Segment screen — start from **Integrations > Segment > Connect**, and the wizard supplies the URL and credentials as you go.

The shape of it:

1. **Create the webhook in Brizz** and choose how Segment will authenticate:
   - **Shared secret (HMAC)** — Brizz generates a signing secret. Paste it into the **Shared Secret** field of the Segment destination. Shown once.
   - **Telemetry key** — reuse a Brizz [telemetry key](/docs/admin/api-keys.md). You'll send it as an `Authorization: Bearer <key>` header in the mapping.
2. **Add the Webhooks destination in Segment** and pick the source that will send events to it.
3. **Add a mapping**, set streaming behavior to **Send**, and choose which event types trigger it (Track, Identify, Page, Group).
4. **Map the fields**: set the mapping's **URL** to the webhook endpoint Brizz gave you. If you chose telemetry key auth, add the `Authorization` header here too.
5. **Send a test record** from Segment and confirm it arrives.

Manage or remove these webhooks later under **Integrations > Segment**.

## Sending Brizz Metadata in Events

Brizz extracts special fields from event [properties](https://segment.com/docs/connections/spec/track/#properties) (or [traits](https://segment.com/docs/connections/spec/identify/#traits) for identify/group) using a naming convention. All fields accept multiple formats:

| Field              | Accepted Property Keys                                              | Description                          |
| ------------------ | ------------------------------------------------------------------- | ------------------------------------ |
| `session_id`       | `brizzSessionId`, `brizz.session.id`, `brizz.session_id`           | Links the event to a Brizz session   |
| `service_name`     | `brizzServiceName`, `brizz.service.name`, `brizz.service_name`     | Application name (settings override) |
| `environment`      | `brizzEnvironment`, `brizz.environment`                             | Deployment environment (settings override) |
| `severity_number`  | `brizzSeverityNumber`, `brizz.severity.number`, `brizz.severity_number` | OTel severity 0-24              |
| `severity` (text)  | `brizzSeverity`, `brizz.severity`                                   | String level (e.g. `"error"` maps to 17) |

:::tip
To correlate Segment events with Brizz traces, send the same `session_id` you use in the Brizz SDK. See [Sessions](/docs/instrument/sessions.md) for details.
:::

**Example:**

:::tabs
:::tab[Python]
```python
import segment.analytics as analytics

analytics.track(
    user_id="user-123",
    event="Order Completed",
    properties={
        "revenue": 99.99,
        "brizzSessionId": session_id,
        "brizzServiceName": "my-app",
        "brizzEnvironment": "production",
    },
)
```
:::tab[Node.js]
```typescript
analytics.track({
  userId: 'user-123',
  event: 'Order Completed',
  properties: {
    revenue: 99.99,
    brizzSessionId: sessionId,
    brizzServiceName: 'my-app',
    brizzEnvironment: 'production',
  },
});
```
:::

All properties that don't start with `brizz` are passed through as-is into the Brizz event body. Brizz-prefixed properties are consumed as metadata and stripped from the body.

## Supported Event Types

| Segment Type | Brizz Event Name                    | Body              |
| ------------ | ----------------------------------- | ----------------- |
| `track`      | Event name (e.g. "Order Completed") | `properties`      |
| `identify`   | `"identify"`                        | `traits`          |
| `page`       | `"page.{name}"` or `"page_view"`   | `properties`      |
| `group`      | `"group"`                           | `{ group_id, ...traits }` |

Batching is supported — when Segment sends a batch of events, they are forwarded to Brizz in a single API call.

## Managing the Integration

To disconnect or reconfigure the Segment integration:

1. Go to **Connections > Destinations** in the Segment dashboard
2. Find your Brizz destination function
3. Disable the toggle or delete the destination

Events already delivered to Brizz will remain in your telemetry history.

## References

- [Segment Spec Overview](https://segment.com/docs/connections/spec/) — the full event specification (track, identify, page, group, common fields)
- [Segment Common Fields](https://segment.com/docs/connections/spec/common/) — context, timestamps, and fields shared by all event types
- [Destination Functions](https://segment.com/docs/connections/functions/destination-functions/) — how Segment custom destination functions work
- [Destination Functions: Batching](https://segment.com/docs/connections/functions/destination-functions/#batching-the-destination-function) — batch handler configuration
- [Functions Editing Environment](https://segment.com/docs/connections/functions/environment/) — testing and deploying functions in Segment
- [Adding a Destination](https://segment.com/docs/connections/destinations/add-destination/) — general guide for connecting sources to destinations
- [Brizz Sessions](/docs/instrument/sessions.md) — how to use session IDs to correlate events with traces
- [Brizz Custom events](/docs/instrument/custom-events.md) — emitting custom events from the Brizz SDK

## See also

- [Custom events](/docs/instrument/custom-events.md) — naming conventions and attributes/body distinction.
- [Sessions](/docs/instrument/sessions.md) — make Segment events line up with the right session.
- [Slack](/docs/integrations/slack.md) and [Jira](/docs/integrations/jira.md) — outbound integrations for issues found in this data.
- [Outbound webhooks](/docs/api/webhooks.md) — the other direction: Brizz calling your endpoint when a session matures.


---

# MCP server (connect Brizz to your AI tools)

Expose Brizz as an MCP server so Claude Code, Claude Desktop, Cursor, Codex, and other AI agents can query your data.

This page is about **connecting Brizz to your AI agent** so it can query Brizz on your behalf. If instead you want to **add Brizz observability to an MCP server you operate**, see [MCP (auto-instrument your server)](/docs/sdks/mcp.md).

Talk to your Brizz data from inside Claude Code, Claude Desktop, Cursor, Codex, and any other AI agent that speaks the Model Context Protocol. Ask things like "what issues showed up today?", "summarize the worst session", or "which services are slowest" — your agent calls Brizz directly and answers from real data.

You sign in once with your Brizz account — no API key to copy, store, or rotate. If your client can't complete an interactive sign-in (headless setups, scripts), use a [Platform API key](#platform-api-key) instead.

## Server URL

```
https://platform.brizz.dev/mcp
```

Paste this URL (or the snippet for your agent below) into your AI tool. The first time the agent uses a Brizz tool, your browser opens to sign you in — same login you use for the dashboard.

## Set up your agent

Pick your client below for the interactive sign-in flow. For a headless client, script, or CI that can't open a browser, use a [Platform API key](#platform-api-key) instead.

:::tabs
:::tab[Claude Code]
Run this from your terminal — any directory works.

```bash
claude mcp add --transport http brizz https://platform.brizz.dev/mcp
```

Type `/mcp` inside Claude Code anytime to manage the connection or sign in again.
:::tab[Claude Desktop]
Open Claude Desktop's config file and add the snippet below, then restart the app.

- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "brizz": {
      "url": "https://platform.brizz.dev/mcp"
    }
  }
}
```
:::tab[Cursor]
Add the snippet to `~/.cursor/mcp.json` for global access, or to `.cursor/mcp.json` inside a project to scope it. Cursor will prompt you to sign in the first time the agent calls Brizz.

```json
{
  "mcpServers": {
    "brizz": {
      "url": "https://platform.brizz.dev/mcp"
    }
  }
}
```
:::tab[Codex / VS Code / Cline]
Run the Codex CLI command — the same JSON shape works for VS Code Continue and Cline too.

```bash
codex mcp add brizz --url https://platform.brizz.dev/mcp
```

```json
{
  "mcpServers": {
    "brizz": {
      "transport": "http",
      "url": "https://platform.brizz.dev/mcp"
    }
  }
}
```
:::tab[Other clients]
Any MCP client that supports streamable HTTP can connect — just point it at the server URL above.

If your client needs a metadata URL for discovery, use:

```
https://platform.brizz.dev/.well-known/oauth-protected-resource
```
:::

## Authentication

Your agent connects with your own Brizz identity: tools run with your role and your tenant, nothing is exposed across tenants, and the agent never sees your password. Pick whichever method your client supports.

### Interactive sign-in (default)

Interactive clients — Claude Code, Claude Desktop, Cursor, Codex — sign you in through your browser the first time the agent calls a Brizz tool, using the same login as the dashboard. There's no API key to copy, store, or rotate; tokens are short-lived and refreshed automatically by your client. Sign out of Brizz anytime to invalidate the session.

### Platform API key

Headless clients, scripts, and CI that can't complete an interactive sign-in can authenticate with a **Platform API key** instead.

1. Create a key under [**User Settings > Platform API Keys**](/app/settings/platform-api-keys). Copy it — it's shown once.
2. Pass it as a bearer token when connecting:

```bash
claude mcp add --transport http brizz https://platform.brizz.dev/mcp --header "Authorization: Bearer <key>"
```

For clients configured via JSON, add the header alongside the URL:

```json
{
  "mcpServers": {
    "brizz": {
      "url": "https://platform.brizz.dev/mcp",
      "headers": {
        "Authorization": "Bearer <key>"
      }
    }
  }
}
```

The key acts with your role and tenant, same as signing in interactively. Revoke it anytime from [**User Settings > Platform API Keys**](/app/settings/platform-api-keys).

## What your agent can do

Your agent sees every Brizz tool by name and picks the one that fits — no setup, no discovery step. Modern clients load tool definitions only when they're used, so a full tool list costs nothing until it's needed.

If your token is read-only, the tools that change data aren't listed at all — the agent can browse and analyze, but not modify.

| Capability | What it does |
| --- | --- |
| Confirm identity | Which user, tenant, and role the agent is acting as. |
| Search & read sessions | Find conversation sessions by filter, fetch one by id, or pull every turn (messages + tool calls) of a conversation. |
| Search & manage issues | Find quality issues by status / priority / severity, fetch one by id, or change its status, priority, or assignment. |
| Query metrics | Return a time-bucketed metric series to chart. |
| Explore organizations | List your organizations and get a health-and-activity summary. |
| Discover what's filterable | The metric catalog, issue enums, intent labels, custom-property keys (e.g. `plan_tier`, `account_id`), and the fields each search accepts. |
| List services | The monitored trace services for your tenant. |
| Multi-turn analysis | Continue an analytics conversation with the Brizz agent across turns. |
| Ask the analyst | Hand an open-ended "why" question to the Brizz analytics agent when no direct tool answers it. |
| Follow a runbook | Load a step-by-step investigation guide — diagnose a session, root-cause an issue, triage a service, chase a cost spike. |

For the full reference — every tool, its arguments, and its allowed values — see [MCP server tools](/docs/integrations/mcp-server/tools.md).

## Investigate an issue from the dashboard

Once your agent is connected, you don't have to write the investigation prompt yourself. On any issue, open **Fix with your agent** and pick the **Brizz MCP** tab. It shows the `brizz_investigate_issue` call for that issue with `service_name` and `issue_id` already filled in — copy it, paste it into your agent, and the agent fetches the handoff document itself.

Anything you type into **Add context for the agent** rides along as the call's `notes` argument, so a hypothesis or a user complaint reaches the investigation without a second message.

The document your agent receives ends in `next-tool-calls` blocks — follow-up MCP calls already scoped to that issue — rather than the shell commands the Manual and Brizz CLI tabs hand out.

If your agent replies that it has no Brizz tools, expand **Not connected to Brizz MCP yet?** in the same tab for the server URL, or finish setup under [**Settings > Integrations**](/app/settings/integrations).

## Troubleshooting

**The agent says it can't reach Brizz.**
Check that you ran the setup command for *your* Brizz workspace — the URL above is generated for the workspace you're currently signed into.

**Sign-in popup never appears.**
Some clients only sign you in the first time a tool runs. Try asking the agent something simple like *"list my Brizz services"* to trigger the flow.

**I want to disconnect.**
Remove the `brizz` entry from your client's MCP config (or run the client's equivalent of `mcp remove brizz`). You can also sign out of Brizz in the dashboard to immediately invalidate the connection.

## See also

- [MCP server tools](/docs/integrations/mcp-server/tools.md) — the full tool reference: every tool, its arguments, and its allowed values.
- [MCP (auto-instrument your server)](/docs/sdks/mcp.md) — the *other* MCP page: instrumenting an MCP server you operate, not connecting agents to Brizz.
- [API keys](/docs/admin/api-keys.md) — the telemetry API keys used to ingest data into Brizz, distinct from the Platform API keys used above to connect the MCP server.
- [Troubleshooting](/docs/help/troubleshooting.md) — sign-in issues and connection diagnostics.


---

# MCP server tools

Every tool the Brizz MCP server exposes — name, what it does, required scope, and full arguments — in one page.

The complete tool reference for the Brizz MCP server. To connect your agent in the first place, see [MCP server](/docs/integrations/mcp-server/overview.md).

## How your agent finds a tool

Every Brizz tool is listed directly — there is no discovery step. Your agent
picks a tool by name from its own tool list and calls it:

```json
{ "tool": "brizz_search_sessions", "arguments": { "limit": 20 } }
```

Modern clients (Claude Code, Claude Desktop, VS Code Copilot) load tool
definitions on demand and search them by name, description, and argument
names, so a long list costs no context until a tool is actually used.

Reach for `brizz_ask_agent` only when no typed tool answers the question — open-ended diagnostics like *"why are users frustrated?"* or *"what's regressing?"*, or when someone explicitly asks for the analyst. For a known object, a filtered search, or a raw metric series, the typed tools are faster and cheaper.

Not sure where to start on an open-ended investigation? Call `brizz_list_skills` for the runbooks this server ships (diagnose a session, root-cause an issue, triage a service, analyze tool failures, chase a cost spike, reproduce a user complaint), then `brizz_get_skill` to load one.

## All tools at a glance

All 34 tools. Tools marked **write** need the `mcp:write` scope; everything else needs `mcp:read`. A token without `mcp:write` is not shown the write tools at all.

| Tool | What it does | Scope |
| --- | --- | --- |
| `brizz_ask_agent` | Ask the analytics agent an open-ended question. | **write** |
| `brizz_list_skills` | The investigation runbooks this server ships, with when to use each. | read |
| `brizz_get_skill` | One runbook's full step-by-step instructions. | read |
| `brizz_whoami` | The user, tenant, scopes, and visible services behind the current token. | read |
| `brizz_describe_schema` | What's filterable in this tenant: metric catalog, issue enums, intent labels, property names. | read |
| `brizz_list_services` | The monitored services for your tenant, default first. | read |
| `brizz_list_session_filters` | The fields you can filter sessions by, and the operators each accepts. | read |
| `brizz_list_custom_properties` | Service-defined property keys such as `plan_tier` or `account_id`. | read |
| `brizz_search_sessions` | Find sessions by time window, organization, or filter conditions. | read |
| `brizz_get_session` | One session's metadata by id — no transcript. | read |
| `brizz_get_conversation` | One session's full transcript: messages and tool calls. | read |
| `brizz_get_session_spans` | One session's raw OTel spans — attributes, status, timing — for deep loop/error reconstruction. | read |
| `brizz_get_conversation_item` | One conversation turn's complete content: message, tool input/result, reasoning, error detail, span attributes. | read |
| `brizz_get_session_tool_schemas` | The tool definitions a session's calls resolved to, as the model saw them at call time. | read |
| `brizz_aggregated_data_for_sessions` | Count, average, and median duration over every session matching a filter. | read |
| `brizz_aggregate_sessions` | Grouped distribution (count or custom metrics) over sessions matching a filter, partitioned by one or more dimensions. | read |
| `brizz_aggregate_tool_calls` | Grouped distribution over individual tool calls — rank tools by call volume, or compute a per-call success rate. | read |
| `brizz_list_event_names` | The custom event names a service emits, with occurrence counts. | read |
| `brizz_get_sessions_events` | Event payloads for a set of known session ids. | read |
| `brizz_get_sessions_first_user_messages` | The opening user message for a set of known session ids. | read |
| `brizz_get_session_errors` | The errors recorded across a set of known session ids, in one query. | read |
| `brizz_search_issues` | Find issues by status, priority, severity, org, journey, or user. | read |
| `brizz_get_issue` | One issue's detail, evidence sessions, and activity log. | read |
| `brizz_investigate_issue` | The curated "Fix with your agent" handoff document — concept guidance, agent setup, whether a system prompt was captured, per-finding evidence, and windowed conversation snippets. | read |
| `brizz_get_issue_findings` | The per-finding evidence rows behind an issue: reasoning, error type, analyzer metadata, session/span pointers. | read |
| `brizz_get_issue_prompt` | The full captured system prompt(s) for an issue, one per agent. | read |
| `brizz_update_issue` | Change an issue's status, priority, assignee, or title. | **write** |
| `brizz_query_metric` | A time-bucketed series for cost, sessions, active users, errors, or issues. | read |
| `brizz_list_organizations` | Organizations ranked by activity, with usage counters. | read |
| `brizz_get_organization_overview` | A consolidated brief for one organization, or tenant-wide. | read |
| `brizz_get_user_usage` | One user's sessions, active dates, durations, journeys, and opening prompts. | read |
| `brizz_continue_agent_conversation` | Start or continue an analyst conversation across turns. | **write** |
| `brizz_list_reports` | The CSV/JSON report exports a user has saved for a service. | read |
| `brizz_download_report` | One saved report's contents, inline and bounded. | read |

## Scopes

Every tool requires either `mcp:read` or `mcp:write`. Read tools are available to any connected token; write tools are only listed to a token that carries `mcp:write`, and are refused at call time without it.

Three tools require `mcp:write`: `brizz_update_issue`, `brizz_ask_agent`, and `brizz_continue_agent_conversation`. A read-only token never sees them in its tool list — so an agent connected read-only can browse and analyze your data but cannot change it or spend an analyst turn.

## Service scoping

Most tools accept an optional `service_name`. Leave it out and the server resolves it for you — your configured default service, your only service, or the most recently active one. Pass it explicitly only when you want a specific one, and call `brizz_list_services` when the server reports a "multiple services" ambiguity or you want to compare across services.

## Working with ids

Session, trace, span, issue, cluster, journey, and label ids are opaque. Pass them back verbatim — never abbreviate, truncate, or reconstruct one. They're fixed width: session and trace ids are 32 hex characters, span ids are 16, and issue/cluster/journey/label ids are 36-character UUIDs.

## Identity and schema

Two orientation tools. Both are cheap — they run no heavy queries — and both are worth calling once at the start of a session so the agent works from real values instead of guesses.

### `brizz_whoami`

Returns the identity behind the current token: user, tenant (workspace), granted scopes, and the services the token can see. Takes no arguments.

```json
{ "tool": "brizz_whoami", "arguments": {} }
```

### `brizz_describe_schema`

Describes what's available for filtering, grouping, and querying in the tenant: the metric catalog, issue priority/severity/status enums, the top intent labels in use, available custom-property names with sample values, the resolved organization-identifier property, journeys, and example queries.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Scope custom-property samples and intents to one service. |

## Sessions and conversations

Finding conversation sessions and reading what happened inside them. The usual path is discover filters, search, then drill in:

`brizz_list_session_filters` → `brizz_search_sessions` → `brizz_get_conversation`

### `brizz_list_services`

Lists the telemetry services (apps) monitored for the tenant. Returns each service's id, name, `is_default`, `last_event_at`, and `session_count_7d`, default service first. Takes no arguments.

Call this only when the server returns a "multiple services" ambiguity error, or when someone wants to compare services.

### `brizz_list_session_filters`

Lists the fields you can filter sessions by, with the operators and value shape each accepts — tool calls, user, journey, metrics, intents, issues, labels, cohorts, and more.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Scope to one service so per-service dynamic filters (e.g. system-event values) are included. |

### `brizz_list_custom_properties`

Lists the custom-property keys attached to a service's sessions — e.g. `plan_tier`, `account_id`, `environment`. These are service-defined, so the list is dynamic and paginated.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `limit` | integer | No | Keys per page. Default 100, max 500. |
| `offset` | integer | No | Pagination offset, default 0. The response reports the next offset when more keys remain. |

To filter by a returned key, pass it as a condition `field` to `brizz_search_sessions` with `filter_type` set to `custom_properties`.

### `brizz_search_sessions`

Searches conversation sessions for one service. Returns a paginated, sorted list with high-level metadata — no transcripts.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `organization_id` | string | No | Restrict to one organization. Uses a dedicated endpoint and **cannot be combined with `filters`**. |
| `filters` | array | No | Filter conditions, all AND-ed together. See the condition shape below. |
| `session_ids` | array of strings | No | Restrict the search to a known set of session ids, verbatim. Bounds content scans. Max 500. |
| `time_range` | string | No | Shorthand window like `24h`, `7d`, `30d`. Default `7d`, max `90d`. Overridden by `start`/`end`. |
| `start` | string | No | Inclusive RFC3339 start. Overrides `time_range`. |
| `end` | string | No | Exclusive RFC3339 end. Overrides `time_range`. |
| `limit` | integer | No | Default 50, max 200. |
| `offset` | integer | No | Default 0. |
| `order_by` | string | No | `start` (default), `end`, or `duration`. |
| `order_dir` | string | No | `asc` or `desc`. Default `desc`. |
| `response_format` | string | No | `concise` (default, top 10), `detailed` (up to `limit`), or `raw` (structured content only). |

#### Filter conditions

Each entry in `filters` is an object:

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `field` | string | Yes | A field from `brizz_list_session_filters`, e.g. `toolName`, `userId`, `journeyId`. |
| `operator` | string | Yes | An operator that field accepts, e.g. `equals`, `contains`, `contains_all`, `contains_any`, `gt`, `exists`. |
| `value` | any | No | Multi-value operators (`contains_all`, `contains_any`, `include`, `exclude`) take a comma-separated string such as `"bash,read_file"`. `contains`/`not_contains` take a string or array of strings. Other operators take a single scalar. Ignored for `exists`. |
| `filter_type` | string | No | Copied verbatim from the field's `filterType` column. Required for custom-property fields — set it to `custom_properties`. Omit for builtin fields. |

```json
{
  "tool": "brizz_search_sessions",
  "arguments": {
    "time_range": "7d",
    "limit": 20,
    "filters": [
      { "field": "toolName", "operator": "contains_any", "value": "bash,read_file" },
      { "field": "plan_tier", "operator": "equals", "value": "enterprise", "filter_type": "custom_properties" }
    ]
  }
}
```

### `brizz_get_session`

Fetches metadata for one session by id: start/end, user, journey, model, cost, outcome. No transcript.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `session_id` | string | Yes | Full session id, verbatim as returned by `brizz_search_sessions`. |

### `brizz_get_conversation`

Fetches the full transcript for one session — user and assistant messages plus tool calls. Applies the same smart-display filter as the dashboard: prefers display variants over raw items, drops internal tool-call bookkeeping, and keeps interrupts and errors. Conversation content is escaped before rendering, so adversarial session text can't inject formatting.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `session_id` | string | Yes | Full session id, verbatim. |
| `limit` | integer | No | Max turns. Default 200, max 200. |
| `offset` | integer | No | Pagination offset for long transcripts. |

### `brizz_get_session_spans`

Returns one session's **raw spans** as structured JSON — the underlying OpenTelemetry records, not the filtered conversation items `brizz_get_conversation` returns. Each span carries its attributes (tool-call arguments and results, model metadata), status code and message, span kind, parent id, and timing. Use it when you need to reconstruct exactly what happened inside a session — a loop that never terminated, a tool that kept erroring — where the smart-display transcript drops the evidence. A span's attributes are bounded (50 keys, 2000 characters per value), so a very large tool payload may be truncated.

Each call returns one 50-span page in ingestion order. Walk a long session by paging with `offset` (0, 50, 100, …) until the `returned` count drops below 50, and sort a page by `timestamp` to reconstruct execution sequence — ingestion order is not wall-clock order.

Note: OpenTelemetry span events, such as exception stack traces, are not included; the status code, status message, and span attributes cover most error cases.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `session_id` | string | Yes | Full session id, verbatim. |
| `offset` | integer | No | Pagination offset; page in 50-span windows until fewer than 50 come back. |

## Session events and cohorts

These work on top of a set of sessions you've already found: quantify the cohort, or pull per-session detail for known ids.

### `brizz_aggregated_data_for_sessions`

Returns aggregate statistics over the **full** set of sessions matching a filter — session count, average duration, and median duration. Use it to answer "how many sessions match X" without fetching every row.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `filters` | array | No | Same condition shape as `brizz_search_sessions`. |
| `time_range` | string | No | Shorthand like `24h`, `7d`, `30d`. Default `7d`, max `90d`. Overridden by `start`/`end`. |
| `start` | string | No | Inclusive RFC3339 start. |
| `end` | string | No | Exclusive RFC3339 end. |

Because it aggregates server-side, the count reflects every matching session — not just the page `brizz_search_sessions` returned.

### `brizz_aggregate_sessions`

Returns a grouped distribution over the sessions matching a filter: partition by one or more dimensions and get a count — or custom metrics — per group. Use it to answer "what's the distribution of X across these sessions" (for example, count of sessions by model, scoped to an issue) in one call.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `filters` | array | No | Same condition shape as `brizz_search_sessions` — e.g. scope to an issue. |
| `group_by` | array of strings | Yes | Dimensions to partition by: `model`, `date`, `hour`, `day`, `week`, `month`, `hasError`, `toolName`, `toolFailed`, `hasToolError`, `terminalErrorClass`, `repeatedToolSignature`, `distinctToolCount`, and more. |
| `metrics` | array | No | Metric aggregations, each `{function, field, alias?}`. Defaults to session count. Functions: `count`, `sum`, `avg`, `min`, `max`, `p50`, `p90`, `p95`, `p99`. Fields: `cost`, `duration`, `durationSeconds`, `totalTokens`, `promptTokens`, `completionTokens`, `spanCount`, `traceCount`, or `*` for count. |
| `having` | array | No | Post-aggregation filters on a metric alias, each `{metric, operator, value}`. Operators: `gt`, `gte`, `lt`, `lte`, `equals`. |
| `order_by` | string | No | Metric alias or group dimension to sort by. Defaults to the session count. |
| `order_dir` | string | No | `asc` or `desc`. Default `desc`. |
| `limit` | integer | No | Max groups. Default 100, max 1000. |
| `time_range` | string | No | Shorthand like `24h`, `7d`, `30d`. Default `7d`, max `90d`. Overridden by `start`/`end`. |
| `start` | string | No | Inclusive RFC3339 start. |
| `end` | string | No | Exclusive RFC3339 end. |

It aggregates server-side, so it answers "distribution across all matching sessions" without paging through rows. Example: count sessions by model within one issue.

```json
{
  "tool": "brizz_aggregate_sessions",
  "arguments": {
    "group_by": ["model"],
    "filters": [{ "field": "issue", "operator": "equals", "value": "<issueId>" }]
  }
}
```

Returns one row per group, e.g. `[{model, count}, ...]`.

Every row counts **sessions**, so a session that called one tool five times counts once. To count the calls themselves, use `brizz_aggregate_tool_calls`.

For a per-tool failure rate, group by `toolName` and `toolFailed` — `toolFailed` says whether *that* tool failed in that session. `hasToolError` is session-wide ("failed at something, anywhere"), so `["toolName", "hasToolError"]` answers the different question "sessions that called X **and** failed at anything" and overcounts X's failures.

### `brizz_aggregate_tool_calls`

The per-call counterpart to `brizz_aggregate_sessions`: it returns a grouped distribution over **individual tool calls**. Use it to rank an agent's tools by call volume, compute a real per-call success or error rate, or reconcile a tool-call count with another system.

Counts are tool *executions*. The request rows some SDKs emit alongside an execution are excluded, so nothing is double-counted — pass a `callType` filter selecting `assistant_tool_call` to opt back in.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `filters` | array | No | Conditions on `toolName`, `toolOutcome`, `errorCategory`, `agentName`, `callType`, `sessionId`, `skillKey`. Operators: `equals`, `not_equals`, `contains`, `not_contains`. Omit `filter_type` or set it to `tool_calls` — custom-property filters are rejected on this source. |
| `group_by` | array of strings | Yes | Dimensions to partition by. Time: `date`, `hour`, `day`, `week`, `month`, `days:N`. Call: `toolName`, `toolOutcome`, `errorCategory`, `agentName`, `callType`, `skillKey`. Dynamic: `customProperty:<key>`. `sessionId` is filter-only. |
| `metrics` | array | No | Each `{function, field, alias?}`. Defaults to a call count. Fields: `*` (count), `duration` (nanoseconds), `durationSeconds`. `toolCount` takes function `distinct` and returns distinct tool names. |
| `having` | array | No | Post-aggregation filters on a metric alias, each `{metric, operator, value}`. |
| `order_by` | string | No | Metric alias or group dimension. Defaults to the call count. |
| `order_dir` | string | No | `asc` or `desc`. Default `desc`. |
| `limit` | integer | No | Max groups. Default 100, max 1000. |
| `time_range` | string | No | Shorthand like `24h`, `7d`, `30d`. Default `7d`, max `90d`. Overridden by `start`/`end`. |
| `start` | string | No | Inclusive RFC3339 start. |
| `end` | string | No | Exclusive RFC3339 end. |

Grouped by `toolOutcome` you get three buckets: `success`, `error`, and `unknown` — the last meaning the SDK emitted no outcome marker. Treat `unknown` as unmeasured rather than folding it into either side.

`unknown` is a **group-by bucket only**, synthesized from an empty stored value. As a *filter* value it matches nothing and returns zero rows without an error, so filter on `success` or `error` and read the `unknown` share off a group-by instead.

```json
{
  "tool": "brizz_aggregate_tool_calls",
  "arguments": {
    "group_by": ["toolName", "toolOutcome"],
    "time_range": "30d"
  }
}
```

This ranks and counts. To read the actual failure messages behind a slice, take the sessions behind it (`brizz_search_sessions` with a `toolName` filter) into `brizz_get_session_errors`.

Note a detection change on 2026-07-27: tool errors recorded before that date are substantially under-counted, so a window straddling it shows a jump that reflects improved detection, not a regression.

### `brizz_list_event_names`

Lists the event names a service emits — custom signals like onboarding, integration, or feedback events — with occurrence counts.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `limit` | integer | No | Default 100, max 500. |

### `brizz_get_sessions_events`

Fetches named event payloads for a set of known session ids. Returns a map of session id to `{event_name: latest_payload}`.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `session_ids` | array of strings | Yes | Session ids, verbatim. Must not be empty. |
| `event_names` | array of strings | No | Specific names from `brizz_list_event_names`. Pass `["*"]` or omit for all events. |

```json
{
  "tool": "brizz_get_sessions_events",
  "arguments": {
    "session_ids": ["3023b8d4917218792959925606c29f37"],
    "event_names": ["feedback"]
  }
}
```

### `brizz_get_sessions_first_user_messages`

Fetches the opening user message for a set of known session ids. Returns a map of session id to message text.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `session_ids` | array of strings | Yes | Session ids, verbatim. Must not be empty. |

### `brizz_get_session_errors`

Fetches every error recorded across a set of known session ids — up to 50 per call, in one query. Use it to check whether a cohort shares a failure instead of pulling each session's transcript; one call costs about what a single `brizz_get_conversation` fetch does.

Two surfaces carry a failure, and they barely overlap, so both are returned and tagged with `source`:

- `error_item` — a standalone error recorded on the session.
- `tool_call` — a tool call whose outcome was an error.

Each error carries its type, category, message, stacktrace, span name, and the tool names called just before it (`preceding_tools`). Sessions with no errors come back listed as checked-and-clean, so "clean" never reads as "not checked".

Two flags mark what a response cannot tell you, rather than letting an absence stand in for an answer:

- `truncated` — the session ran past the per-session cap. It has further errors, and `preceding_tools` may be incomplete, so an empty list here does not mean nothing preceded the failure. Narrow the window or read the session directly.
- `no_detail_recorded` — the failure is real but no error text was stored for it. Failed tool calls from before Brizz recorded tool exceptions separately look like this.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `session_ids` | array of strings | Yes | Session ids, verbatim. 1–50 per call. |
| `start` | string | No | RFC3339. The time window is the only thing that narrows this scan — pass it whenever you have one. |
| `end` | string | No | RFC3339. |
| `offset` | integer | No | Byte offset into each message and stacktrace, for paging a long value. |
| `max_bytes` | integer | No | Bytes of each message and stacktrace to return (default 2000, max 200000). |

For one session's failures in full turn context, use `brizz_get_conversation` with `filter: "errors"` instead.

## Issues

Issues are aggregated quality problems detected across sessions. These tools mirror the dashboard's Issues page.

### `brizz_search_issues`

Searches issues for a service with the same filters available in the dashboard. Returns issues ranked by priority.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `search` | string | No | Free-text across issue titles and descriptions. |
| `organization_id` | string | No | Restrict to one organization. |
| `journey_id` | string | No | Restrict to one journey. |
| `user_id` | string | No | Restrict to one end-user. |
| `statuses` | array of strings | No | `open`, `tracked`, `closed`. |
| `priorities` | array of strings | No | `critical`, `high`, `medium`, `low`. |
| `severity_levels` | array of strings | No | Filter by severity level. |
| `time_range` | string | No | Shorthand like `7d`, `30d`. Default `7d`, max `90d`. |
| `start` | string | No | Inclusive RFC3339 start. |
| `end` | string | No | Exclusive RFC3339 end. |
| `order_by` | string | No | Sort field, e.g. `priority`, `last_seen`, `occurrence_count`. |
| `order_dir` | string | No | `asc` or `desc`. |
| `limit` | integer | No | Default 50, max 200. |
| `offset` | integer | No | Pagination offset. |
| `response_format` | string | No | `concise` (default, top 10), `detailed`, or `raw`. |

### `brizz_get_issue`

Fetches one issue's full detail: metadata, the evidence sessions where it was detected, and the activity log when available.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `issue_id` | string | Yes | Full issue id, verbatim as returned by `brizz_search_issues`. |

From here you can follow an evidence session into `brizz_get_conversation` to see what actually happened.

### `brizz_investigate_issue`

The curated "Fix with your agent" handoff document for one issue: concept guidance for its issue type, the agent's setup (model, params, tools), whether a system prompt was captured (the text itself is not inline — `brizz_get_issue_prompt` returns it), per-finding evidence, conversation snippets windowed around the flagged turns, and a follow-up funnel phrased as `brizz_*` tool calls. A shortcut for starting from one curated document rather than assembling the picture read by read; it is one heavy read, so fall back to the `investigate-issue` runbook if it fails.

The returned markdown is the whole answer and is not truncated by the server: the export applies its own ~40,000-character budget with truncation footnotes and reports the true size in `char_count`, so treat it as self-bounding rather than a capped preview.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `issue_id` | string | Yes | Full issue id, verbatim as returned by `brizz_search_issues`. |
| `notes` | string | No | Free-text context to steer the investigation — a user complaint, a hypothesis, what you already ruled out. Max 4000 characters. |

```json
{
  "tool": "brizz_investigate_issue",
  "arguments": {
    "issue_id": "550e8400-e29b-41d4-a716-446655440000",
    "notes": "the user says checkout silently fails on the last step"
  }
}
```

Drill into what it points at with `brizz_get_issue_findings` (more evidence rows) and `brizz_get_issue_prompt` (the full captured system prompt).

### `brizz_get_issue_findings`

The per-finding evidence rows behind one issue: reasoning, error type, analyzer metadata, and session/span pointers.

- `brizz_investigate_issue` truncates each finding to 2000 characters and points here for more detail.
- `brizz_get_issue_findings` renders a Markdown table in `content[0].text`, where each Reasoning cell is shortened to the first line and 120 characters.
- `brizz_get_issue_findings` also returns `structuredContent.findings[].reasoning`, which preserves the full reasoning and is referenced by a note below the table whenever the rendered cell is shortened.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `issue_id` | string | Yes | Full issue id, verbatim. |
| `error_type` | string | No | Restrict to findings tagged with exactly this error type. One value only. |
| `search` | string | No | Substring match against a finding's `description` field. |
| `session_id` | string | No | Restrict to findings from one session id. |
| `limit` | integer | No | Default 50, max 50 — a page this size stays inside the response budget for all but the densest issues; page the rest with `offset`. |
| `offset` | integer | No | Pagination offset. |
| `response_format` | string | No | `concise` (default, top 10), `detailed`, or `raw`. |
| `fields` | array of strings | No | Only these keys on each finding in the structured payload. `finding_id` is always included. Unknown names are reported in `_warnings`, not rejected. |

Pass `fields` on the first call rather than after a truncated one. Each finding's `metadata` is typically about two thirds of the response's bytes, so asking for only the columns you need — say `["session_id", "reasoning", "error_type"]` — usually keeps a dense issue inside the byte budget instead of shedding. `metadata` is also the first thing dropped when a page does exceed the budget; a narrower `fields`, a smaller `limit`, or an added filter brings it back.

### `brizz_get_issue_prompt`

The full captured system prompt(s) for one issue, one entry per agent — the root agent first, then agents owning a finding (alphabetical), then every other agent (alphabetical); an unnamed subagent renders as "Subagent" — nothing else on this server returns system-prompt text. A prompt commonly runs to tens of thousands of characters, so this tool windows it explicitly rather than truncating it silently: each entry reports `{text, length, offset, truncated}`, exactly like `brizz_get_conversation_item`, and you page the rest by re-calling with a larger `offset`.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `issue_id` | string | Yes | Full issue id, verbatim. |
| `agent_name` | string | No | Restrict to one agent's prompt instead of the whole roster — match a previous call's `agent_name`, or `"root"` for the root agent. |
| `offset` | integer | No | Byte offset into each agent's prompt text. |
| `max_bytes` | integer | No | Bytes of each prompt to return, from `offset`. Default 20000, max 200000. |

Errors clearly when the issue never captured a system prompt, or captured one that came back empty. Paging an individual prompt's `offset` past its true length returns an explicit `offset_past_end` marker rather than a blank result.

### `brizz_update_issue`

Mutates one or more fields on an existing issue. Applies whichever fields are provided and skips the rest. **Partial success is reported** — a failure on one field does not abort the others. Requires `mcp:write`.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `issue_id` | string | Yes | Issue to update. |
| `status` | string | No | `open`, `tracked`, `ignored`, or `resolved`. |
| `priority` | string | No | `critical`, `high`, `medium`, or `low`. |
| `assignee_id` | string | No | User UUID of the new assignee, who must be a tenant member. |
| `unassign` | boolean | No | Set true to clear the assignee. Mutually exclusive with `assignee_id`. |
| `title` | string | No | New title, 1–500 characters. |
| `response_format` | string | No | `concise` (default), `detailed`, or `raw`. |

```json
{
  "tool": "brizz_update_issue",
  "arguments": {
    "issue_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "tracked",
    "priority": "high"
  }
}
```

Note that the status values accepted by `brizz_update_issue` (`open`, `tracked`, `ignored`, `resolved`) differ from the ones `brizz_search_issues` filters on (`open`, `tracked`, `closed`).

## Metrics

### `brizz_query_metric`

Returns the raw time-bucketed series for **one** activity metric, one row per bucket. Use it when you need the underlying series itself — to plot a chart, sum a total, or feed a calculation.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `metric` | string | Yes | One of `cost`, `sessions`, `active_users`, `errors`, `issues`. |
| `time_range` | string | No | Shorthand like `24h`, `7d`, `30d`. Default `7d`, max `90d`. |
| `start` | string | No | Inclusive RFC3339 start. |
| `end` | string | No | Exclusive RFC3339 end. |
| `granularity` | string | No | Bucket size: `hour` or `day`. The backend picks a heuristic default if omitted. |
| `organization_id` | string | No | Scope the metric to one organization. |

The result carries the metric name, service, `from`/`to` bounds, a `unit`, and a `points` array of `{timestamp, value}`.

```json
{
  "tool": "brizz_query_metric",
  "arguments": { "metric": "cost", "time_range": "30d", "granularity": "day" }
}
```

## Organizations

Organizations are your end-customers.

### `brizz_list_organizations`

Lists organizations for one service, ranked by activity. Returns a paginated list with usage counters — sessions, users, issues, cost — and lifecycle status.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `search` | string | No | Free-text across org name and id. |
| `time_range` | string | No | Shorthand like `7d`, `30d`. Default `7d`, max `90d`. Overridden by `start`/`end`. |
| `start` | string | No | Inclusive RFC3339 start. |
| `end` | string | No | Exclusive RFC3339 end. |
| `limit` | integer | No | Default 50, max 200. |
| `offset` | integer | No | Pagination offset. |
| `order_by` | string | No | `lastSeen` (default), `sessionCount`, `userCount`, `totalCostUSD`, `issueCount`, or `activity`. |
| `order_dir` | string | No | `asc` or `desc`. Default `desc`. |

### `brizz_get_organization_overview`

The headline analytics surface. Behaviour depends on whether you pass `organization`:

- **With `organization`** — a single-organization brief: usage counters, quality metrics, top users, top journeys, top issues, and recent sessions, mirroring the dashboard's organization details drawer.
- **Without `organization`** — a tenant-wide overview: top organizations by activity, global totals, and pointers to drill into specifics.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `organization` | string | No | Organization id, exact name, fuzzy name, or domain. Omit for the tenant-wide overview. |
| `time_range` | string | No | Shorthand like `7d`, `30d`. Default `7d`. |
| `start` | string | No | Inclusive RFC3339 start. |
| `end` | string | No | Exclusive RFC3339 end. |
| `response_format` | string | No | `concise` (default), `detailed`, or `raw`. |

Because `organization` accepts a fuzzy name, multiple matches are possible. In that case the response comes back with `status: ambiguous` and a candidate list so you can pick the right id and call again.

## User usage

### `brizz_get_user_usage`

Fetches a usage rollup for a single end-user: total session count, first and last active dates, average and median session duration, the journeys they engaged with and how many times, and a deduplicated list of their opening prompts.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `service_name` | string | No | Defaults to the resolved service. |
| `filters` | array | **Yes** | One or more conditions identifying the user. Same condition shape as `brizz_search_sessions`. |
| `time_range` | string | No | Shorthand like `24h`, `7d`, `30d`. Default `30d`, max `90d`. Overridden by `start`/`end`. |
| `start` | string | No | Inclusive RFC3339 start. |
| `end` | string | No | Exclusive RFC3339 end. |

Unlike most tools here, `filters` is required — it's how you say which user you mean.

```json
{
  "tool": "brizz_get_user_usage",
  "arguments": {
    "filters": [{ "field": "userId", "operator": "equals", "value": "u-123" }]
  }
}
```

Or by a custom property such as email — note the `filter_type`:

```json
{
  "filters": [
    {
      "field": "email",
      "operator": "equals",
      "value": "x@example.com",
      "filter_type": "custom_properties"
    }
  ]
}
```

Call `brizz_list_session_filters` first to find the correct field name for your service — user identity is instrumented differently across services. Note the default window here is `30d`, wider than the `7d` used by most other tools.

## Analytics agent

The analytics agent answers questions no single typed tool can. It runs a full agent turn on the Brizz side, so it costs more and takes longer than a typed tool.

Good reasons to use it: open-ended diagnostics like *"why are users frustrated?"* or *"what's regressing this week?"*, questions needing cross-tool orchestration, or when someone explicitly asks for the analyst. Bad reasons: fetching a known object, running a filtered search, or pulling a metric series.

### `brizz_ask_agent`

Sends a single question and returns the analyst's answer. Requires `mcp:write`; without that scope, the tool is omitted from `tools/list` and direct calls are rejected.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `message` | string | Yes | The natural-language question. |
| `service_id` | string | No | Service UUID. Omit to use the resolved default service. |
| `service_name` | string | No | Service slug. Omit to use the resolved default service. |

Any id you reference in `message` must be passed in full and verbatim as a tool returned it. Ids are fixed width: session and trace ids are 32 hex characters (e.g. `3023b8d4917218792959925606c29f37`), span ids are 16, and issue/cluster/journey/label ids are 36-character UUIDs (e.g. `550e8400-e29b-41d4-a716-446655440000`).

```json
{
  "tool": "brizz_ask_agent",
  "arguments": { "message": "Why did cost spike last Tuesday?" }
}
```

### `brizz_continue_agent_conversation`

Holds a multi-turn conversation with the analyst, so context carries across messages. Requires `mcp:write`. Use it when the question is likely to need clarification or chained follow-ups; for a one-shot question, `brizz_ask_agent` is simpler.

| Argument | Type | Required | Notes |
| --- | --- | --- | --- |
| `method` | string | Yes | `start` to open a new conversation, `send` to post a follow-up. |
| `service_id` | string | No | Service UUID, for `method=start`. Ignored for `send`. |
| `service_name` | string | No | Service slug, for `method=start`. Ignored for `send`. |
| `title` | string | No | Optional title, for `method=start`. |
| `conversation_id` | string | Required for `send` | The conversation to continue. |
| `message` | string | Required for `send` | Follow-up message text. |

```json
{
  "tool": "brizz_continue_agent_conversation",
  "arguments": { "method": "start", "title": "Cost investigation" }
}
```

Then follow up with the returned id:

```json
{
  "tool": "brizz_continue_agent_conversation",
  "arguments": {
    "method": "send",
    "conversation_id": "<id from start>",
    "message": "Break that down by organization."
  }
}
```

## See also

- [MCP server](/docs/integrations/mcp-server/overview.md) — connecting Claude Code, Cursor, Codex, and other clients to Brizz.
- [MCP (auto-instrument your server)](/docs/sdks/mcp.md) — adding Brizz observability to an MCP server you operate.


---

# Brizz CLI

Install and authenticate the brizz CLI — read sessions, issues, intents, and metrics from the terminal, and hand structured investigations to an AI coding agent.

The **Brizz CLI** (`brizz`) reads your Brizz analytics from the terminal. It's built for AI coding agents — Claude Code, Cursor, and the like — to answer questions about your agent's behavior through a stable interface instead of scraping the dashboard, and it's equally usable directly.

## Install

:::tabs
:::tab[macOS (Homebrew)]
```bash
brew install brizzai/tap/brizz-cli
```
:::tab[macOS & Linux]
```bash
curl -fsSL https://raw.githubusercontent.com/brizzai/brizz-cli/master/install.sh | sh
```
:::tab[Windows]
```text
Download the Windows .zip from github.com/brizzai/brizz-cli/releases,
extract it, and add it to your PATH.
```
:::

`brizz upgrade` updates it in place.

## Authenticate

```bash
brizz auth login          # opens your browser (OAuth)
brizz auth whoami         # who you are, and the active tenant
```

OAuth covers every tenant your account can access. For CI and agents, use a **Platform API key** from **Settings → User Settings → Platform API Keys** — not a telemetry key, which returns `401` here:

```bash
brizz auth login --api-key "$BRIZZ_API_KEY"
```

`BRIZZ_API_KEY` in the environment works on its own. A Platform API key is pinned to one tenant and carries the role chosen when it was created. See the [API overview](/docs/api/overview.md) for the credential classes.

## Pick a tenant and app

```bash
brizz tenant list
brizz tenant switch acme        # persist a default
brizz app switch checkout-agent
```

Tenant resolution is `--tenant`, then `BRIZZ_TENANT`, then the persisted default. One accessible tenant is picked automatically; with several and no choice made, the command stops with an error rather than guessing.

## Read your data

```bash
brizz sessions list
brizz sessions view <id>               # one session's detail
brizz sessions conversation <id>       # turns and tool calls
brizz sessions browse                  # interactive picker

brizz issues list
brizz issues evidence <issue-id>       # the occurrences behind an issue
brizz issues investigate <id>          # structured investigation bundle

brizz intents list
brizz metrics query --metric cost
brizz status
```

Session and issue ids accept unique prefixes, resolved against 200 records — the newest sessions, but the highest-priority issues. Anything outside that needs a full UUID. `intents list` adds open-issue counts to the first 30 clusters only.

## Output

On a terminal you get tables and cards. Piped, you get JSON: `tenant`, `app`, and `metrics list` emit true NDJSON, while the session, issue, intent, evidence, and conversation commands emit one indented JSON envelope — pipe those to `jq` rather than reading line by line. `whoami`, `status`, and `summarize` fall back to YAML, and `version` prints text regardless of `--output`.

Override with `--output json|ndjson|yaml|tty|wide` (or `--json`). Disable color with `--no-color` or `NO_COLOR`.

## Agent mode

`--agent` (or `BRIZZ_AGENT=1`) switches to Markdown and adds a next-commands trailer on the supported analytics commands, so an agent can navigate an investigation without knowing command names in advance. It also suppresses update nudges; add `--no-pager` if long output must not be paged.

```bash
brizz issues investigate <id> --agent
```

`brizz agent-guide` prints a workflow guide and `brizz explain <resource>` the built-in field reference. Both are curated summaries — `brizz <command> --help` is the complete one.

## Availability

The CLI reads only what your role already allows. Command-usage logs are **on by default**; opt out with `brizz config telemetry disable`, `BRIZZ_TELEMETRY=0`, or `DO_NOT_TRACK=1`. Crash reporting is separate.

## See also

- [API overview](/docs/api/overview.md) — the Platform API key, and how it differs from a telemetry key.
- [MCP server](/docs/integrations/mcp-server/overview.md) — giving an agent access to Brizz over MCP instead.
- [Issues](/docs/platform/issues.md) and [Sessions](/docs/platform/sessions.md) — what these commands read.


---

# Server DSN

Authenticate an SDK with a single connection string that carries the credential, the ingestion endpoint, and the service name.

A **Server DSN** is a single connection string that authenticates one service to Brizz. It replaces the `api_key` + `app_name` pair: the credential, the ingestion endpoint, and the service name all travel inside one value.

```
https://<credential>@<ingest-host>/<service-name>
```

Because the service name travels in the string, giving each service its own DSN attributes its traces correctly without extra configuration in your code. The service name is the part your SDK sends, so it's a convention worth keeping rather than a restriction the credential enforces.

## Creating a Server DSN

### During onboarding

Setting up a new service generates its DSN for you. Copy it before leaving the page — the credential is shown once.

### From the dashboard

1. Navigate to **Organization Settings**
2. Select the **API Keys** tab
3. Click **Create Telemetry API Key**
4. Under **Credential Type**, choose **Server DSN**
5. Give the credential a **Name** — how it's listed in the dashboard, not part of the DSN
6. Optionally enter the **service name** this DSN reports as — it's baked into the string and becomes the service you see in the dashboard. Leave it blank and the DSN comes back with a literal `<service-name>` placeholder for you to substitute later
7. Copy the DSN immediately — it won't be shown again

:::info
The service name must match the service you expect to see in Brizz. Sending from two codebases under one DSN merges their traces into a single service.
:::

## Using a Server DSN

Store it in an environment variable — a DSN contains a credential and must be treated like a password.

```bash
export BRIZZ_DSN="https://<credential>@<ingest-host>/<service-name>"
```

:::tabs
:::tab[Python]
```python
import os
from brizz import Brizz

Brizz.initialize(
    dsn=os.environ["BRIZZ_DSN"],
)
```
:::tab[Node.js]
```typescript
import { Brizz } from '@brizz/sdk';

Brizz.initialize({
  dsn: process.env.BRIZZ_DSN,
});
```
:::

That's the whole difference from the API-key setup — `app_name` is gone, because the DSN already carries the service name. Everything after initialization (sessions, users, events) is identical.

:::tip
Initialize Brizz **before** importing your AI libraries. Auto-instrumentation hooks those modules at import time, so loading them first means calls go untraced. See [Send your first session](/docs/get-started/first-session.md).
:::

## Server DSN vs API key

| | Server DSN | API key |
|---|---|---|
| Environment variable | `BRIZZ_DSN` | `BRIZZ_API_KEY` |
| Service name | Baked into the string | Set separately via `app_name` |
| Ingestion endpoint | Baked into the string | Resolved by the SDK |
| Scope | One service | Shared across services |
| Safe in client code | No | No |

Both authenticate the same SDKs and support the same features. If your workspace issues Server DSNs, use those — one value per service is less to configure and less to get wrong.

## Client DSN

A **Client DSN** is a separate credential class for the browser SDK (`@brizz/browser`). It's public by design and ships in your front-end bundle: it can submit telemetry but cannot read your data. It's meant for browser traffic and requires an `Origin` header on every request, so it isn't a substitute for a Server DSN on a backend.

Create one the same way as a Server DSN, choosing **Client DSN** as the credential type. Client DSNs start with `brizz-ing-c-`, and when the browser SDK is initialized with a `dsn` it rejects any other credential class. Requests must carry an `Origin` header, and you can optionally restrict which origins are accepted with an `allowed_origins` list — a browser-origin check, not proof of site ownership. See [Browser SDK](/docs/sdks/browser.md) for what to do with it.

## Rotating a DSN

1. Create a new Server DSN with the same service name
2. Update `BRIZZ_DSN` in your environment and redeploy
3. Confirm new sessions are arriving — see [Verify it landed](/docs/get-started/verify.md)
4. Delete the old DSN

## Troubleshooting

### No data arriving

- Confirm `BRIZZ_DSN` is actually set in the running environment, not just in a local `.env` that isn't loaded
- Check the DSN wasn't truncated on copy — it ends with the service name, not the host
- Make sure `Brizz.initialize()` runs before any AI library is imported

### Traces land under the wrong service

The service name comes from the DSN, not from your code. Check which DSN that environment is using.

### "Invalid DSN" at startup

- Verify no whitespace or quotes crept into the value
- Confirm the DSN hasn't been deleted in **Organization Settings → API Keys**
- A literal `<service-name>` left in the string is rejected on purpose — replace it with your real service name

## See also

- [API keys](/docs/admin/api-keys.md) — the alternative credential class, and rotation practices that apply to both.
- [Send your first session](/docs/get-started/first-session.md) — initialize the SDK and capture a session.
- [Services & configuration](/docs/admin/services-and-configuration.md) — how services appear and are managed in Brizz.
- [Telemetry ingestion API](/docs/api/telemetry.md) — sending without an SDK.


---

# API keys

Create, store, and rotate the API keys your SDKs and integrations use to authenticate to Brizz.

Brizz uses API keys to authenticate SDKs and API requests.

This guide covers how to create keys, store them safely, and rotate them without downtime.

:::info
API keys are the legacy credential. New services should use a [Server DSN](/docs/admin/server-dsn.md), which carries the credential, the ingestion endpoint, and the service name in one string — so there's no separate app name to configure. Existing API keys keep working.
:::

## Creating API Keys

### Via Dashboard

1. Navigate to **Organization Settings** in the dashboard
2. Select the **API Keys** tab
3. Click **Create Telemetry API Key**
4. If your workspace offers a **Credential Type** choice, select **API Key** — it's marked *Legacy* there
5. Give the key a descriptive **Name** (e.g., "Production Server", "Local Development")
6. Set an **Expiry Duration**, or choose never to expire
7. Copy the key immediately — it won't be shown again

:::info
Keys can be created with an expiration date or configured to never expire. We recommend setting an expiration and rotating regularly.
:::

## Using API Keys

### Environment Variables (Recommended)

Store your API key in an environment variable:

```bash
export BRIZZ_API_KEY="your-api-key-here"
```

:::tip
Do not expose API keys in browser or mobile client code. In a browser, use a [Client DSN](/docs/admin/server-dsn.md#client-dsn); send mobile telemetry through a trusted server, since a Client DSN requires a browser `Origin` header.
:::

### Python

```python
import os
from brizz import Brizz

Brizz.initialize(
  api_key=os.environ["BRIZZ_API_KEY"],
  app_name="my-ai-app",
)
```

### TypeScript

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

Brizz.initialize({
  apiKey: process.env.BRIZZ_API_KEY!,
  appName: 'my-ai-app',
});
```

## API Key Best Practices

### DO ✅

- Use environment variables for API keys
- Rotate keys regularly (every 90 days recommended)
- Use separate keys for development and production
- Delete unused keys promptly

### DON'T ❌

- Commit API keys to version control
- Share keys between team members
- Expose keys in client-side code

## Rotating Keys

To rotate an API key without downtime:

1. Create a new API key
2. Update your application to use the new key
3. Verify the new key is working
4. Delete the old key

## Troubleshooting

### "Invalid API Key" Error

- Verify the key is copied correctly (no extra spaces)
- Check that the key hasn't been deleted
- Ensure you're using the correct environment

### "Expired" (or "Expiring Soon")

- If a key is expired, create a new one and update your environment variable
- Keep both keys active briefly during rollout to avoid downtime

## See also

- [Server DSN](/docs/admin/server-dsn.md) — the single-string alternative that carries the credential, endpoint, and service name together.
- [Sessions](/docs/instrument/sessions.md) — what to do once the key works.
- [Telemetry ingestion API](/docs/api/telemetry.md) — using the key without an SDK.
- [SSO (JumpCloud)](/docs/admin/sso-jumpcloud.md) — replace password login with SAML SSO.
- [PII & privacy](/docs/instrument/pii-and-privacy.md) — pairs with key rotation as part of a least-privilege posture.


---

# JumpCloud SSO (SAML)

Configure JumpCloud as a SAML 2.0 identity provider for Brizz

Set up JumpCloud as your SAML 2.0 identity provider so your team can sign in to Brizz with their JumpCloud credentials. Supports both SP-initiated login (from the Brizz login page) and IdP-initiated login (from the JumpCloud User Portal).

## Prerequisites

- A JumpCloud admin account
- A Brizz **Organization Admin** role
- **SSO enabled** for your Brizz organization (contact Brizz support if the SSO settings page shows "SSO is not enabled")
- Your organization's email domain(s) registered on your tenant (required for SP-initiated discovery by email)

## Step 1: Create the SAML application in JumpCloud

1. In the JumpCloud Admin Console, go to **User Authentication > SSO Applications**.
2. Click **+ Add New Application**, search for **Custom SAML App**, then **Next**.
3. Set a **Display Label** (e.g. "Brizz") and click **Next**.

## Step 2: Configure SP settings

You'll need the **SP Entity ID** and **ACS URL** from Brizz. Both are shown on the SSO settings page in Brizz (**Settings > SSO**). For your tenant they are:

- **SP Entity ID**: `https://platform.brizz.dev/api/v1/saml/your-tenant`
- **ACS URL**: `https://platform.brizz.dev/api/v1/saml/your-tenant/acs`

In the JumpCloud SAML app, on the **SSO** tab:

| Field | Value |
| --- | --- |
| **SP Entity ID** | Paste from Brizz |
| **ACS URL** | Paste from Brizz |
| **SAMLSubject NameID** | `email` |
| **SAMLSubject NameID Format** | `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` |
| **Signature Algorithm** | `RSA-SHA256` |
| **Sign Assertion** | Checked |
| **Default RelayState** | *(leave blank)* |
| **Login URL** *(for IdP-initiated)* | `https://sso.jumpcloud.com/saml2/<your-app-name>` |

> Setting the **Login URL** is what makes the app tile appear (and work) in users' JumpCloud User Portal — required for IdP-initiated login.

## Step 3: Configure user attributes

Under **Attributes**, add the following — these match Brizz's default attribute mappings:

| Service Provider Attribute Name | JumpCloud Attribute Name |
| --- | --- |
| `email` | `email` |
| `firstName` | `firstname` |
| `lastName` | `lastname` |
| `displayName` | `displayname` |

Click **Activate** to save the application. JumpCloud will prompt you to download the **IdP metadata XML** or copy the **IdP Metadata URL** — keep this handy for the next step.

## Step 4: Assign users

On the **User Groups** tab of the SAML app, assign the groups (or individual users on the **Users** tab) that should be able to sign in to Brizz. Users must be assigned before they can authenticate.

## Step 5: Connect JumpCloud in Brizz

1. In Brizz, go to **Settings > SSO**.
2. Paste the **IdP Metadata URL** from JumpCloud and click **Save**. Brizz will fetch the IdP Entity ID, SSO URL, and signing certificate automatically.
3. Toggle **Enable SAML** on.
4. Click **Test Connection** to verify the certificate and metadata are valid.

If you don't have a metadata URL, you can configure manually instead — paste the **IdP Entity ID**, **IdP SSO URL**, and the signing **certificate** (PEM format) from JumpCloud.

## Step 6: Sign in

### SP-initiated (from Brizz)

1. Go to the Brizz login page.
2. Click **Sign in with SSO** and enter your work email.
3. Brizz discovers your tenant by email domain and redirects to JumpCloud to authenticate.

### IdP-initiated (from JumpCloud)

1. Open the JumpCloud **User Portal**.
2. Click the **Brizz** application tile.
3. JumpCloud posts the assertion to Brizz and you land in the dashboard.

## Just-in-Time provisioning

By default, **JIT provisioning** is enabled — users assigned to the SAML app in JumpCloud are automatically created in Brizz on first login, with the default role configured on the SSO settings page. To require pre-existing users, disable **Allow JIT** in the SSO settings.

## Troubleshooting

| Symptom | Likely cause |
| --- | --- |
| `sso_disabled` on the login page after redirect | SSO feature flag is off for your organization — contact Brizz support |
| `saml_error` after IdP redirect | SP Entity ID, ACS URL, or signing certificate mismatch — re-check Steps 2 and 5, and use **Refresh Metadata** in Brizz |
| Tile missing in JumpCloud User Portal | **Login URL** not set on the JumpCloud SAML app (Step 2) |
| User redirected but never reaches dashboard | `Platform.URL` misconfigured, or `/auth/callback` not handling the token hash |
| "Email attribute not found" | The `email` attribute isn't being sent — re-check Step 3 mappings |
| SP-initiated SSO can't find your tenant by email | Your organization's email domain isn't registered on the tenant |

When debugging, the Brizz backend logs (`backend.log`) emit a detailed `ACS: SAML assertion validation failed` entry with the underlying `InvalidResponseError` — this almost always pinpoints the mismatch (issuer, audience, or signature).

## Certificate rotation

JumpCloud signing certificates have an expiry date. Brizz tracks it and surfaces a warning on the SSO settings page as the expiry approaches. When JumpCloud rotates the certificate, click **Refresh Metadata** in Brizz to pick up the new certificate — no downtime required.

## See also

- [API keys](/docs/admin/api-keys.md) — programmatic access; SSO covers humans, API keys cover services.
- [Troubleshooting](/docs/help/troubleshooting.md) — broader auth/ingestion issues.


---

# Organization & members

Your organization's identity, the people in it, and the role each person holds.

The **Organization & members** settings are where you manage who belongs to your tenant, what each person can do, and how your organization presents itself across Brizz.

## What it is

Every Brizz tenant is one organization. It has an identity (name, logo, details), a set of members, and a role for each member that decides what they can see and change. This page is where an organization owner shapes all three.

## Roles and what each can do

Brizz uses a small set of roles, from least to most privileged. Higher roles include everything the lower ones can do.

| Role | What it can do |
| --- | --- |
| **Member** | View analytics and use the product — sessions, issues, dashboards, and everything read-facing. |
| **Admin** | Everything a member can, plus day-to-day tenant management and configuration. |
| **Organization Admin** (owner) | Full control of the organization — billing, security settings, and member management, on top of everything admins can do. |

When you invite someone, you pick their role; you can change it later from the members list. Assign the lowest role that lets a person do their job, and reserve the Organization Admin role for the people who own billing and security.

## Managing members

The members list shows everyone in your organization with their role. From here you:

- **Invite people** — add a member by email and assign their role. They join the organization once they accept.
- **Change a role** — promote or demote a member as their responsibilities change.
- **Remove people** — revoke access for someone who has left or no longer needs it.

Use the in-product members list for the current roster and the exact controls, rather than tracking membership elsewhere — the list is the source of truth for who has access.

## Organization profile

The organization profile is your tenant's identity: its **name**, **logo**, and other organization details. These appear across the dashboard, so keep them current — the name and logo are how members recognize which organization they're working in, especially when they belong to more than one.

## Display preferences

Display preferences are tenant-wide display defaults — the defaults every member sees until they override them for themselves. Setting them here gives your whole organization a consistent starting point instead of each person configuring the same things from scratch. Use the in-product controls to see which display settings are available and to set them.

## Legal & compliance

Some tenants have a **Legal & Compliance** area for organization-level legal and compliance details. It only appears when it's enabled for your tenant. If you don't see it, it isn't enabled for your organization.

## Availability

Managing the organization profile, members, and display preferences requires **Organization Admin** or higher. Members and admins can work in the product, but organization identity and membership are owned by Organization Admins.

## See also

- [API keys](/docs/admin/api-keys.md) — programmatic access for services.
- [SSO (JumpCloud)](/docs/admin/sso-jumpcloud.md) — replace password login with SAML SSO for your members.
- [Plan & billing](/docs/admin/plan-and-billing.md) — the subscription behind your organization.


---

# Plan & billing

Your subscription plan and tier, and how plan limits surface across the product.

The **Plan & billing** settings show the subscription behind your organization, what it includes, and how much you've used. Your plan determines which parts of Brizz are available to your tenant.

## What it is

Your organization is on a subscription plan. The plan defines your tier — the set of features and limits your tenant is entitled to. This page is where you view your current plan, compare it with the others, and register interest in changing it; the change itself is completed with us rather than on the page.

## Your plan and what it unlocks

Your plan sets which capabilities your tenant can use. A higher tier unlocks more of the product; a lower tier includes a smaller set. For exactly what your current plan includes, use the in-product plan comparison — it reflects your live entitlements rather than a fixed list that can drift out of date.

## Usage

Alongside entitlements, this page shows how much you've used. Usage counts the **distinct sessions** recorded in the current calendar month (UTC), so it moves with your traffic rather than with anything you configure, and it resets at the start of each month.

If your plan carries a usage allowance, Brizz warns you as you approach it and shows here when you've gone past it. What happens at the limit depends on your plan, so check here rather than assuming.

:::warning
On plans that pause ingestion at the limit, telemetry sent while you're paused is rejected and **not recorded** — it isn't backfilled when the month rolls over or when you upgrade. Watch this page ahead of a busy period rather than after one.
:::

## Where plan gating shows up

Plan limits don't live only on this page. When a feature isn't part of your current plan, it surfaces as a locked feature at the point where you'd use it — the relevant area of the product indicates that it requires a higher tier rather than failing silently. If something you expected to use appears locked, check your plan here to confirm whether it's included in your tier.

## Availability

Requires **Organization Admin** or higher.

## See also

- [Organization & members](/docs/admin/organization-and-members.md) — the people and roles inside your organization.


---

# Audit logs

The audit trail of administrative and security-relevant actions taken in your tenant.

The **Audit logs** page is the record of who did what, and when, inside your organization. It's where you go to answer questions like "who changed this setting?" or "when was that member removed?"

## What it is

The audit log is a searchable, filterable trail of administrative and security-relevant actions taken in your tenant. Each entry captures the action, the person who performed it, and the time it happened, so you can reconstruct how your organization's configuration and access changed over time.

## What's recorded

The log records administrative and security actions — the changes that affect your organization's configuration, membership, and access. Rather than relying on a fixed list here, open the in-product audit view to see the actual events for your tenant: filter and search by actor, action, or time range to find the specific change you're investigating. The log is a read record; it reflects what happened, and you review it rather than edit it.

## Availability

Requires **Organization Admin** or higher.

## See also

- [Organization & members](/docs/admin/organization-and-members.md) — the roles whose actions the log records.
- [SSO (JumpCloud)](/docs/admin/sso-jumpcloud.md) — sign-in events pair with the audit trail.


---

# Data controls

Tenant-level controls over what enters your analytics — a blocklist to exclude activity, and bulk import.

The **Data controls** settings govern what data lives in your tenant: which activity is kept out of your analytics, and how to bring data in in bulk.

## What it is

Data controls are two tenant-level tools that shape your dataset. The **blocklist** excludes specific activity from analytics, and **import** loads data into the tenant in bulk. Together they let you keep noise out and pull relevant data in.

## Blocklist

The blocklist keeps unwanted traffic out of your analytics — internal test accounts, bots, load-test runs, or any traffic that would otherwise skew your numbers. Editing it requires **Organization Admin** or higher.

### How a rule matches

A rule is `<property> equals | not_equals <value>`. You type the property name yourself, and Brizz matches it against the incoming signal's attributes — span attributes on a trace, log attributes on a log. It tries both the name you typed and a `brizz.`-prefixed version, which is what makes a [custom session property](/docs/instrument/sessions.md) match: send `plan`, write a rule on `plan`, and it finds `brizz.plan`.

Names and values accept only letters, digits, `_` and `-`, so a dotted key like `brizz.user.id` can't be entered. Resource attributes aren't visible to rules either — the SDK's `environment` setting is one, so send it as a session property if you need to match on it.

Each rule can hold up to ten alternative values, which match as OR — one rule covers `env` equals `staging` or `test` or `ci` without needing three rules.

### What a rule does when it matches

Each rule picks one of three actions, and the difference matters:

- **Block** — the telemetry is dropped on arrival. Nothing is stored and nothing is analyzed. Data already in your tenant is unaffected.
- **Skip Analytics** — the raw traces are still stored, but the matching telemetry is kept out of the conversation and session analytics built on top of them. Expect the trace to exist while the session view has nothing to show.
- **Skip Metrics** — sessions stay fully visible and still get analyzed; labelling and issue detection keep running. Only quality-metric extraction is skipped, so issues derived from those metrics aren't produced for matching sessions.

Reach for **Block** when you never want the data at all, and **Skip Metrics** when you still want to open the session and read it. **Skip Analytics** sits between the two: the data is retained but most of the product won't show it.

### Default vs per-service rules

Rules come in two layers. **Default rules** are evaluated for every service. **Per-service rules** are evaluated in addition to them, for the one service you attach them to — so a service ends up subject to the defaults *plus* its own, not one instead of the other.

When more than one rule matches the same traffic, **Block wins**: if any matching rule blocks, the telemetry is dropped and the Skip actions become moot.

## Import

Import bulk-loads data into your tenant. Use it to bring in data in a single operation rather than one record at a time. The in-product import flow walks through the supported input and steps for a given import.

## Availability

Requires **Organization Admin** or higher.

## See also

- [PII & privacy](/docs/instrument/pii-and-privacy.md) — control what sensitive data reaches Brizz in the first place.
- [Identify users](/docs/instrument/identify-users.md) — send a user identity so you can write a blocklist rule against it.
- [Sessions (instrumentation)](/docs/instrument/sessions.md) — attach custom properties to a session so blocklist rules can match on them.


---

# Services & configuration

The AI services configured for your tenant, and the per-service settings that control each one.

The **Services & configuration** settings are where you see the AI services your tenant sends data from and configure how each one behaves.

## What it is

A **service** is one of the AI services, agents, or MCP servers configured for your tenant — the sources that send telemetry to Brizz. This area lists those services and gives each one its own set of configuration tabs, so you can manage a single service's settings without affecting the others.

## The services list

The services list shows the AI services, agents, and MCP servers configured for your tenant. It's the entry point: pick a service from the list to open its configuration. Use the in-product list for the current set of services rather than tracking them elsewhere — it reflects what's actually reporting into your tenant.

## Per-service configuration

Open a service to reveal its configuration tabs. Each tab owns one aspect of how that service is set up.

### General

General holds the service's basic configuration. It's the default view for a service and is where you manage its core settings. Two areas within General are admin-only: **retention** (how long the service's data is kept) and **bring-your-own-model** settings. Members can view General; changing the admin-only areas requires **Admin** or higher.

### Health Center

Health Center checks the service's recent telemetry for missing or malformed data and suggests how to fix what it finds. Access requires **Admin** or higher and the page is available only when the feature is enabled for your organization.

### Timeline

Timeline is the service's change-event history — a record of how the service has changed over time, including **system-prompt changes**. Use it to correlate a shift in your agent's behavior or quality with a specific change: when a metric moves, the Timeline tells you what changed around then.

### Events

Events is where you manage the event and telemetry mappings for the service — how the events your code emits map into Brizz. This is the tab that turns your [custom events](/docs/instrument/custom-events.md) into the signals Brizz understands for this service. Managing event mappings requires **Admin** or higher.

### Webhooks

Webhooks configures **outbound** webhooks for the service: Brizz calls an endpoint you own when one of this service's sessions or traces matures — `session.matured` when a conversation goes idle and Brizz finishes analyzing it, `trace.matured` when a trace settles. It's the alternative to polling for new data. Managing webhooks requires **Admin** or higher, and the feature is off by default per organization — ask Brizz support to enable it. See [Outbound webhooks](/docs/api/webhooks.md) for the payload, signature verification, and retry behavior.

This is not the same as the *inbound* webhooks under **Integrations**, where an external system such as [Segment](/docs/integrations/segment.md) pushes events into Brizz at a URL Brizz provides. Those are configured per integration, not per service.

### Labeling rules

A service also has labeling rules that run against its data. Managing them requires **Admin** or higher. The rules are covered in the [Labels](/docs/platform/labels.md) documentation — see there for how they work and how to configure them.

## Availability

The services list requires **Organization Admin** or higher. Within a service, **General** and **Timeline** are viewable by any **Member**, while **Health Center**, **Events**, **Labeling rules**, and **Webhooks** require **Admin** or higher. Opening an admin-only tab directly as a **Member** shows an access-denied message. Health Center and outbound webhooks also have to be enabled for your organization; until outbound webhooks are enabled, creating a subscription returns `403`.

## See also

- [Labels](/docs/platform/labels.md) — labeling rules that run per service.
- [Outbound webhooks](/docs/api/webhooks.md) — the events the Webhooks tab subscribes to, and how to receive them.
- [API keys](/docs/admin/api-keys.md) — the keys a service uses to send telemetry.
- [Custom events](/docs/instrument/custom-events.md) — the events the Events mapping tab maps.
- [External links](/docs/instrument/external-links.md) — attach external links to a service's sessions.


---

# Labeler instances

Configure the automatic labelers that classify your telemetry — which labeler runs, on what scope, and which label it writes into.

A **labeler instance** is one automatic labeler, switched on for a service. It's the configuration behind the automatic paths on the [Labels](/docs/platform/labels.md) page: it decides *which* labeler runs and *which* label it writes into.

## What it is

A **labeler type** (some are deterministic rules like counting tool calls or detecting language; others use a model), a **scope** — the unit of telemetry it reads — and the **telemetry label** whose value it sets.

Each type declares its supported scopes, and some are grouped into a **family** so you can create several related instances at once. An instance can target one service or every service in the tenant; a service's list shows both its own and the tenant-wide ones.

## How it's populated

Once enabled, it runs as your telemetry is processed. Enabling one doesn't relabel what you already have — expect labels on new sessions, not backfilled history.

## How to read it in the dashboard

The table lists name, description, scope, the label it writes into, and status.

Each row opens a drawer of assignments for that row's **label** on the selected service. It's scoped to the label rather than the instance, so if several instances write to the same label their assignments appear together.

## How to act on it

1. **Point each instance at a purposeful label.** The label is what you'll filter and group by, so name it for the question you want to answer.
2. **Check the assignments before you trust it.** A mis-classifying labeler is worse than no label, because it looks authoritative in a chart.
3. **Disable rather than delete** when unsure — that stops new assignments but keeps the configuration.
4. **Enable it before the traffic you want labelled**, since it won't relabel what's already there.

## Availability

Requires **Organization Admin**; other roles see an access-denied message. A service must be selected, since both the list and the drawer are scoped to one.

## See also

- [Labels](/docs/platform/labels.md) — what labels are, and the manual and rule-based ways to apply them.
- [Custom charts](/docs/platform/custom-charts.md) — group a metric by a label a labeler produced.


---

# Projects

Group services into projects and restrict which people see them on the project-aware, service-scoped pages.

**Projects** group your services and restrict who sees them. Without projects, everyone in the organization sees every service; with them, a service that belongs to a project is shown only to that project's members — and to organization admins, who always see every service.

:::info
Projects is off by default. If **Projects** isn't in your organization settings, [contact us](mailto:support@brizz.ai) to enable it.
:::

## What it is

A project is a name, an optional description, a set of **members**, and a set of **services**. Reach for it when different teams run different agents and shouldn't be looking at each other's traffic day to day.

Treat it as scoping rather than hard isolation: it restricts the project-aware, service-scoped pages, but some information — including tenant usage figures — remains tenant-wide. Don't rely on a project to keep tenant-wide data from someone who already has access to your organization.

Services that aren't assigned to a project sit in a **Default** group, which stays visible to everyone.

## How to read it in the dashboard

Each project is listed with its services and its member count. From here you can:

- **Create a project** — name it, describe it, and add members.
- **Rename or re-describe** an existing project.
- **Add or remove members** — membership is what grants a non-admin visibility of that project's services.
- **Move a service** between projects, or back to Default.

Once projects are in use, the service selector groups services under their project heading, so where a service lives is visible everywhere you switch services rather than only in settings.

## How to act on it

1. **Start from who should see what.** A project is an access boundary first and an organizing device second — if everyone should see everything, you don't need one.
2. **Watch the Default group.** A new service lands in Default and is therefore visible to everyone. If you're using projects for isolation, assign new services deliberately.
3. **Remember that removing a member removes their visibility.** Someone dropped from a project stops seeing its services on the scoped pages.
4. **Don't expect projects to restrict admins.** Organization admins see every service regardless of membership, so a project isn't a way to hide a service from them.
5. **Know the label boundary.** Label data and service-specific rules follow service access; label definitions and tenant-wide rules remain organization-wide. Project-restricted users can view but not change tenant-wide rules, and only organization admins can delete label definitions.

## Availability

Off by default, and managing projects requires **Organization Admin**.

## See also

- [Organization & members](/docs/admin/organization-and-members.md) — the people and roles you assign to a project.
- [Services & configuration](/docs/admin/services-and-configuration.md) — per-service settings for the services a project contains.


---

# API Overview

Overview of the Brizz REST API

Brizz has two HTTP surfaces on two different hosts: **ingestion**, where you send telemetry in, and the **platform API**, where you read your data back out and manage configuration. They take different credentials and different `Authorization` schemes — mixing them up is the most common cause of a `401`.

Most teams should use the official SDKs for ingestion and only use the raw API for custom pipelines.

## Base URLs

| Surface | Base URL | What it does |
|---|---|---|
| Telemetry ingestion | `https://telemetry.brizz.dev` | Accepts traces and events. Write-only — it has no read endpoints. |
| Platform API | `https://platform.brizz.dev/api/v1` | Reads sessions, traces, and analysis results; manages configuration such as webhook subscriptions. |

## Authentication

### Telemetry ingestion — `Bearer`

Ingestion takes a **Telemetry API key** or a [DSN](/docs/admin/server-dsn.md), created under **Organization Settings > API Keys**, sent as `Bearer`:

```bash
curl -X POST "https://telemetry.brizz.dev/raw/events" \
  -H "Authorization: Bearer $BRIZZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"user.login","service_name":"my-app","session_id":"s-1","timestamp":"2025-11-24T10:00:00Z"}'
```

### Platform API — `Token`

The platform API takes a **Platform API key**, created under [**User Settings > Platform API Keys**](/app/settings/platform-api-keys), sent as `Token`:

```bash
curl "https://platform.brizz.dev/api/v1/telemetry/otel/sessions/my-agent/SESSION_ID/transcript" \
  -H "Authorization: Token $BRIZZ_PLATFORM_API_KEY"
```

:::warning
The two credentials are not interchangeable, and neither is the scheme. On the platform API, `Bearer` is reserved for the dashboard's sign-in token, so a Platform API key sent as `Bearer` is checked against the wrong validator and returns `401` even though the key itself is valid — use `Token`. A Telemetry API key or DSN is not a Platform API key at all: it authenticates ingestion only, and returns `401` against the platform API whichever scheme you use. Both failures return an identical bare `401`, so check the key class and the scheme together rather than assuming the key is bad.
:::

A Platform API key acts as the user who created it, with that user's role and tenant, and inherits their project membership. Create it under an account that will outlive any one person's access — if the owning user is deprovisioned, integrations using their key stop working. The key is pinned to its own tenant; an `X-Tenant-ID` header is ignored on this path.

## Request Format

- All requests must use HTTPS
- Request bodies should be JSON with `Content-Type: application/json`
- Dates should be in ISO 8601 format

## Response Format

Ingestion endpoints respond with a simple JSON status:

```json
{ "status": "success" }
```

### Error Responses

Errors follow this format:

```json
{ "error": "Invalid JSON format" }
```

## Common Error Codes

| Code | HTTP Status | Description |
|------|-------------|-------------|
| `unauthorized` | 401 | Invalid or missing API key |
| `forbidden` | 403 | Insufficient permissions |
| `not_found` | 404 | Resource not found |
| `rate_limited` | 429 | Too many requests |
| `invalid_request` | 400 | Invalid request parameters |
| `server_error` | 500 | Internal server error |

## Endpoints

- [Telemetry ingestion](/docs/api/telemetry.md) — `POST /raw/traces`, `POST /raw/events`.
- [Reading data](/docs/api/reading-data.md) — fetching sessions, transcripts, traces, and spans back out.
- [Outbound webhooks](/docs/api/webhooks.md) — subscribing to `session.matured` and `trace.matured` instead of polling.

## SDKs

We provide official SDKs that handle authentication, retries, batching, and auto-instrumentation:

- [Python SDK](/docs/sdks/python.md)
- [Node.js / TypeScript SDK](/docs/sdks/typescript.md)

## See also

- [Telemetry ingestion](/docs/api/telemetry.md) — endpoint reference for the ingestion host.
- [Reading data](/docs/api/reading-data.md) — endpoint reference for the platform host.
- [Outbound webhooks](/docs/api/webhooks.md) — get pushed an event instead of polling.
- [API keys](/docs/admin/api-keys.md) — creating and rotating the *telemetry* keys used for ingestion.
- [Choose your SDK](/docs/sdks.md) — prefer an SDK over raw HTTP for almost every case.

Support: support@brizz.ai


---

# Telemetry ingestion

Low-level HTTP endpoints for sending traces and events to Brizz.

Brizz provides a simple HTTP API for sending raw telemetry. Most teams should prefer the **official SDKs**, which handle:

- Authentication headers
- OpenTelemetry formatting
- Batching/retries
- Automatic instrumentation of supported AI libraries

Use this API when you need a custom pipeline or want to send telemetry from a non-supported runtime.

## Base URL

```
https://telemetry.brizz.dev
```

## Authentication

All requests require an API key:

```
Authorization: Bearer YOUR_API_KEY
```

Brizz accepts:

- **Legacy API keys** — work as-is.
- **Server DSN** (`brizz-ing-s-…`) — single-paste credential for the Brizz backend SDK. Bundles bearer, endpoint, and service name. Don't use with the frontend SDK.
- **Client DSN** (`brizz-ing-c-…`) — single-paste credential for the Brizz frontend SDK. Safe to ship inside browser bundles. Optionally pin to an `allowed_origins` list (set in the dashboard) to restrict which sites can use the key.

## Send traces

`POST /raw/traces`

A trace payload includes:

- `provider` (currently `openai`)
- `service_name` (your app/service name)
- `session_id` (conversation/workflow identifier)
- `request` (the full provider request)
- `responses` (1–2 responses; use 2 for tool-call workflows)

Example:

```bash
curl -X POST "https://telemetry.brizz.dev/raw/traces" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $BRIZZ_API_KEY" \
  -d '{
    "provider": "openai",
    "service_name": "my-chatbot",
    "session_id": "session-001",
    "environment": "production",
    "request": {
      "model": "gpt-4",
      "messages": [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "What is the capital of France?"}
      ]
    },
    "responses": [
      {
        "id": "chatcmpl-demo123",
        "object": "chat.completion",
        "model": "gpt-4",
        "choices": [
          {"index": 0, "message": {"role": "assistant", "content": "The capital of France is Paris."}, "finish_reason": "stop"}
        ],
        "usage": {"prompt_tokens": 15, "completion_tokens": 8, "total_tokens": 23}
      }
    ],
    "start_time": "2025-11-24T10:00:00Z",
    "end_time": "2025-11-24T10:00:02Z"
  }'
```

Success response:

```json
{ "status": "success" }
```

## Send events

`POST /raw/events`

Example:

```bash
curl -X POST "https://telemetry.brizz.dev/raw/events" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $BRIZZ_API_KEY" \
  -d '{
    "name": "user.login",
    "service_name": "my-app",
    "session_id": "session-001",
    "timestamp": "2025-11-24T10:00:00Z",
    "severity_number": 9,
    "attributes": {"user_id": "user-123", "method": "oauth"},
    "body": {"success": true, "duration_ms": 245},
    "environment": "production"
  }'
```

## Common errors

- **400**: invalid payload
- **401**: missing/invalid API key
- **403**: DSN origin rule violation
- **500**: server error

:::tip
If you’re using Node/Python, prefer the SDKs unless you have a strong reason not to.
:::

## See also

- [API overview](/docs/api/overview.md) — base URLs, auth, error model.
- [Choose your SDK](/docs/sdks.md) — language-specific options that handle batching and instrumentation for you.
- [API keys](/docs/admin/api-keys.md) — creating and rotating the key used here.


---

# Reading data

Fetch sessions, transcripts, traces, spans, and analysis results back out of Brizz over HTTP.

Brizz's platform API reads back everything the SDK sent in, plus what Brizz computed from it — conversations, traces, spans, costs, and detected problems. Use it to pull a conversation into your own tooling, or to follow up on an [outbound webhook](/docs/api/webhooks.md), whose payload carries identifiers and expects you to fetch the content yourself.

## Base URL and authentication

```
https://platform.brizz.dev/api/v1
```

Every request takes a **Platform API key** as `Token` — not `Bearer`, and not a telemetry key. See [Authentication](/docs/api/overview.md#authentication) for why the distinction matters and where to create the key.

```bash
curl "https://platform.brizz.dev/api/v1/telemetry/otel/sessions/my-agent/SESSION_ID/transcript" \
  -H "Authorization: Token $BRIZZ_PLATFORM_API_KEY"
```

Requests are scoped to the key's tenant and to the projects its owner can see. `{serviceName}` throughout is the trace service name — the same value a webhook payload carries as `data.service_name`.

## Read a session

Given a service name and a session id — the two fields a `session.matured` webhook gives you — this is the endpoint to start from:

`GET /telemetry/otel/sessions/{serviceName}/{sessionId}/transcript`

Parameters:

- `limit` — items per page. Default 100, maximum 500.
- `offset` — pagination offset.
- `content` — `lean` (default) previews heavy tool inputs and results and reports their original lengths; `full` returns them inline. Start with `lean` and drill into single items.

```bash
curl "https://platform.brizz.dev/api/v1/telemetry/otel/sessions/my-agent/sess_abc123/transcript?limit=200" \
  -H "Authorization: Token $BRIZZ_PLATFORM_API_KEY"
```

Success response — conversation items in the order the dashboard renders them, with counts and pagination alongside.

Prefer `/transcript` over the dashboard's `/conversation` endpoint. `/transcript` reads persisted conversation items rather than reprocessing raw spans on every request, it pages over items rather than spans, and — the part that matters to a webhook receiver — it reports whether the data is ready instead of silently returning less than exists.

Always check `status` before treating a response as final:

| `status` | Meaning | What to do |
|---|---|---|
| `complete` | Every span the session produced has been through the pipeline. | Use it. |
| `partial` | Items exist, but the session has spans newer than anything materialized — the tail is still coming. | Use what's there if you want, then re-read after `retryAfterSeconds`. |
| `processing` | The session has spans but nothing materialized yet. `items` is empty. | Wait `retryAfterSeconds` and retry. Do **not** read this as "no data". |

```json
{ "status": "processing", "retryAfterSeconds": 15 }
```

`retryAfterSeconds` accompanies both `partial` and `processing`; honor it rather than picking your own interval. Watch `truncated` too — when `true`, the session exceeded the per-read item cap and the tail is missing regardless of paging.

### Other session reads

All of these hang off `https://platform.brizz.dev/api/v1/telemetry/otel/sessions/{serviceName}/{sessionId}`:

| Purpose | Endpoint |
|---|---|
| One transcript item in full, with reasoning and raw span attributes | `GET …/transcript/items/{itemId}` |
| The traces that make up the session | `GET …/traces` |
| Raw spans | `GET …/spans` |
| Span hierarchy | `GET …/tree`, `GET …/graph` |
| Events emitted during the session | `GET …/events` |
| AI-written summary | `GET …/tldr` |
| Problems Brizz detected | `GET …/problem-analysis` |
| Cost breakdown | `GET …/cost-by-model`, `GET …/tool-costs` |
| Tool schemas seen in the session | `GET …/tool-schemas` |

Session metrics and intents are keyed differently:

| Purpose | Endpoint |
|---|---|
| Metrics computed for the session | `GET /telemetry/metrics/session/{serviceName}/{sessionId}` |
| One metric by name | `GET /telemetry/metrics/session/{sessionId}/{metricName}` |
| Intents matched to the session | `GET /telemetry/otel/sessions/{sessionId}/intents?trace_service_name={serviceName}` |

### Session metadata

There is no `GET /telemetry/otel/sessions/{serviceName}/{sessionId}`. To fetch a single session's row — status, timings, labels, metric rollups — query the list endpoint with a filter:

```bash
curl -X POST "https://platform.brizz.dev/api/v1/telemetry/otel/sessions/filtered" \
  -H "Authorization: Token $BRIZZ_PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "serviceName": "my-agent",
    "filters": [{"field": "sessionId", "operator": "equals", "value": "sess_abc123"}],
    "limit": 1,
    "skipTotalCount": true
  }'
```

The same endpoint without the `sessionId` filter is how you search sessions by time range, label, or metric.

## Read a trace

Given a service name and a trace id — what a `trace.matured` webhook gives you:

| Purpose | Endpoint |
|---|---|
| The trace | `GET /telemetry/otel/trace/{traceId}?serviceName={serviceName}` |
| Its spans | `GET /telemetry/otel/spans/{serviceName}/{traceId}` |
| One span | `GET /telemetry/otel/spans/{spanId}` |
| Its events | `GET /telemetry/otel/events/{serviceName}/{traceId}` |
| Metrics computed for it | `GET /telemetry/metrics/trace/{traceId}` |

```bash
curl "https://platform.brizz.dev/api/v1/telemetry/otel/trace/TRACE_ID?serviceName=my-agent" \
  -H "Authorization: Token $BRIZZ_PLATFORM_API_KEY"
```

:::warning
Two things to get right here. The path is **singular** — `/telemetry/otel/trace/{traceId}`; there is no `GET /telemetry/otel/traces/{traceId}` and it returns `404`, because the plural `traces` path is the bulk query `POST /telemetry/otel/traces/filtered`. And unlike the spans and events endpoints below it, this one takes the service name as a **query parameter** rather than a path segment — omit it and you get `400 — serviceName query parameter is required`. Both mistakes fail loudly rather than returning an empty result, so they surface the first time you run the call.
:::

## Read analysis results

Issues and findings are the deduplicated problem signals Brizz derives across sessions, rather than anything tied to a single one:

| Purpose | Endpoint |
|---|---|
| Search issues | `POST /telemetry/otel/issues/filtered` |
| Issue statistics | `POST /telemetry/otel/issues/{serviceName}/stats` |
| One issue's breakdown | `GET /telemetry/otel/issues/{issueId}/breakdown?service_name={serviceName}` |
| Search findings | `POST /telemetry/otel/findings/filtered` |
| Finding statistics | `GET /telemetry/otel/findings/{serviceName}/stats` |

Both `filtered` endpoints identify the service in the request body, but spell the field differently: sessions and traces take `serviceName`, issues and findings take `service_name`.

## Common errors

- **401** — missing key, a telemetry key instead of a Platform API key, or a Platform API key sent as `Bearer` instead of `Token`.
- **403** — the key's role or its owner's project membership doesn't cover this service.
- **404** — no such session, trace, or span in your tenant.
- **402** — your plan doesn't include this endpoint. Intents and journeys are the reads most likely to return this.

## See also

- [API overview](/docs/api/overview.md) — the two hosts, the two credentials, and the error model.
- [Outbound webhooks](/docs/api/webhooks.md) — get told when a session or trace is ready to read.
- [MCP server](/docs/integrations/mcp-server/overview.md) — the same reads exposed to AI agents as tools, if you'd rather not write HTTP calls.
- [Telemetry ingestion](/docs/api/telemetry.md) — the write side, on a different host with a different credential.

Support: support@brizz.ai


---

# Outbound webhooks

Receive an HTTP callback when a session or trace matures, signed with the Standard Webhooks spec.

Outbound webhooks push you an event the moment Brizz finishes materializing a session, or settles a trace — instead of you polling the API. Deliveries are signed with [Standard Webhooks](https://www.standardwebhooks.com), retried on failure, and idempotent by design.

**Outbound** means Brizz calls *your* endpoint. If you're looking for the other direction — an external system like Segment pushing events *into* Brizz, at a URL Brizz gives you — that's an inbound webhook, set up under **Integrations**. See [Segment](/docs/integrations/segment.md).

:::info
Outbound webhooks are off by default per organization. If creating or updating a subscription returns `403`, ask Brizz support to enable them for your account. Listing, deleting, and disabling subscriptions keep working either way — you can always see what exists and switch it off.
:::

## What "matured" means

A session or trace **matures** once it has gone quiet for longer than its *maturation window* — Brizz takes the idle gap as the conversation being finished, and runs its analysis pass over the whole thing. That pass is what materializes the transcript and computes metrics and detected problems, so maturation is the moment the data is worth reading. It's the reason these events exist: they tell you a session is ready, rather than making you poll for it.

The window is a few minutes of inactivity. It's tuned per environment and can change, so build against the behavior — "you'll hear from us shortly after the conversation goes quiet" — rather than against a specific number of minutes.

A session can mature **more than once**. If a user comes back after an idle gap, that's a new stretch of conversation on the same `session_id`, and Brizz reprocesses the session and matures it again. Each pass is a separate delivery with its own `webhook-id` and an incremented `maturation_count`. Traces have no idle-gap concept, so `trace.matured` fires once with `maturation_count` of `1`.

:::warning
`maturation_count` is **not** a retry counter. A value above `1` means the conversation genuinely grew and was reanalyzed — the newest delivery has the most complete picture of the session, and earlier ones are now stale. Retries of a *single* delivery reuse the same `webhook-id` and never change `maturation_count`. Dedupe on `webhook-id`; read `maturation_count` as a version number.
:::

## Managing subscriptions

Subscriptions are managed through the platform API, or from **Service settings > Webhooks** in the dashboard. Creating, updating, and deleting a subscription requires **admin** or higher.

API calls take a **Platform API key** — created under [**User Settings > Platform API Keys**](/app/settings/platform-api-keys) — sent as `Token`, not `Bearer`. A telemetry key or DSN will not work here, and a Platform API key sent as `Bearer` returns `401`; see [Authentication](/docs/api/overview.md#authentication).

**Create a subscription** — `POST https://platform.brizz.dev/api/v1/webhooks`

```bash
curl -X POST "https://platform.brizz.dev/api/v1/webhooks" \
  -H "Authorization: Token $BRIZZ_PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trace_service_id": "SERVICE_ID",
    "name": "Production callback",
    "url": "https://example.com/hooks/brizz",
    "event_types": ["session.matured", "trace.matured"]
  }'
```

The response includes a `secret` field — the signing secret, shown **exactly once**. Store it now; every other read of this subscription omits it.

```json
{
  "id": "8f3e...",
  "trace_service_id": "SERVICE_ID",
  "name": "Production callback",
  "url": "https://example.com/hooks/brizz",
  "event_types": ["session.matured", "trace.matured"],
  "enabled": true,
  "consecutive_failures": 0,
  "created_at": "2026-01-01T00:00:00Z",
  "updated_at": "2026-01-01T00:00:00Z",
  "secret": "whsec_..."
}
```

Other operations, all under `https://platform.brizz.dev/api/v1/webhooks/:id`:

| Method | Path | Notes |
|---|---|---|
| `GET` | `/webhooks` | List every subscription for your tenant. |
| `GET` | `/webhooks/:id` | One subscription, including its health rollup (`last_delivery_status`, `consecutive_failures`, …). |
| `PUT` | `/webhooks/:id` | Update `name`, `url`, or `event_types`. |
| `DELETE` | `/webhooks/:id` | Permanently remove the subscription and its delivery history. |
| `POST` | `/webhooks/:id/enable` | Turn a subscription back on and clear any platform-set failure state. |
| `POST` | `/webhooks/:id/disable` | Turn a subscription off. Always available, even if your organization has lost the entitlement — you can always stop your own traffic. |
| `POST` | `/webhooks/:id/rotate-secret` | See [Rotating your signing secret](#rotating-your-signing-secret). |
| `GET` | `/webhooks/:id/deliveries` | The 10 most recent delivery attempts, newest first — useful for debugging. |

Each trace service may have up to **5 subscriptions in total**. The cap counts every subscription registered against the service, including disabled ones — disabling a subscription does not free a slot; delete it to do that. The registered URL must be HTTPS, resolve publicly (no private/internal address), and carry no embedded credentials.

## Event types

| Event | Fires when | `maturation_count` |
|---|---|---|
| `session.matured` | A session [matures](#what-matured-means) — analytics has finished its pass and the conversation is materialized. | Starts at 1; increments each time the session re-opens after an idle gap and matures again. |
| `trace.matured` | A trace matures. | **Always 1.** A trace has no idle-gap concept, so unlike a session it never re-matures — if the same trace is re-selected by a later run, that's treated as a duplicate of the same delivery, not a new one. |

## Payload

Every delivery body is a single JSON object — never an array, even if multiple subscriptions matched the same event.

```json
{
  "id": "01978a3e-...",
  "type": "session.matured",
  "timestamp": "2026-01-15T09:00:03Z",
  "data": {
    "tenant_id": "3f9c...",
    "trace_service_id": "8f3e...",
    "service_name": "my-agent",
    "maturation_count": 1,
    "occurred_at": "2026-01-15T08:59:58Z",
    "session_id": "sess_abc123"
  }
}
```

`trace.matured` is identical except `type` and `data` carry `trace_id` instead of `session_id`.

| Field | Description |
|---|---|
| `id` | This delivery's id — same value as the `webhook-id` header. Stable across every retry of this delivery. |
| `type` | `session.matured` or `trace.matured`. |
| `timestamp` | When the event was enqueued. Fixed for the life of the delivery — it does not change between retries. Use `data.occurred_at` for when the event actually happened. |
| `data.occurred_at` | When the entity actually matured (the analytics batch window end). This is the field to use for ordering or freshness checks. |
| `data.maturation_count` | The version field — see [What "matured" means](#what-matured-means) and [Idempotency](#idempotency). |

The example above is formatted for reading. On the wire, keys are serialized in **alphabetical order** at every level — `data`, `id`, `timestamp`, `type` at the top. That makes no difference to a JSON parser, but it does mean you cannot reconstruct the signed bytes by re-serializing a parsed object. See [signature verification](#headers--signature-verification).

## Act on the event

The event carries identifiers, not conversation content, by design: a payload small enough to deliver reliably, and you fetch exactly as much as you need. Every call below goes to the platform API with a **Platform API key** as `Token` — a different host and credential from telemetry ingestion, covered in [Reading data](/docs/api/reading-data.md).

After a `session.matured`, `data.service_name` and `data.session_id` are the two path parameters you need:

| What you want | Endpoint |
|---|---|
| The conversation *(start here)* | `GET /telemetry/otel/sessions/{serviceName}/{sessionId}/transcript` |
| One transcript item, in full | `GET …/transcript/items/{itemId}` |
| The session's traces | `GET …/traces` |
| Raw spans | `GET …/spans` |
| Events emitted during the session | `GET …/events` |
| AI-written summary | `GET …/tldr` |
| Problems Brizz detected | `GET …/problem-analysis` |
| Cost | `GET …/cost-by-model`, `GET …/tool-costs` |
| Intents matched to the session | `GET /telemetry/otel/sessions/{sessionId}/intents?trace_service_name={serviceName}` |

After a `trace.matured`, you have `data.service_name` and `data.trace_id`:

| What you want | Endpoint |
|---|---|
| The trace | `GET /telemetry/otel/trace/{traceId}?serviceName={serviceName}` |
| Its spans | `GET /telemetry/otel/spans/{serviceName}/{traceId}` |
| Its events | `GET /telemetry/otel/events/{serviceName}/{traceId}` |
| Metrics computed for it | `GET /telemetry/metrics/trace/{traceId}` |

Two easy mistakes on that first one: the path is **singular** `trace` (there is no `GET /traces/{traceId}` — it `404`s), and it takes the service name as a **query parameter** rather than a path segment like the two below it. Omitting it returns `400 — serviceName query parameter is required`.

### End to end

Verify, then fetch the transcript, then handle the case where Brizz hasn't finished writing it yet:

```python
from svix.webhooks import Webhook, WebhookVerificationError
import requests

@app.post("/hooks/brizz")
def receive(request):
    try:
        event = Webhook(WEBHOOK_SECRET).verify(request.body, request.headers)
    except WebhookVerificationError:
        return Response(status=400)

    if event["type"] != "session.matured":
        return Response(status=200)

    if already_processed(request.headers["webhook-id"]):
        return Response(status=200)

    data = event["data"]
    r = requests.get(
        f"{API_URL}/api/v1/telemetry/otel/sessions"
        f"/{data['service_name']}/{data['session_id']}/transcript",
        headers={"Authorization": f"Token {PLATFORM_API_KEY}"},
        params={"limit": 200},
    ).json()

    # The transcript is written by the same pass that fired this event, so a fast
    # receiver can arrive first. This is "wait", not "no data".
    if r.get("status") == "processing":
        retry_later(request.headers["webhook-id"], after=r["retryAfterSeconds"])
        return Response(status=200)

    analyze(r["items"], version=data["maturation_count"])
    return Response(status=200)
```

The example fetches inline to keep it readable. In production, acknowledge with `2xx` first and move the fetch and analysis onto your own queue keyed by `webhook-id` — a handler slow enough to time out becomes a retried delivery, and you'll do the work twice.

Brizz also sends several deliveries to your endpoint at once, so your handler has to be safe to run concurrently. How many arrive together is set platform-side and isn't tunable per subscription — answering `429` defers the delivery that received it, per the [retry curve](#retries), but the next batch fans out just as wide. Treat `429` as a retry signal rather than a throttle, and note that repeated failures of any kind, `429` included, eventually open the circuit described under [Retries](#retries).

## Headers & signature verification

Every delivery carries three headers, per the [Standard Webhooks](https://www.standardwebhooks.com) spec:

| Header | Description |
|---|---|
| `webhook-id` | This delivery's unique id. |
| `webhook-timestamp` | Unix timestamp (seconds) the request was signed at. |
| `webhook-signature` | One or more `v1,<base64 hmac>` signatures, space-separated. |

Because this is the Standard Webhooks spec, verify with an off-the-shelf client rather than hand-rolling HMAC comparison — it also handles timestamp tolerance and constant-time comparison for you:

:::tabs
:::tab[Python]
```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(WEBHOOK_SECRET)  # the "whsec_..." value from creation or rotation

try:
    payload = wh.verify(request.body, request.headers)
except WebhookVerificationError:
    return Response(status=400)
```
:::tab[Node.js]
```typescript
import { Webhook } from 'svix';

const wh = new Webhook(WEBHOOK_SECRET); // the "whsec_..." value from creation or rotation

try {
  const payload = wh.verify(rawBody, headers);
} catch (err) {
  return res.status(400).end();
}
```
:::

`svix`'s client library implements the same spec Brizz signs with; any other Standard Webhooks-compatible library works too.

:::warning
Verify against the **raw request body** — the exact bytes received. Brizz serializes the payload with keys in alphabetical order, and most JSON libraries won't reproduce that byte-for-byte when you re-serialize a parsed object. Parse-then-re-serialize is the classic way to end up with signatures that never validate. Capture the raw body before your framework parses it.
:::

## Idempotency

Dedupe on the `webhook-id` header (equivalently, the top-level `id` in the body): it is stable across every retry of one delivery, so a retried attempt you've already processed is safe to acknowledge and drop.

`maturation_count` is the version field for genuinely distinct maturations of the same entity — a session that re-opens and matures again arrives as a new delivery with a new `webhook-id` and an incremented count, not a retry of the old one.

**Arrival order is not guaranteed.** Deliveries go out in parallel, so a session's second maturation can reach you before its first, and events for different sessions arrive in no particular order. Compare `maturation_count` with the value you've already stored for that entity and keep the highest rather than overwriting with whatever landed last; use `data.occurred_at` when you need to order events by when they actually happened.

:::info
Delivery history — and with it the idempotency record — is retained for 30 days. If the same entity matures again after that window, the new delivery's `maturation_count` restarts at 1. Don't assume `maturation_count` is monotonic forever; treat each delivery's `webhook-id` as authoritative for dedup, and `maturation_count` as informational beyond the retention window.
:::

## Retries

If your endpoint doesn't respond `2xx`, Brizz retries on a fixed backoff curve, for up to **9 attempts over roughly 52 hours**:

| Attempt | Delay before it |
|---|---|
| 1 | — (first attempt) |
| 2 | 5 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 5 hours |
| 7 | 10 hours |
| 8 | 14 hours |
| 9 | 20 hours |

Each delay is jittered by **±10%**, centered on the tabled value — so an attempt can land slightly earlier than the table says as well as later. The jitter matters because maturation happens in batches: many sessions settle at once, and unjittered retries from that burst would all hit your endpoint at the same instant on every subsequent attempt too. Don't build timing assumptions tighter than the table's own resolution.

If your endpoint was down for longer than the curve above, use [`GET /webhooks/:id/deliveries`](#managing-subscriptions) or your own polling to catch up on anything that failed permanently — don't rely on a webhook you missed re-arriving on its own.

What a response means:

- **`2xx`** — delivered. Retry state resets.
- **`429` or `408`** — retried per the curve above. If you send `Retry-After`, Brizz honors it when it's *later* than the curve's own next step (never sooner).
- **Any other `4xx`/`5xx`, or a connection failure/timeout** — retried per the curve.
- **A `3xx` redirect** — **not retried.** Brizz never follows redirects; a redirect means your registered URL is handing delivery to a second, unverified address, so the subscription is disabled immediately.
- **`410 Gone`** — **not retried.** Treated as your own signal to stop sending; the subscription is disabled immediately.

If every attempt in the curve fails, the delivery is marked permanently failed (no further retries for that event), and if failures continue across separate deliveries for **5 days straight**, the subscription itself is disabled — check `GET /webhooks/:id` for `disabled_reason` and re-enable it once your endpoint is healthy again. The same field also reports `private_address` if your URL's DNS record ever changes to resolve inside a private network — Brizz re-validates the destination on every attempt, not only at registration.

## Rotating your signing secret

`POST https://platform.brizz.dev/api/v1/webhooks/:id/rotate-secret`

```bash
curl -X POST "https://platform.brizz.dev/api/v1/webhooks/ID/rotate-secret" \
  -H "Authorization: Token $BRIZZ_PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"previous_secret_ttl_hours": 24}'
```

Returns a new `secret`, shown once, same as creation. During the TTL you specify, every delivery is signed with **both** the old and new secret, space-joined in one `webhook-signature` header — so you can swap your verifier over to the new secret at any point inside that window without a single delivery failing to verify in between.

- `previous_secret_ttl_hours` is optional, **0–168** (7 days), default **24**.
- `0` means an immediate cutover — use this if you're rotating because the old secret leaked, not on a routine schedule.

## See also

- [API overview](/docs/api/overview.md) — base URLs, the two credentials, general error model.
- [Reading data](/docs/api/reading-data.md) — full reference for the follow-up calls above.
- [Services & configuration](/docs/admin/services-and-configuration.md) — managing subscriptions from the dashboard instead of the API.
- [Segment](/docs/integrations/segment.md) — the *inbound* webhook, where an external system pushes events *into* Brizz. Different direction, unrelated feature.

Support: support@brizz.ai


---

# Troubleshooting

Fix common setup and ingestion issues quickly.

Common issues and how to resolve them.

## Missing Sessions or Traces

If you don't see data in the dashboard:

1. **Check API Key**: Ensure your `BRIZZ_API_KEY` is correct and has write permissions.
2. **Check Environment**: Ensure you are running in a server environment (Node.js/Python), not a browser.
3. **Check Initialization Order**:
   - **Python**: `Brizz.initialize()` must run **before** `import openai`.
   - **Node.js**: `Brizz.initialize()` must run before your app makes any AI call, and every AI library you use must be listed in `instrumentModules`.
4. **Wait**: Data ingestion can take a few seconds.

## "Module loaded before instrumentation" (Node.js)

This warning means an AI library (like `openai`) was imported before Brizz could instrument it.

**Fix:**
- Move `Brizz.initialize()` to the very top of your entry file.
- Pass the library to `instrumentModules` so Brizz hooks it regardless of import order — see the [Node.js SDK docs](/docs/sdks/typescript.md#initialization).

## Vercel AI SDK Not Tracking

If using the Vercel AI SDK (`ai` package), you must explicitly enable telemetry.

**Fix:**
Add `experimental_telemetry: { isEnabled: true }` to your `generateText` or `streamText` calls.

## Authentication Errors (401)

- Verify your API key is valid.
- Ensure you are not stripping the `Authorization` header in a proxy or firewall.

## Debugging

Enable debug logging to see what the SDK is doing.

**Node.js**:
```bash
BRIZZ_LOG_LEVEL=debug node ...
```

**Python**:
Configure standard Python logging to `DEBUG` level for the `brizz` logger.

## Still Stuck?

Contact us at [support@brizz.ai](mailto:support@brizz.ai).

## See also

- [Verify it landed](/docs/get-started/verify.md) — the original checklist for confirming a trace arrived.
- [Glossary](/docs/help/glossary.md) — definitions for terms used in error messages.
- [Node.js / TypeScript SDK](/docs/sdks/typescript.md) and [Python SDK](/docs/sdks/python.md) — initialization order details per language.


---

# Glossary

Alphabetical reference for terms used throughout the Brizz docs.

Quick definitions for every term that shows up across the docs. For a longer narrative of how these fit together, see [Core concepts](/docs/introduction/concepts.md).

## Adapter

A small SDK package that adds native span shapes for a specific framework (LangChain, Vercel AI SDK, Agno, Strands). See [Choose your SDK](/docs/sdks.md).

## Attribute

A key/value pair set on a span or event. Attributes are flat (string/number/boolean), filterable in the dashboard, and intended for high-cardinality querying.

## Body

The free-form JSON payload of an event. Use it for comments, larger blobs, and context that doesn't need to be filterable.

## Custom evaluator

A rule you define that runs against conversation data and writes an [issue](#issue) when it matches. Rules can be regex, classifier output, or LLM-as-judge.

## DSN

A single-paste credential bundling bearer token, endpoint, and service name. **Server DSN** (`brizz-ing-s-…`) is for backend SDKs; **Client DSN** (`brizz-ing-c-…`) is safe to ship in browser bundles.

## Event

A discrete signal emitted from your code — a business outcome, a milestone, a feedback action. Distinct from an LLM call. See [Custom events](/docs/instrument/custom-events.md).

## External metric

A [metric](#metric) *you* report rather than one Brizz computes — an eval score, a customer rating, a latency your own system measured. See [External metrics](/docs/platform/external-metrics.md).

## Intent

A semantic cluster of similar user prompts — "cancel my subscription," "stop auto-renew," and "turn off billing" collapse into one **Cancellation** intent. See [User intents](/docs/platform/user-intents.md).

## Issue

A deduplicated problem — one row per underlying issue, with frequency, first/last seen, and affected users. Carries an issue type and a priority. See [Issues](/docs/platform/issues.md).

## Journey

An aggregated path through your agent — a sequence many sessions follow. See [User journeys](/docs/platform/user-journeys.md).

## Maturation / matured

A [session](#session) or [trace](#trace) **matures** once it has been idle longer than its maturation window — a few minutes of quiet. Brizz reads that gap as the conversation being finished and runs its analysis pass, which is what materializes the transcript and produces metrics and [issues](#issue). A session can mature more than once if the user comes back after a gap; each pass increments its `maturation_count`. See [Outbound webhooks](/docs/api/webhooks.md).

## MCP (Model Context Protocol)

An open protocol for agents and tools to talk to each other. Brizz uses MCP in two directions: [auto-instrument an MCP server you run](/docs/sdks/mcp.md), and [expose Brizz as an MCP server](/docs/integrations/mcp-server/overview.md) so AI agents can query it.

## Metric

A number scored against a session — a quality rating, a latency, a cost. Where an [event](#event) says *what happened*, a metric says *how well it went*. Brizz computes some itself; the ones you report are [external metrics](#external-metric).

## OpenTelemetry / OTel

The open standard Brizz's SDK speaks. Spans, traces, and attributes follow OTel conventions, which means raw OTel exporters can also send data to Brizz.

## PII

Personally Identifiable Information — emails, phone numbers, SSNs, etc. Brizz includes built-in masking; see [PII & privacy](/docs/instrument/pii-and-privacy.md).

## Polarity

Which direction is good for a [metric](#metric). `positive` means a higher value is better (a quality score); `negative` means a higher value is worse (a hallucination rate). It's what lets Brizz colour a gauge without being told twice.

## Session

A logical conversation thread that groups multiple traces. The session ID is whatever you set in `start_session` / `withSessionId`. See [Sessions (instrumentation)](/docs/instrument/sessions.md) and [Sessions (in the dashboard)](/docs/platform/sessions.md).

## Span

One step inside a trace — an LLM call, a tool invocation, a function execution. Traces are trees of spans.

## SSO

Single Sign-On. Brizz supports SAML 2.0 IdPs; see [SSO (JumpCloud)](/docs/admin/sso-jumpcloud.md).

## System event

A built-in event type in the dashboard (e.g., **Positive Feedback**, **Negative Feedback**). Map your custom event names to system events in **Organization Settings → Event** so filters and charts pick them up.

## Tenant

An isolated workspace within Brizz. Every user belongs to one or more tenants; data never crosses tenants. Sometimes called an *organization*.

## Trace

The record of one end-to-end operation through your agent — one user message and the chain of LLM/tool calls it triggered. A trace is a tree of [spans](#span).

## User

The person interacting with your agent. Attach `user_id` (and optionally name/email) to a session to unlock user-level analytics. See [Identify users](/docs/instrument/identify-users.md).

## Webhook

An HTTP callback between Brizz and another system. Brizz has both directions, and they're configured separately:

- **Outbound** — Brizz calls an endpoint you own when a session or trace [matures](#maturation--matured). Configured per service, signed, and retried. See [Outbound webhooks](/docs/api/webhooks.md).
- **Inbound** — an external system such as [Segment](/docs/integrations/segment.md) pushes events into Brizz at a URL Brizz provides. Configured per integration.

## See also

- [Core concepts](/docs/introduction/concepts.md) — the narrative version of this page.
- [Troubleshooting](/docs/help/troubleshooting.md) — when something's not working.
