Skip to content

fix(intake): stop publishing span filters that answer HTTP 500 - #1225

Merged
aleckhoury merged 3 commits into
mainfrom
intake-span-filter-500/akhoury
Aug 12, 2026
Merged

fix(intake): stop publishing span filters that answer HTTP 500#1225
aleckhoury merged 3 commits into
mainfrom
intake-span-filter-500/akhoury

Conversation

@aleckhoury

@aleckhoury aleckhoury commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Five span filters returned HTTP 500 with the body An unexpected error occurred.:

dataset_id  dataset_name  dataset_version  prompt_name  prompt_version

ATTRIBUTE_EQ_FILTER_FIELDS in spans/api/spans.py routed all five to the span attribute catalog. SpanAttributeField has no entry for any of them, so spec_for_field raised ValueError while the SQL was built, and nothing caught it. A bad request read as a server fault.

Reproduced against a running platform before the fix:

dataset_id: InternalServerError status=500 - {'detail': 'An unexpected error occurred.'}
dataset_name: InternalServerError status=500 - {'detail': 'An unexpected error occurred.'}
dataset_version: InternalServerError status=500 - {'detail': 'An unexpected error occurred.'}
prompt_name: InternalServerError status=500 - {'detail': 'An unexpected error occurred.'}
prompt_version: InternalServerError status=500 - {'detail': 'An unexpected error occurred.'}

This was reachable from the public API docs: the endpoint description named prompt_name and prompt_version explicitly.

Why remove them instead of adding catalog entries

SpanSemanticAttributes has no dataset or prompt-name field, so Intake never ingests, stores, or returns this data. SpanEvaluationContext exposes only evaluation_id and test_case_id. A catalog entry would let the query run and return zero rows every time, and a silently empty result is harder to diagnose than a rejected field.

Changes

  • spans_schemas.py: drop the five fields from SpanFilter.
  • spans.py: ATTRIBUTE_EQ_FILTER_FIELDS stays an explicit list of the eight. SpanFilter is the public contract and the catalog is storage plumbing, so adding a name should be a deliberate act rather than a set operation between the two. The contract tests below supply the drift protection.
  • spans.py: the endpoint description now names only the fifteen served fields and says which operators they take.
  • openapi/: regenerated. Pure deletion of the five properties plus the reworded description.
  • SDK and CLI: regenerated with make update-sdk. SpanFilterParam, the spans resource, both checked-in CLIs, and docs/cli/reference.mdx no longer offer the five fields. SpanFilterParam now carries exactly the fifteen fields a live Intake serves.
  • plugins/nemo-insights: the Analyst told its model that dataset_name and evaluation_run_id were valid span filters. The first returned 500 and the second is not a field at all. It also advertised a status value of "ok", which does not exist: SpanStatus is success, error, cancelled, unknown, and a live Intake answers status=ok with 400 Filter field 'status' must be one of. All three corrected.

Tests

services/intake/tests/test_spans_filter_contract.py walks the chain that broke — filter JSON, then the API filter builder, then the SQL builder — for every field SpanFilter publishes. It holds both directions of the contract: no published field is unroutable, and no routed name has fallen out of the schema.

I confirmed the guard works by temporarily republishing dataset_name, which fails three tests with:

AssertionError: SpanFilter publishes ['dataset_name'], which _span_filter cannot route.
Either handle the field or remove it from the published schema.

Test plan

  • uv run --frozen pytest services/intake/tests --ignore=services/intake/tests/integration — 277 passed
  • uv run --frozen pytest services/intake/tests/integration/spans — 108 passed
  • uv run ruff check — clean
  • uv run --frozen ty check services/intake — 7 diagnostics, none in changed files
  • make update-sdk — five fields gone, diff scoped to intake spans with no unrelated generator drift
  • Live probe of the status values: success 200, error 200, ok 400
  • Pre-existing nemo-insights test failures confirmed identical on main
  • Re-run the 500 repro after deploy to confirm the five now return 400

Related

Found while adding Intake trace query tools to Eval Author in #1212, which had inherited the same wrong filter vocabulary in its docstrings.

Summary by CodeRabbit

  • Changes

    • Updated span and span-group filtering to use supported fields such as project, evaluation, test case, agent, model, tool, and provider.
    • Removed dataset and prompt filtering options from the API, CLI commands, and documentation.
    • Clarified that non-time filters use exact matching, while started_at supports gte and lte comparisons.
    • Updated status filter examples and supported values.
  • Tests

    • Added coverage ensuring published filters are supported, routable, and correctly translated into queries.

Filtering spans by dataset_id, dataset_name, dataset_version, prompt_name, or
prompt_version returned HTTP 500 with the body "An unexpected error occurred."

ATTRIBUTE_EQ_FILTER_FIELDS routed all five to the span attribute catalog, and
SpanAttributeField has no entry for any of them, so spec_for_field raised
ValueError while the SQL was built. Nothing caught it, so a bad request read as a
server fault.

Remove them rather than add catalog entries. SpanSemanticAttributes has no
dataset or prompt-name field, so Intake never ingests, stores, or returns this
data. A catalog entry would return zero rows for every query, and silently empty
results are harder to diagnose than a rejected field.

Derive ATTRIBUTE_EQ_FILTER_FIELDS from the catalog instead of listing it by hand.
The list and the catalog had no enforced relationship, which is what let them
drift. Derived, a SpanFilter field with no catalog entry falls through to the
existing 400 instead of raising.

The published description advertised prompt_name and prompt_version, so it was
sending callers at the broken fields. It now names only the fifteen served fields
and says which operators they take.

Add test_spans_filter_contract.py, which walks filter JSON to SQL for every
published field. Verified it fails when any of the five is republished.

Follow-up: the SDK and its generated CLI commands still expose the five. They
regenerate from this spec through Stainless, which needs credentials I do not
have here: run make update-sdk, then make generate-cli-commands.

Signed-off-by: Alec Khoury <akhoury@nvidia.com>
@aleckhoury
aleckhoury requested review from a team as code owners August 10, 2026 22:12
@github-actions github-actions Bot added the fix label Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d513ba7e-f6cd-46ce-aaa3-314a84f75b46

📥 Commits

Reviewing files that changed from the base of the PR and between d5e9e4e and 7d1e9e7.

⛔ Files ignored due to path filters (8)
  • sdk/python/nemo-platform/.nmpcontext/openapi.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/intake/spans/__init__.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/intake/spans/groups.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/intake/spans/spans.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/intake/span_filter_param.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/intake/span_list_params.py is excluded by !sdk/**
  • sdk/python/nemo-platform/tests/api_resources/intake/spans/test_groups.py is excluded by !sdk/**
  • sdk/python/nemo-platform/tests/api_resources/intake/test_spans.py is excluded by !sdk/**
📒 Files selected for processing (11)
  • docs/cli/reference.mdx
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/intake/spans/__init__.py
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/intake/spans/groups.py
  • plugins/nemo-insights/src/nemo_insights_plugin/analyst/agent.py
  • plugins/nemo-insights/src/nemo_insights_plugin/analyst/functions/spans.py
  • services/intake/src/nmp/intake/spans/api/spans.py
  • services/intake/src/nmp/intake/spans/api/spans_schemas.py
  • services/intake/tests/test_spans_filter_contract.py
💤 Files with no reviewable changes (2)
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/intake/spans/groups.py
  • services/intake/src/nmp/intake/spans/api/spans_schemas.py
🚧 Files skipped from review as they are similar to previous changes (9)
  • docs/cli/reference.mdx
  • plugins/nemo-insights/src/nemo_insights_plugin/analyst/functions/spans.py
  • plugins/nemo-insights/src/nemo_insights_plugin/analyst/agent.py
  • services/intake/src/nmp/intake/spans/api/spans.py
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/intake/spans/init.py
  • openapi/openapi.yaml
  • openapi/ga/individual/platform.openapi.yaml
  • services/intake/tests/test_spans_filter_contract.py
  • openapi/ga/openapi.yaml

📝 Walkthrough

Walkthrough

Updated the span filter contract across OpenAPI schemas, Intake routing, CLI commands, analyst documentation, and tests. Removed dataset and prompt filters. Added contract tests for routing, catalog coverage, SQL generation, and unpublished fields.

Changes

Span filter contract

Layer / File(s) Summary
Published SpanFilter schema
openapi/..., services/intake/src/nmp/intake/spans/api/spans_schemas.py
Removed dataset and prompt filter properties. Documented evaluation_id, test_case_id, exact-value filters, and started_at ranges.
Filter routing and integrations
services/intake/src/nmp/intake/spans/api/spans.py, packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/intake/spans/..., plugins/nemo_insights/src/nemo_insights_plugin/analyst/..., docs/cli/reference.mdx
Restricted attribute routing to supported fields. Removed retired filters from CLI requests and group queries. Updated analyst and CLI filter documentation.
Filter contract validation
services/intake/tests/test_spans_filter_contract.py
Added tests for JSON parsing, SQL generation, catalog coverage, routing completeness, and unpublished fields.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: removing span filters that caused HTTP 500 responses.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch intake-span-filter-500/akhoury

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openapi/ga/individual/platform.openapi.yaml (1)

17988-17990: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Regenerate the SDK and CLI filters for the removed SpanFilter fields.

dataset_id, dataset_name, dataset_version, prompt_name, and prompt_version are absent from all three OpenAPI artifacts but remain exposed by the Python SDK and CLI. Intake rejects these filters with HTTP 400. Run make update-sdk and confirm that the fields are absent from all generated clients and CLI copies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openapi/ga/individual/platform.openapi.yaml` around lines 17988 - 17990,
Regenerate the SDK and CLI using make update-sdk based on the OpenAPI
definitions in openapi/ga/individual/platform.openapi.yaml lines 17988-17990,
openapi/ga/openapi.yaml lines 17990-17991, and openapi/openapi.yaml line 17991;
ensure dataset_id, dataset_name, dataset_version, prompt_name, and
prompt_version are removed from all generated Python SDK clients and CLI filter
copies, while retaining the supported evaluation-id filter.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/intake/tests/test_spans_filter_contract.py`:
- Around line 26-30: Update the FILTER_VALUES fixture’s "status" entry to use
the valid SpanStatus value "ok" (or the corresponding enum member) instead of
"success", while leaving the other filter values unchanged.

---

Outside diff comments:
In `@openapi/ga/individual/platform.openapi.yaml`:
- Around line 17988-17990: Regenerate the SDK and CLI using make update-sdk
based on the OpenAPI definitions in openapi/ga/individual/platform.openapi.yaml
lines 17988-17990, openapi/ga/openapi.yaml lines 17990-17991, and
openapi/openapi.yaml line 17991; ensure dataset_id, dataset_name,
dataset_version, prompt_name, and prompt_version are removed from all generated
Python SDK clients and CLI filter copies, while retaining the supported
evaluation-id filter.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: cff08208-f8e4-455c-9fbc-6add581f08e1

📥 Commits

Reviewing files that changed from the base of the PR and between 33cecaf and 59b43c8.

📒 Files selected for processing (8)
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • plugins/nemo-insights/src/nemo_insights_plugin/analyst/agent.py
  • plugins/nemo-insights/src/nemo_insights_plugin/analyst/functions/spans.py
  • services/intake/src/nmp/intake/spans/api/spans.py
  • services/intake/src/nmp/intake/spans/api/spans_schemas.py
  • services/intake/tests/test_spans_filter_contract.py
💤 Files with no reviewable changes (1)
  • services/intake/src/nmp/intake/spans/api/spans_schemas.py

Comment thread services/intake/tests/test_spans_filter_contract.py
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 32001/40624 78.8% 63.5%
Integration Tests 18553/38550 48.1% 20.8%

Comment thread services/intake/src/nmp/intake/spans/api/spans.py Outdated
Comment thread openapi/openapi.yaml
Removing five fields from SpanFilter changed the published contract, but the
checked-in Python SDK and CLI still offered them, so the repo shipped two
different answers to the same question. Regenerating drops them from
SpanFilterParam, the spans resource, both CLIs, and the reference docs. The SDK
filter type now carries exactly the fifteen fields a live Intake serves.

The Analyst docstrings advertised status values of "ok" and "error". There is no
"ok": SpanStatus is success, error, cancelled, unknown, and a live Intake answers
status=ok with "400 Filter field 'status' must be one of". An agent following
those docs wrote a query that could not succeed.

ATTRIBUTE_EQ_FILTER_FIELDS goes back to an explicit list. Deriving it by
intersecting the schema with the storage catalog was hard to read and blurred a
public contract into storage plumbing. Adding a name is now deliberate, and the
contract tests supply the drift protection that deriving was standing in for,
including a new test that no routed name has fallen out of the schema.

Signed-off-by: Alec Khoury <akhoury@nvidia.com>
@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@aleckhoury
aleckhoury added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit ce328da Aug 12, 2026
64 checks passed
@aleckhoury
aleckhoury deleted the intake-span-filter-500/akhoury branch August 12, 2026 14:38
aleckhoury added a commit that referenced this pull request Aug 12, 2026
Per review on #1212: the workaround should not need to exist, and it no longer
does, so the notes describing it go too.

The docstring no longer warns the agent off the five dataset and prompt span
filters, and _explain no longer has a 500 branch naming them. #1225 unpublished
those fields, so they read as unknown filter fields like any other typo, and the
400 branch already says where the real vocabulary lives. A 500 now falls through
to the generic branch, which still names the status, since an Intake fault leaves
the caller nothing to correct.

The module docstring also still said span groups sort only by count, which #1242
changed and this branch already relies on.

Signed-off-by: Alec Khoury <akhoury@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants