Add context to traces
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 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.
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)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:
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/setInputinside the function replaces the captured input.set_output/setOutputinside the function is overwritten by the return value when output recording is on. To record a different output, turn recording off withrecord_output=False(Python) orrecordOutput: false(TypeScript), then callset_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.
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 resultimport { 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.