niceeval view it lists every model call and tool execution turn by turn, with each one’s duration and tokens.
If your application already sends OTel traces, you already have this data. AI SDK telemetry, LangGraph traces exported to LangSmith, OpenLLMetry or OpenInference auto-instrumentation, and your own instrumentation following GenAI semantics all count. Have the application send a copy of its spans to NiceEval too. No business code in the application changes, so this is still a non-invasive integration; see Tier.
The waterfall is only for viewing and does not take part in scoring. Assertions such as t.calledTool and t.maxTokens still read the events and usage the Adapter returns from send. Missing or late spans only make the waterfall incomplete; they never change pass or fail.
Where spans come from and where they go
When evals run, NiceEval starts an OTLP receiver on the local machine. Spans sent by the application are grouped bysend turn, normalized to GenAI semantics, and saved with the Attempt’s results; niceeval view then draws the waterfall from them. Spans do not take part in assertions and do not affect the Agent’s behavior, execution errors, or verdict. Missing instrumentation, late spans, or a lost batch only make the waterfall incomplete.
Send spans to NiceEval
The integration has three changes: the Adapter carries each turn’s trace header, the application knows where to send, and the application’s instrumentation turns on export. 1. The Adapter carriestraceparent. Write send as usual with the same event mapping, plus one line: send this turn’s traceparent to the application with the request.
uiMessageStreamAgent you do not need this line; it adds ctx.telemetry.headers to the request headers automatically.
2. Tell the application where to send. The receiver address is configured when the application starts and does not go through send. A standard OTel SDK reads the OTEL_* environment variables only once, at process startup. Choose by how the application runs:
-
A service you start yourself that keeps running (most common): fix the receiver port in
niceeval.config.ts. Writing this configuration turns on OTel integration:SetOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/tracesonce when the service starts, and you never need to change it no matter how many evals you run. When forwarding through an OTel Collector, point it at this address too. Because the port is fixed, only one niceeval process can run on the same machine at a time. When another process holds the port, NiceEval reports an error directly and tells you to pick a free port. The receiver address given to the application defaults to127.0.0.1. Only when the application has to go through a tunnel or other routing to reach the machine running NiceEval, usehostto write that address, for exampletelemetry: { host: "otel.internal", port: 4318 }. A Docker Sandbox Agent starts the receiver inside the same Sandbox by default, so the container does not need to connect back to the host. OTLP receiving is configured only through these two fields and does not read environment variables. -
A child process started by NiceEval (CLI-style Agents): nothing to do. Put
ctx.telemetry.env(standardOTEL_*environment variables, ready to spread) into the process environment. Each run is a new process, so it reads this run’s address.
- AI SDK
- LangGraph / LangChain
- OpenLLMetry
- OpenInference
- Your own gen_ai instrumentation
The official OTel integration is recommended (The exporter uses the standard OTel Node SDK, with the endpoint pointing to NiceEval (injected env or fixed port). Runnable examples: application-side instrumentation in
@ai-sdk/otel, which produces standard GenAI semantics). The older experimental_telemetry (ai.*) can also be drawn:examples/zh/origin/ai-sdk-v7 (src/backend/otel.ts, official @ai-sdk/otel), and the full eval project after integration in examples/zh/tier1/ai-sdk-v7.Attribute spans to Turns
When several evals run in parallel, the same receiver gets spans from several conversations at once. NiceEval sorts them into their turns in two ways:- By
traceparent(recommended, accurate under concurrency): whensendmakes the request, spreadctx.telemetry.headersinto the request headers so each turn carries a newtraceparent. When the application’s instrumentation supports trace context propagation (all standard OTel HTTP server instrumentation does), this turn’s spans attach under the trace NiceEval provided and match exactly by traceId. - By time (fallback): when the application does not propagate trace context, attribution uses the start and end times of
send. This is only reliable when turns run one at a time, so NiceEval runs this Agent’s turns one at a time and notes it in the logs, instead of mixing spans from different conversations. Oncetraceparentis confirmed to work, concurrency resumes automatically.
SimpleSpanProcessor, or flush at the end of each turn. BatchSpanProcessor buffers for a while before sending, so spans can arrive late into the next turn; a waterfall that occasionally misses its tail is usually caused by this.
Keep your existing OTel backend and send to both
The application most likely already sends traces to its own observability backend, such as Langfuse, SigNoz, or a production collector. Connecting NiceEval does not require switching backends or a second set of instrumentation. A TracerProvider can have several SpanProcessors, sending the same spans to two places:Control waterfall content with semantic mapping
Whether a span shows in the waterfall as a model call, a tool execution, or a sub-Agent depends on its attributes. Before drawing, NiceEval normalizes each span to GenAI semantics, mainly looking atgen_ai.operation.name (the standard operation name) and the normalized kind:
When the application instruments directly according to the GenAI semantic conventions (the “Your own gen_ai instrumentation” tab above), all of this holds automatically. Common spans from AI SDK, LangSmith, and OpenLLMetry / OpenInference are also recognized automatically.
Private instrumentation: write your own mapping
A waterfall full of grayother usually means the application uses its own span names that automatic recognition does not know. There are two ways to fix it:
- Change the instrumentation (recommended): add a
gen_ai.operation.nameattribute to spans in the application. It is a one-line change, and your own observability backend benefits too. - Write a
spanMapper: when changing the application is inconvenient, declare a pure function on the Agent that translates custom spans to the semantics in the table above before drawing.tagSpanwrites the category back to the span, only adding attributes and never changing existing ones;heuristicTagis the same logic automatic recognition uses. Both are exported fromniceeval/adapter:
mapCodexSpans, exported from niceeval/adapter, is a spanMapper implementation you can use as a reference. spanMapper only affects the waterfall. A wrong mapping only makes categories or colors inaccurate; it never affects assertions.
Check the result
After an eval runs, open the browser withpnpm exec niceeval view and go into an Attempt to see each turn’s waterfall. In the terminal, use pnpm exec niceeval show @<locator> to see the Attempt’s results and log notes.
- The waterfall is empty: not a single span arrived during the whole run, usually because the application did not reach the receiver address, for example the environment variable was not set, or the service was not restarted after a config change. NiceEval notes this in the logs, and assertions are judged as usual.
- The waterfall occasionally misses its tail: first check whether the application uses
BatchSpanProcessor; see “Attribute spans to Turns” above. - All spans show as
other: see “Private instrumentation: write your own mapping” above.
What OTel integration does not handle
- All assertion data comes from
send. Tool calls must be mapped intoevents(with a built-in converter or handwritten mapping; see Write send), and usage goes in the return value ofsend. Data that appears only in spans and not ineventsdoes not take part in assertions. - Multi-turn sessions and HITL still happen in
send. Spans do not express “waiting for human input”, and session continuation is decided by the application interface (for session continuation see Write send; for HITL see HITL).
Related reading
- Connect Your Agent:
sendevent mapping and the data source for assertions. - Write send: the full tutorial for a handwritten Adapter; step six is the Adapter-side configuration for this page.
- Events reference: the event structure assertions read.