Agent versions
When more than one build of the same agent sends traces to one Neens agent — a canary next to the current release, a rollout that is still draining, a staging build pointed at production data — you want to see each build separately. Neens reads an agent version off every trace and lets you group the Traces and Sessions charts, dashboard widgets and failure modes by it, so you can answer “is this failure only in v1.3?” or “how far has v2.4 rolled out?” without guessing.
At a glance
| What sets it | The neens.version_label attribute on a span, or on the OTLP resource; service.version on the resource as a fallback |
| Where you see it | Traces / Sessions chart → Group by: Agent version · the Agent Version filter and column · dashboard widgets (Agent version dimension) · each failure mode’s Agent versions card · Cost & Quality |
| API | ?group_by=version on GET /sessions/trend and GET /conversations/trend · ?version= on the list and trend endpoints · version dimension in dashboard widgets · version_distribution on GET /clusters/{id} |
| Traces without a version | Kept as their own Unversioned group, never merged into a real version. Select them with version=__unversioned__ |
Tag your traces with a version
Use any string that names a build: a semantic version (v2.4.0), a release name, or a short commit
SHA. Neens takes the version for a trace from the first of these that is present:
- a span attribute
neens.version_label(the first span in the trace that carries one), - the OTLP resource attribute
neens.version_label, - the OTLP resource attribute
service.version(the standard OpenTelemetry one, so an agent that already sets it is versioned with no change).
An empty value counts as no version. The resource fallbacks apply to OTLP (POST /v1/traces);
on POST /ingest/openinference and POST /ingest/raw, put neens.version_label on a span.
Set the version once on the tracer provider’s resource. Every trace the process exports then carries it:
import os
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
resource = Resource.create({
"service.name": "support-agent",
# The build that is running — e.g. injected by your deploy pipeline.
"neens.version_label": os.environ.get("AGENT_VERSION", "v2.4.0"),
})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
endpoint="https://<your-neens-host>/v1/traces",
headers={"Authorization": "Bearer nk_live_your_key_here"},
)))
trace.set_tracer_provider(provider)To set it per request instead (for example when one process serves two prompt variants), put it on the request’s top-level span:
with tracer.start_as_current_span("agent.handle_request") as span:
span.set_attribute("neens.version_label", variant.version)
...The version is read when a trace arrives. Traces that were sent before you added the attribute stay Unversioned.
Break the Traces chart down by version
Open Traces (or Sessions)
The chart above the list shows the volume for the selected time range.
Choose Group by → Agent version
The chart switches to stacked bands, one per version. Each band is a count of traces (or sessions) over time, and the bands add up to the same total as the ungrouped chart. Traces with no version form an Unversioned band. When there are more than seven versions, the busiest seven are shown and the rest are combined into Other.
Narrow the list to one version
Use Filters → Agent Version to show one version’s traces in the table, or Unversioned to find traces that are missing the attribute.
A rollout reads directly off the chart: the new version’s band grows while the old one shrinks, and anything still on the old build after the cutover is easy to spot.
On Sessions, a session made of several traces is counted once. If its traces carry different versions, it is counted under the lowest version label; it is Unversioned only when none of its traces carries a version.
Through the API:
curl -H "Authorization: Bearer nk_live_your_key_here" \
"https://<your-neens-host>/api/sessions/trend?range=30d&group_by=version"{
"group_by": "version",
"series": [
{ "key": "v2.4.0", "other": false, "total": 752, "counts": { "2026-09-28": 33, "...": 0 } },
{ "key": "v2.3.0", "other": false, "total": 62, "counts": { "2026-09-28": 3 } },
{ "key": null, "other": false, "total": 47, "counts": { "2026-09-28": 2 } }
]
}A null key is the unversioned group. Add version=v2.3.0 to scope the chart and its tiles to one
version, or version=__unversioned__ for the traces without one.
The chart counts every trace in the list, including the traces a pre-prod evaluation replays, which carry the candidate’s version. Cost & Quality and the failure-mode breakdown below leave those replays out because they only look at production traffic.
Add a version breakdown to a dashboard
In a dashboard, click Add widget, open the Custom tab, pick a trace-grain measure such as Traces, Error rate, or p50 latency, and set Dimension to Agent version. A bar, donut, or table works best. Traces without a version show as Unversioned.
curl -X POST -H "Authorization: Bearer nk_live_your_key_here" \
-H "Content-Type: application/json" \
"https://<your-neens-host>/api/dashboards/<dashboard-id>/widgets" \
-d '{"type": "bar", "measure": "error_rate", "dimensions": ["version"], "range": "7d"}'version also works as a widget filter (for example "filters": {"version": ["v2.4.0"]}) and
on the per-case business measures, where a case takes the version of its first trace. It does not
apply to the cost, token, span, or score measures. The builder doesn’t offer those combinations,
and the API rejects them with 422. See the full matrix in the metrics catalogue.
See which versions a failure mode hits
Open a failure mode from Failure Modes. The Agent versions card on its Overview tab lists, for each version:
| Column | Meaning |
|---|---|
| Traces in mode | How many of the failure mode’s traces ran on this version. |
| Share of mode | That count as a share of the failure mode. |
| Version traces | All of the agent’s traces on this version between the failure mode’s first and last trace. |
| Hit rate | Traces in mode ÷ version traces — how often this version’s traffic ends up in this failure mode. |
Compare hit rates, not shares. A version that serves most of the traffic also owns most of every failure. For example, if v2.3.0 hits a failure mode at 26% and v2.4.0, serving four times the traffic over the same weeks, hits it at 0%, the failure is specific to v2.3.0 (here, v2.4.0 fixed it). A version with traffic in that period but no traces in the failure mode is listed with a 0% hit rate, not left out. Pre-prod replays are not counted.
The same breakdown is version_distribution on GET /clusters/{id}, and the get_failure_mode
MCP tool returns it as versionDistribution. list_traces accepts a
version filter.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Everything is Unversioned | The agent doesn’t set a version, or sets it under a different name | Set neens.version_label (or service.version on the OTLP resource), spelled exactly as shown |
A version set with OTEL_RESOURCE_ATTRIBUTES doesn’t show up on OpenInference or raw traces | The resource fallback only applies to OTLP | Set neens.version_label on a span, or send over POST /v1/traces |
| Old traces have no version | The version is read when a trace arrives | Only traces sent after the change are versioned |
| Very many bands, mostly in Other | The label changes per commit | Use a release-level label (v2.4.0) instead of a per-commit SHA |
Related
- Traces & sessions — the list, filters and the trend chart.
- Failure modes & clustering — how failure modes are formed.
- Cost & Quality — cost and quality per agent version.
- Pre-prod evaluations — test a candidate version before it ships.