---
title: "Add context to traces"
description: "Put sessions, customers, versions, and handled errors on your traces so judges and queries can use them."
sidebar:
  label: "Add context"
seo:
  title: "Add context to traces | Judgment Docs"
  description: "Put sessions, customers, versions, and handled errors on your traces so judges and queries can use them."
---

Judges, Issues, and SQL can only slice by what the trace records. Add context once the trace structure from [Set up tracing](/documentation/connect/set-up-tracing) looks right.

## Sessions, customers, and end users

Call these inside the root span while it is active. Each one writes the attribute on the active span and carries it to child spans through baggage. With no active span they do nothing.

**Python**

```python
from judgeval import Tracer

@Tracer.observe(span_type="agent", span_name="lunch_order")
def handle_request(chat_id: str, office_id: str, user_id: str, message: str) -> str:
    Tracer.set_session_id(chat_id)
    Tracer.set_customer_id(office_id)
    Tracer.set_customer_user_id(user_id)
    return run_agent(message)
```

**TypeScript**

```typescript
import { Tracer } from "judgeval";

const handleRequest = Tracer.observe(
  async function handleRequest(chatId: string, officeId: string, userId: string, message: string) {
    Tracer.setSessionId(chatId);
    Tracer.setCustomerId(officeId);
    Tracer.setCustomerUserId(userId);
    return runAgent(message);
  },
  { spanType: "agent", spanName: "lunch_order" },
);
```

Judgment assigns a trace to a session from the root span's `judgment.session_id` only. A session ID set only on child spans does not group the trace. Use a stable ID such as the chat or job ID, and reuse it across retries and follow-ups, which are new traces in the same session.

`customer_id` is the account or tenant. `customer_user_id` is the person. Both show up as filters and SQL columns.

## Environment

Pass `environment` to `Tracer.init`. The SDK stores it as the OpenTelemetry resource attribute `deployment.environment.name`, so it applies to every span from that process. Raw OTLP exporters set the same resource attribute. There is no span attribute or header for it. Traces without one show as **No environment**.

## Agent version

Set `judgment.agent_version` on the root span to compare versions, for example `lunch-agent-v1` against `lunch-agent-v2`. No SDK helper sets it; use `set_attribute`:

```python
Tracer.set_attribute("judgment.agent_version", "lunch-agent-v2")  # TypeScript: Tracer.setAttribute(...)
```

In Judgment's virtual SQL (vsql), `telemetry.traces.agent_version` is the root span's value, and `telemetry.spans.agent_version` is each span's own value.

## Tags

Tags are not span attributes, and there is no `judgment.*` key for them. `Tracer.tag("big-order")` or `Tracer.tag(["catering", "over-budget"])` sends the tags in the background to the trace tags API (`POST /v1/projects/{projectId}/traces/{traceId}/tags`, developer role) for the current trace. You can also tag a trace from its menu in the trace view.

## Service name

Both SDKs set the resource attribute `service.name` to the project name. When several services send to one project, give each its own name: `Tracer.init(project_name="lunch-order-agent", resource_attributes={"service.name": "menu-service"})` in Python, or `resourceAttributes: { "service.name": "menu-service" }` in the TypeScript `Tracer.init` options. Values you pass win over the defaults.

## Input and output

`Tracer.observe` records the function's arguments as `judgment.input` before the body runs and its return value as `judgment.output` after the body returns. That ordering decides what an override does:

- `set_input` / `setInput` inside the function replaces the captured input.
- `set_output` / `setOutput` inside the function is overwritten by the return value when output recording is on. To record a different output, turn recording off with `record_output=False` (Python) or `recordOutput: false` (TypeScript), then call `set_output`.

Use the same pattern with `record_input=False` to keep sensitive or very large arguments out of the trace. A span over 8 MiB of attributes rejects the whole export request.

## Custom attributes

`Tracer.set_attribute` / `setAttribute` writes to the current span only. `set_propagating_attribute` / `setPropagatingAttribute` also copies the value to child spans. Use your own namespace, such as `lunch.headcount`; the propagating helpers ignore keys that start with `judgment.`.

## Mark handled failures

An uncaught exception marks the observed span as an error. A failure your code handles does not, so mark it. When a tool returns an error and the agent moves on, mark the tool span, and leave the root successful if the run still succeeded.

**Python**

```python
from judgeval import Tracer
from opentelemetry.trace import Status, StatusCode

@Tracer.observe(span_type="tool", span_name="get_menu")
def get_menu(restaurant: str) -> dict:
    result = menu_api.fetch(restaurant)
    if result.get("status") == 504:
        Tracer.get_current_span().set_status(Status(StatusCode.ERROR, "504 Gateway Timeout"))
    return result
```

**TypeScript**

```typescript
import { Tracer } from "judgeval";

const getMenu = Tracer.observe(
  async function getMenu(restaurant: string) {
    const result = await menuApi.fetch(restaurant);
    if (result.status === 504) Tracer.setError(new Error("504 Gateway Timeout"));
    return result;
  },
  { spanType: "tool", spanName: "get_menu" },
);
```

## Model usage without a wrapper

When no wrapper or integration covers a model call, put it in its own `llm` span and call `recordLLMMetadata` with any of `model`, `provider`, `non_cached_input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, and `total_cost_usd`. Judgment calculates cost from provider, model, and tokens, so set `total_cost_usd` only when the provider reports one. Reasoning and image token keys have no helper field; set them with `set_attribute`. See [Attribute keys and mappers](/documentation/reference/attribute-keys-and-mappers).
