---
title: "Agent Tracing"
description: "Learn how to manually instrument AI agents in browser applications."
url: https://docs.sentry.io/platforms/javascript/agent-tracing-browser/
---

# Browser AI Tracing | Sentry for JavaScript

With [Sentry Agent Tracing](https://docs.sentry.io/product/agents/dashboards.md), you can monitor and debug your AI systems with full-stack context. You'll be able to track key insights like token usage, latency, tool usage, and error rates. Agent Tracing data will be fully connected to your other Sentry data like logs, errors, and traces.

## [Prerequisites](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#prerequisites)

Before setting up Agent Tracing, ensure you have [tracing enabled](https://docs.sentry.io/platforms/javascript/tracing.md) in your Sentry configuration.

**Browser applications require manual instrumentation.** Unlike Node.js applications, the JavaScript SDK does not provide automatic instrumentation for AI libraries in the browser.

## [Using Integration Helpers](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#using-integration-helpers)

For supported AI libraries, Sentry provides manual instrumentation helpers that simplify span creation. These helpers handle the complexity of creating properly structured spans with the correct attributes.

**Supported libraries:**

* [OpenAI](https://docs.sentry.io/platforms/javascript/configuration/integrations/openai.md)
* [Anthropic](https://docs.sentry.io/platforms/javascript/configuration/integrations/anthropic.md)
* [Google Gen AI SDK](https://docs.sentry.io/platforms/javascript/configuration/integrations/google-genai.md)
* [LangChain](https://docs.sentry.io/platforms/javascript/configuration/integrations/langchain.md)
* [LangGraph](https://docs.sentry.io/platforms/javascript/configuration/integrations/langgraph.md)

Each integration page includes a manual-instrumentation example with options like `recordInputs` and `recordOutputs`.

```javascript
import * as Sentry from "<sdk-package-name>";
import OpenAI from "openai";

const client = Sentry.instrumentOpenAiClient(
  new OpenAI({ apiKey: "...", dangerouslyAllowBrowser: true }),
  {
    recordInputs: true,
    recordOutputs: true,
  },
);

// All calls are now instrumented
const response = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "Hello!" }],
});
```

## [Manual Span Creation](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#manual-span-creation)

If you're using a library that Sentry doesn't provide helpers for, create spans manually. Spans need well-defined names and data attributes so agent data shows up correctly in the [AI Agents Dashboards](https://sentry.io/orgredirect/organizations/:orgslug/dashboards/?filter=onlyPrebuilt\&query=agents\&sort=mostPopular).

### [Span Hierarchy](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#span-hierarchy)

When instrumenting an agent loop, spans nest like this:

```bash
── invoke_agent My Agent          (gen_ai.invoke_agent)
   ├── chat gpt-4o                (gen_ai.chat)         ← 1st LLM call
   ├── execute_tool get_weather   (gen_ai.execute_tool)  ← tool run
   ├── chat gpt-4o                (gen_ai.chat)         ← 2nd LLM call
   └── ...
```

`gen_ai.invoke_agent` is the container. `gen_ai.chat` and `gen_ai.execute_tool` spans are its children (siblings of each other). A `gen_ai.chat` span can also appear without an agent parent for standalone LLM calls.

### [Common Attributes](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#common-attributes)

Set these **when the span starts** (before the model or tool call), so head-based sampling can see them:

* `gen_ai.operation.name` — required; classifies the span (`chat`, `invoke_agent`, `execute_tool`, …)
* `gen_ai.provider.name` — e.g. `openai`, `anthropic`
* `gen_ai.request.model` — requested model (pass the **raw** provider string)
* `gen_ai.agent.name` / `gen_ai.tool.name` — when applicable

Complex values (messages, tool definitions, arrays) must be **JSON strings** — span attributes only accept primitives.

For manual spans, prompt/response/tool content is whatever you set on the span. Omit those attributes (or gate them yourself) when you do not want content captured. Integration helpers honor `recordInputs` / `recordOutputs` (and related defaults).

### [AI Request Span](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#ai-request-span)

This span represents a request to an LLM model or service that generates a response based on the input prompt.

```javascript
const messages = [
  { role: "user", parts: [{ type: "text", content: "Tell me a joke" }] },
];
const tools = [
  { name: "get_weather", description: "Get weather for a city" },
];

await Sentry.startSpan(
  {
    op: "gen_ai.chat",
    name: "chat o3-mini",
    attributes: {
      "gen_ai.operation.name": "chat",
      "gen_ai.request.model": "o3-mini",
      "gen_ai.provider.name": "openai",
      "gen_ai.agent.name": "Weather Agent", // when this call is under an agent
      "gen_ai.system_instructions": "You are a helpful assistant.",
      "gen_ai.tool.definitions": JSON.stringify(tools),
      "gen_ai.input.messages": JSON.stringify(messages),
    },
  },
  async (span) => {
    // Call your model provider; map its response into span attributes below
    const result = await yourLLMClient.chat({
      model: "o3-mini",
      messages,
    });

    span.setAttribute("gen_ai.response.model", result.model);
    span.setAttribute("gen_ai.response.id", result.id);
    span.setAttribute(
      "gen_ai.output.messages",
      JSON.stringify([
        {
          role: "assistant",
          parts: [{ type: "text", content: result.text }],
        },
      ]),
    );
    span.setAttribute(
      "gen_ai.response.finish_reasons",
      JSON.stringify([result.finishReason]),
    );
    span.setAttribute(
      "gen_ai.usage.input_tokens",
      result.usage.inputTokens,
    );
    span.setAttribute(
      "gen_ai.usage.output_tokens",
      result.usage.outputTokens,
    );
    // If the provider reports cached tokens, record them as a subset of input tokens
    if (result.usage.cachedInputTokens != null) {
      span.setAttribute(
        "gen_ai.usage.cache_read.input_tokens",
        result.usage.cachedInputTokens,
      );
    }
    return result;
  },
);
```

Keep system prompts in `gen_ai.system_instructions`, not inside `gen_ai.input.messages`. [Conversation titles](https://docs.sentry.io/product/agents/conversations.md#conversation-titles) are derived from the first user message in the input messages.

AI Request span attributes

* The span `op` (transaction mode) or the span's `sentry.op` attribute (stream mode) MUST be `"gen_ai.{gen_ai.operation.name}"`. (e.g. `"gen_ai.chat"`)
* The span `name` SHOULD be `"{gen_ai.operation.name} {gen_ai.request.model}"`. (e.g. `"chat o3-mini"`)
* The `gen_ai.operation.name` attribute MUST be `"chat"`, `"embeddings"`, `"generate_content"` or `"text_completion"`.
* The `gen_ai.provider.name` attribute MUST be the Generative AI product as identified by the client or server instrumentation. (e.g. `"openai"`)
* The `gen_ai.request.model` attribute MUST be the requested model. (e.g. `"o3-mini"`)
* The `gen_ai.response.model` attribute MUST be the concrete model that responded. (e.g. `"gpt-4o-2024-08-06"`)
* If the request originates from an agent, `gen_ai.agent.name` SHOULD be set to the agent's name. (e.g. `"Weather Agent"`)
* If relevant, `gen_ai.pipeline.name` SHOULD be set to the name of the AI workflow or pipeline. (e.g. `"weather-pipeline"`)

### [Request Attributes](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#request-attributes)

| Data Attribute                     | Type   | Requirement Level | Description                                                                                                     | Example                                                               |
| ---------------------------------- | ------ | ----------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `gen_ai.input.messages`            | string | optional          | List of message objects sent to the LLM. **\[0]**, **\[1]**                                                     | `'[{"role": "user", "parts": [{"type": "text", "content": "..."}]}]'` |
| `gen_ai.tool.definitions`          | string | optional          | List of objects describing the available tools. **\[0]**                                                        | `'[{"name": "random_number", "description": "..."}]'`                 |
| `gen_ai.system_instructions`       | string | optional          | The system instructions passed to the model.                                                                    | `"You are a helpful assistant."`                                      |
| `gen_ai.request.frequency_penalty` | float  | optional          | Model configuration parameter.                                                                                  | `0.5`                                                                 |
| `gen_ai.request.max_tokens`        | int    | optional          | Model configuration parameter.                                                                                  | `500`                                                                 |
| `gen_ai.request.seed`              | string | optional          | Seed for reproducible outputs.                                                                                  | `"12345"`                                                             |
| `gen_ai.request.temperature`       | float  | optional          | Model configuration parameter.                                                                                  | `0.1`                                                                 |
| `gen_ai.request.top_k`             | int    | optional          | Limits model to K most likely next tokens.                                                                      | `40`                                                                  |
| `gen_ai.request.top_p`             | float  | optional          | Model configuration parameter.                                                                                  | `0.7`                                                                 |
| `gen_ai.request.presence_penalty`  | float  | optional          | Model configuration parameter.                                                                                  | `0.5`                                                                 |
| `gen_ai.request.reasoning.level`   | string | optional          | The reasoning or thinking effort level requested for a GenAI model. Supported values vary by provider.          | `"medium"`                                                            |
| `gen_ai.request.messages`          | string | optional          | **Deprecated.** Use `gen_ai.input.messages` instead. List of message objects sent to the LLM. **\[0]**          | `'[{"role": "system", "content": "..."}]'`                            |
| `gen_ai.request.available_tools`   | string | optional          | **Deprecated.** Use `gen_ai.tool.definitions` instead. List of objects describing the available tools. **\[0]** | `'[{"name": "random_number", "description": "..."}]'`                 |

### [Response Attributes](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#response-attributes)

| Data Attribute                        | Type    | Requirement Level | Description                                                                                                         | Example                                                                      |
| ------------------------------------- | ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `gen_ai.response.model`               | string  | required          | The concrete model that responded (may differ from `gen_ai.request.model`).                                         | `"gpt-4o-2024-08-06"`                                                        |
| `gen_ai.output.messages`              | string  | optional          | Stringified array of message objects representing the model's output. **\[0]**, **\[1]**                            | `'[{"role": "assistant", "parts": [{"type": "text", "content": "..."}]}]'`   |
| `gen_ai.response.finish_reasons`      | string  | optional          | Stringified array of reasons the model stopped generating. **\[0]**                                                 | `'["stop"]'`                                                                 |
| `gen_ai.response.id`                  | string  | optional          | Unique identifier for the completion.                                                                               | `"chatcmpl-abc123"`                                                          |
| `gen_ai.response.streaming`           | boolean | optional          | Whether the response was streamed.                                                                                  | `true`                                                                       |
| `gen_ai.response.time_to_first_chunk` | double  | optional          | Seconds until first response chunk in streaming.                                                                    | `0.5`                                                                        |
| `gen_ai.response.text`                | string  | optional          | **Deprecated.** Use `gen_ai.output.messages` instead. The text representation of the model's responses.             | `"The weather in Paris is rainy"`                                            |
| `gen_ai.response.tool_calls`          | string  | optional          | **Deprecated.** Use `gen_ai.output.messages` instead. The tool calls in the model's response. **\[0]**              | `'[{"name": "random_number", "type": "function_call", "arguments": "..."}]'` |
| `gen_ai.response.time_to_first_token` | double  | optional          | **Deprecated.** Use `gen_ai.response.time_to_first_chunk` instead. Seconds until first response chunk in streaming. | `0.5`                                                                        |

### [Token Usage](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#token-usage)

| Data Attribute                             | Type | Requirement Level | Description                                                                                                                    | Example |
| ------------------------------------------ | ---- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `gen_ai.usage.input_tokens`                | int  | optional          | The number of tokens used in the AI input (prompt), including cached tokens. **\[2]**                                          | `60`    |
| `gen_ai.usage.cache_read.input_tokens`     | int  | optional          | The number of cached tokens used in the AI input (prompt).                                                                     | `50`    |
| `gen_ai.usage.cache_creation.input_tokens` | int  | optional          | Tokens written to cache when processing input.                                                                                 | `20`    |
| `gen_ai.usage.output_tokens`               | int  | optional          | The number of tokens used in the AI output, including reasoning tokens. **\[3]**                                               | `130`   |
| `gen_ai.usage.reasoning.output_tokens`     | int  | optional          | The number of tokens used for reasoning.                                                                                       | `30`    |
| `gen_ai.usage.total_tokens`                | int  | optional          | The sum of `gen_ai.usage.input_tokens` and `gen_ai.usage.output_tokens`.                                                       | `190`   |
| `gen_ai.usage.input_tokens.cached`         | int  | optional          | **Deprecated.** Use `gen_ai.usage.cache_read.input_tokens` instead. The number of cached tokens used in the AI input (prompt). | `50`    |
| `gen_ai.usage.input_tokens.cache_write`    | int  | optional          | **Deprecated.** Use `gen_ai.usage.cache_creation.input_tokens` instead. Tokens written to cache when processing input.         | `20`    |
| `gen_ai.usage.output_tokens.reasoning`     | int  | optional          | **Deprecated.** Use `gen_ai.usage.reasoning.output_tokens` instead. The number of tokens used for reasoning.                   | `30`    |

* **\[0]:** Span attributes only allow primitive data types. This means you need to use a stringified version of a list of dictionaries. Do NOT set `[{"foo": "bar"}]` but rather the string `'[{"foo": "bar"}]'` (must be parsable JSON).
* **\[1]:** Messages use the format `{role, parts}` where `parts` is an array of typed objects: `[{"role": "user", "parts": [{"type": "text", "content": "..."}]}]`. The `role` must be `"user"`, `"assistant"`, `"tool"`, or `"system"`. Each part has a `type`; common types include `"text"` (user-visible content), `"reasoning"` (internal thinking/chain-of-thought), `"tool_call"`, and `"tool_call_response"`. Use `{"type": "reasoning", "content": "..."}` for the model's thinking output — Sentry surfaces it separately and filters it out of the user-facing Conversations view, so do not represent thinking content as a `"text"` part. For backwards compatibility, the legacy format `{role, content}` is also accepted.
* **\[2]:** Cached tokens are a subset of input tokens; `gen_ai.usage.input_tokens` includes `gen_ai.usage.cache_read.input_tokens`.
* **\[3]:** Reasoning tokens are a subset of output tokens; `gen_ai.usage.output_tokens` includes `gen_ai.usage.reasoning.output_tokens`.

#### [Message parts](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#message-parts)

Messages use `{role, parts}` where each part has a `type`. Common types:

* `text` — user-visible content
* `reasoning` — internal thinking (not shown in the user-facing Conversations view)
* `tool_call` / `tool_call_response` — tool invocations linked by a shared `id`

Unknown part types are not shown prominently in the Conversations UI. They remain available only in the raw span attribute values.

##### [Thinking / reasoning](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#thinking--reasoning)

Models with extended thinking (such as Anthropic's `thinking` blocks, Gemini's `thought`, or DeepSeek's `reasoning_content`) produce internal reasoning that isn't part of the user-visible reply. Represent this as a `reasoning` part alongside the user-facing `text` part — don't fold thinking into `text`.

```javascript
span.setAttribute(
  "gen_ai.output.messages",
  JSON.stringify([
    {
      role: "assistant",
      parts: [
        { type: "reasoning", content: "6 times 7 is 42." },
        { type: "text", content: "The answer is 42." },
      ],
    },
  ]),
);
span.setAttribute("gen_ai.usage.output_tokens", result.usage.outputTokens);
// Reasoning tokens are a subset of output tokens
span.setAttribute(
  "gen_ai.usage.reasoning.output_tokens",
  result.usage.reasoningTokens,
);
```

When previous thinking is fed back into a multi-turn request, include the same `reasoning` parts in assistant messages within `gen_ai.input.messages`.

##### [Tool calls in messages](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#tool-calls-in-messages)

Link a tool request to its result with the same `id`:

```javascript
// Model asked to call a tool
span.setAttribute(
  "gen_ai.output.messages",
  JSON.stringify([
    {
      role: "assistant",
      parts: [
        {
          type: "tool_call",
          id: "call_abc",
          name: "get_weather",
          arguments: { location: "Paris" },
        },
      ],
    },
  ]),
);

// Later chat span: tool result fed back to the model
const inputWithTool = [
  {
    role: "user",
    parts: [{ type: "text", content: "Weather in Paris?" }],
  },
  {
    role: "assistant",
    parts: [
      {
        type: "tool_call",
        id: "call_abc",
        name: "get_weather",
        arguments: { location: "Paris" },
      },
    ],
  },
  {
    role: "tool",
    parts: [
      {
        type: "tool_call_response",
        id: "call_abc",
        name: "get_weather",
        content: '{"temp_c": 18}',
      },
    ],
  },
];
span.setAttribute("gen_ai.input.messages", JSON.stringify(inputWithTool));
```

### [Invoke Agent Span](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#invoke-agent-span)

This span represents the execution of an AI agent, capturing the full lifecycle from receiving a task to producing a final response.

For a complete guide on naming agents across all supported frameworks, see [Naming Your Agents](https://docs.sentry.io/product/agents/naming.md).

```javascript
const messages = [
  {
    role: "user",
    parts: [{ type: "text", content: "What's the weather in Paris?" }],
  },
];
const tools = [
  { name: "get_weather", description: "Get weather for a city" },
];

await Sentry.startSpan(
  {
    op: "gen_ai.invoke_agent",
    name: "invoke_agent Weather Agent",
    attributes: {
      "gen_ai.operation.name": "invoke_agent",
      "gen_ai.agent.name": "Weather Agent",
      "gen_ai.provider.name": "openai",
      "gen_ai.request.model": "o3-mini",
      "gen_ai.system_instructions": "You are a weather assistant.",
      "gen_ai.tool.definitions": JSON.stringify(tools),
      "gen_ai.input.messages": JSON.stringify(messages),
    },
  },
  async (span) => {
    // myAgent is your agent runner; expect { output, usage: { inputTokens, outputTokens } }
    const result = await myAgent.run();

    span.setAttribute(
      "gen_ai.output.messages",
      JSON.stringify([
        {
          role: "assistant",
          parts: [{ type: "text", content: String(result.output) }],
        },
      ]),
    );
    span.setAttribute(
      "gen_ai.usage.input_tokens",
      result.usage.inputTokens,
    );
    span.setAttribute(
      "gen_ai.usage.output_tokens",
      result.usage.outputTokens,
    );
    return result;
  },
);
```

Child `gen_ai.chat` spans should also set `gen_ai.agent.name` so model usage can be attributed per agent.

Invoke Agent span attributes

Describes AI agent invocation.

* The span `op` (transaction mode) or the span's `sentry.op` attribute (stream mode) MUST be `"gen_ai.invoke_agent"`.
* The span `name` SHOULD be `"invoke_agent {gen_ai.agent.name}"`.
* The `gen_ai.operation.name` attribute MUST be `"invoke_agent"`.
* The `gen_ai.agent.name` attribute SHOULD be set to the agent's name. (e.g. `"Weather Agent"`)
* If relevant, `gen_ai.pipeline.name` SHOULD be set to the name of the AI workflow or pipeline the agent belongs to.

Additional attributes on the span:

### [Request Attributes](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#request-attributes)

| Data Attribute                   | Type   | Requirement Level | Description                                                                                                     | Example                                                               |
| -------------------------------- | ------ | ----------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `gen_ai.input.messages`          | string | optional          | List of message objects given to the agent. **\[0]**, **\[1]**                                                  | `'[{"role": "user", "parts": [{"type": "text", "content": "..."}]}]'` |
| `gen_ai.tool.definitions`        | string | optional          | List of objects describing the available tools. **\[0]**                                                        | `'[{"name": "random_number", "description": "..."}]'`                 |
| `gen_ai.system_instructions`     | string | optional          | The system instructions passed to the model.                                                                    | `"You are a helpful assistant."`                                      |
| `gen_ai.pipeline.name`           | string | optional          | The name of the AI workflow or pipeline the agent belongs to.                                                   | `"weather-pipeline"`                                                  |
| `gen_ai.request.messages`        | string | optional          | **Deprecated.** Use `gen_ai.input.messages` instead. List of message objects given to the agent. **\[0]**       | `'[{"role": "system", "content": "..."}]'`                            |
| `gen_ai.request.available_tools` | string | optional          | **Deprecated.** Use `gen_ai.tool.definitions` instead. List of objects describing the available tools. **\[0]** | `'[{"name": "random_number", "description": "..."}]'`                 |

### [Response Attributes](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#response-attributes)

| Data Attribute               | Type   | Requirement Level | Description                                                                                            | Example                                                                      |
| ---------------------------- | ------ | ----------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| `gen_ai.output.messages`     | string | optional          | Stringified array of message objects representing the agent's output. **\[0]**, **\[1]**               | `'[{"role": "assistant", "parts": [{"type": "text", "content": "..."}]}]'`   |
| `gen_ai.response.text`       | string | optional          | **Deprecated.** Use `gen_ai.output.messages` instead. The text representation of the agent's response. | `"The weather in Paris is rainy"`                                            |
| `gen_ai.response.tool_calls` | string | optional          | **Deprecated.** Use `gen_ai.output.messages` instead. The tool calls in the model's response. **\[0]** | `'[{"name": "random_number", "type": "function_call", "arguments": "..."}]'` |

### [Token Usage](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#token-usage)

| Data Attribute                             | Type | Requirement Level | Description                                                                                                                    | Example |
| ------------------------------------------ | ---- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------- |
| `gen_ai.usage.input_tokens`                | int  | optional          | The number of tokens used in the AI input (prompt), including cached tokens. **\[2]**                                          | `60`    |
| `gen_ai.usage.cache_read.input_tokens`     | int  | optional          | The number of cached tokens used in the AI input (prompt).                                                                     | `50`    |
| `gen_ai.usage.cache_creation.input_tokens` | int  | optional          | Tokens written to cache when processing input.                                                                                 | `20`    |
| `gen_ai.usage.output_tokens`               | int  | optional          | The number of tokens used in the AI output, including reasoning tokens. **\[3]**                                               | `130`   |
| `gen_ai.usage.reasoning.output_tokens`     | int  | optional          | The number of tokens used for reasoning.                                                                                       | `30`    |
| `gen_ai.usage.total_tokens`                | int  | optional          | The sum of `gen_ai.usage.input_tokens` and `gen_ai.usage.output_tokens`.                                                       | `190`   |
| `gen_ai.usage.input_tokens.cached`         | int  | optional          | **Deprecated.** Use `gen_ai.usage.cache_read.input_tokens` instead. The number of cached tokens used in the AI input (prompt). | `50`    |
| `gen_ai.usage.input_tokens.cache_write`    | int  | optional          | **Deprecated.** Use `gen_ai.usage.cache_creation.input_tokens` instead. Tokens written to cache when processing input.         | `20`    |
| `gen_ai.usage.output_tokens.reasoning`     | int  | optional          | **Deprecated.** Use `gen_ai.usage.reasoning.output_tokens` instead. The number of tokens used for reasoning.                   | `30`    |

* **\[0]:** Span attributes only allow primitive data types. This means you need to use a stringified version of a list of dictionaries. Do NOT set `[{"foo": "bar"}]` but rather the string `'[{"foo": "bar"}]'` (must be parsable JSON).
* **\[1]:** Messages use the format `{role, parts}` where `parts` is an array of typed objects: `[{"role": "user", "parts": [{"type": "text", "content": "..."}]}]`. The `role` must be `"user"`, `"assistant"`, `"tool"`, or `"system"`. Each part has a `type`; common types include `"text"` (user-visible content), `"reasoning"` (internal thinking/chain-of-thought), `"tool_call"`, and `"tool_call_response"`. Use `{"type": "reasoning", "content": "..."}` for the model's thinking output — Sentry surfaces it separately and filters it out of the user-facing Conversations view, so do not represent thinking content as a `"text"` part. For backwards compatibility, the legacy format `{role, content}` is also accepted.
* **\[2]:** Cached tokens are a subset of input tokens; `gen_ai.usage.input_tokens` includes `gen_ai.usage.cache_read.input_tokens`.
* **\[3]:** Reasoning tokens are a subset of output tokens; `gen_ai.usage.output_tokens` includes `gen_ai.usage.reasoning.output_tokens`.

### [Execute Tool Span](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#execute-tool-span)

This span represents the execution of a tool or function that was requested by an AI model, including the input arguments and resulting output.

```javascript
await Sentry.startSpan(
  {
    op: "gen_ai.execute_tool",
    name: "execute_tool get_weather",
    attributes: {
      "gen_ai.operation.name": "execute_tool",
      "gen_ai.tool.name": "get_weather",
      "gen_ai.tool.description": "Get weather for a city",
      "gen_ai.tool.call.arguments": JSON.stringify({ location: "Paris" }),
    },
  },
  async (span) => {
    try {
      const result = await getWeather({ location: "Paris" });
      span.setAttribute("gen_ai.tool.call.result", JSON.stringify(result));
      return result;
    } catch (error) {
      span.setStatus({ code: 2, message: "internal_error" });
      span.setAttribute(
        "error.type",
        error instanceof Error ? error.constructor.name : "Error",
      );
      throw error;
    }
  },
);
```

Marking failed tools with an error status populates the Tool Errors widget.

Execute Tool span attributes

Describes a tool execution.

* The span `op` (transaction mode) or the span's `sentry.op` attribute (stream mode) MUST be `"gen_ai.execute_tool"`.
* The span `name` SHOULD be `"execute_tool {gen_ai.tool.name}"`. (e.g. `"execute_tool query_database"`)
* The `gen_ai.operation.name` attribute MUST be `"execute_tool"`.
* The `gen_ai.tool.name` attribute SHOULD be set to the name of the tool. (e.g. `"query_database"`)

Additional attributes on the span:

| Data Attribute               | Type   | Requirement Level | Description                                                                                           | Example                                    |
| ---------------------------- | ------ | ----------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `gen_ai.tool.name`           | string | optional          | Name of the tool executed.                                                                            | `"random_number"`                          |
| `gen_ai.tool.call.arguments` | string | optional          | Arguments of the tool call (stringified JSON).                                                        | `"{\"max\":10}"`                           |
| `gen_ai.tool.call.result`    | string | optional          | Result of the tool call (stringified).                                                                | `"7"`                                      |
| `gen_ai.tool.description`    | string | optional          | Description of the tool executed.                                                                     | `"Tool returning a random number"`         |
| `gen_ai.tool.type`           | string | optional          | The type of the tools.                                                                                | `"function"`; `"extension"`; `"datastore"` |
| `gen_ai.tool.input`          | string | optional          | **Deprecated.** Use `gen_ai.tool.call.arguments` instead. Input given to the executed tool as string. | `"{\"max\":10}"`                           |
| `gen_ai.tool.output`         | string | optional          | **Deprecated.** Use `gen_ai.tool.call.result` instead. The output from the tool.                      | `"7"`                                      |

### [Streaming Responses](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#streaming-responses)

When the model streams tokens, keep the span open until the stream finishes (including when you yield chunks to the client). Set response attributes when you have the final usage and text:

* `gen_ai.response.streaming` — `true`
* `gen_ai.response.time_to_first_chunk` — seconds until the first chunk
* `gen_ai.response.tokens_per_second` — output throughput, if you can measure it
* `gen_ai.output.messages`, token usage, and `gen_ai.response.model` — same as a non-streaming call, once the stream completes

Use `Sentry.startInactiveSpan` so the span outlives the initial call, and `Sentry.withActiveSpan` so child spans nest correctly. End the span when the stream completes or errors:

```javascript
async function callLLMStreaming(model, messages) {
  const span = Sentry.startInactiveSpan({
    name: `chat ${model}`,
    op: "gen_ai.chat",
    attributes: {
      "gen_ai.operation.name": "chat",
      "gen_ai.request.model": model,
      "gen_ai.input.messages": JSON.stringify(messages),
    },
  });

  try {
    const stream = await Sentry.withActiveSpan(span, () =>
      yourLLMClient.stream({ model, messages }),
    );

    // Accumulate from chunk events — stream "end" has no payload
    let text = "";
    let usage = { inputTokens: 0, outputTokens: 0 };
    let responseModel = model;

    stream.on("data", (chunk) => {
      // Map chunk fields to your provider's shape
      if (chunk.text) {
        text += chunk.text;
      }
      if (chunk.usage) {
        usage = chunk.usage;
      }
      if (chunk.model) {
        responseModel = chunk.model;
      }
    });

    stream.on("end", () => {
      span.setAttribute(
        "gen_ai.output.messages",
        JSON.stringify([
          {
            role: "assistant",
            parts: [{ type: "text", content: text }],
          },
        ]),
      );
      span.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens);
      span.setAttribute("gen_ai.usage.output_tokens", usage.outputTokens);
      span.setAttribute("gen_ai.response.model", responseModel);
      span.setAttribute("gen_ai.response.streaming", true);
      span.end();
    });

    stream.on("error", (error) => {
      span.setStatus({ code: 2, message: "internal_error" });
      span.setAttribute(
        "error.type",
        error instanceof Error ? error.constructor.name : "Error",
      );
      span.end();
    });
    return stream;
  } catch (error) {
    span.setStatus({ code: 2, message: "internal_error" });
    span.setAttribute(
      "error.type",
      error instanceof Error ? error.constructor.name : "Error",
    );
    span.end();
    throw error;
  }
}
```

## [Token Usage and Cost Gotchas](https://docs.sentry.io/platforms/javascript/agent-tracing-browser.md#token-usage-and-cost-gotchas)

When manually setting token attributes, be aware of how Sentry uses them to [calculate model costs](https://docs.sentry.io/product/agents/costs.md).

**Cached and reasoning tokens are subsets, not separate counts.** `gen_ai.usage.input_tokens` is the **total** input token count that already includes any cached tokens. Similarly, `gen_ai.usage.output_tokens` already includes reasoning tokens. Sentry subtracts the cached/reasoning counts from the totals to compute the "raw" portion, so reporting them incorrectly can produce wrong or negative costs.

For example, say your LLM call uses 100 input tokens total, 90 of which were served from cache. Using a standard rate of $0.01 per token and a cached rate of $0.001 per token:

**Correct** — `input_tokens` is the total (includes cached):

* `gen_ai.usage.input_tokens = 100`
* `gen_ai.usage.cache_read.input_tokens = 90`
* Sentry calculates: `(100 - 90) × $0.01 + 90 × $0.001` = `$0.10 + $0.09` = **$0.19** ✓

**Wrong** — `input_tokens` set to only the non-cached tokens, making cached larger than total:

* `gen_ai.usage.input_tokens = 10`
* `gen_ai.usage.cache_read.input_tokens = 90`
* Sentry calculates: `(10 - 90) × $0.01 + 90 × $0.001` = `−$0.80 + $0.09` = **−$0.71**

Because `input_tokens.cached` (90) is larger than `input_tokens` (10), the subtraction goes negative, resulting in a negative total cost.

The same applies to `gen_ai.usage.output_tokens` and `gen_ai.usage.reasoning.output_tokens`.

Sentry derives [model cost](https://docs.sentry.io/product/agents/costs.md) from the model name and token counts. You do not need to set `gen_ai.cost.*` attributes. Pass the raw provider model string unchanged so pricing can resolve.
