feat(rest-api): add type-aware attribute filter to GET /v1/spans#12524
feat(rest-api): add type-aware attribute filter to GET /v1/spans#12524anticorrelator wants to merge 15 commits intomainfrom
Conversation
… endpoints Add attribute-based filtering to both GET spans endpoints so users can query spans by arbitrary key-value attributes (e.g., llm.model_name, session.id) using exact string comparison via repeatable `attribute_filter=key:value` query parameters with AND semantics. - Server: `_parse_attribute_filter` helper + Query param on both `span_search` and `span_search_otlpv1` handlers - Client SDK: `attribute_filters` param on sync/async `get_spans()` with version gating (>= 13.24.0) - CLI: `--attribute-filter` option on `px span list` - OpenAPI schema and generated types regenerated - Unit, integration, and client serialization tests added Closes #12522
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
@arizeai/phoenix-cli
@arizeai/phoenix-client
@arizeai/phoenix-evals
@arizeai/phoenix-mcp
@arizeai/phoenix-otel
commit: |
- Fix CLI help text to show key:value format instead of operator syntax - Rename Python client param from attribute_filters to attribute_filter (singular) to match existing naming convention - Add attributeFilter support to TS SDK with capability check and array normalization - Add test coverage: OTLP error paths, colon-in-value, async client parity - Document colon-in-value behavior in OpenAPI description
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
|
Let's work through non KV based approach: use cases: metadata, threads (sessions), include, exclude |
| status_code=422, | ||
| detail="Invalid attribute_filter: key must not be empty", | ||
| ) | ||
| key_parts = key.split(".") |
There was a problem hiding this comment.
we should think though how complex the first part should be. Can you come up with some reasonably complicated things you might want to filter for here
| if ":" not in filter_str: | ||
| raise HTTPException( | ||
| status_code=422, | ||
| detail=f"Invalid attribute_filter '{filter_str}': expected format 'key:value'", |
There was a problem hiding this comment.
we need to think about URI encoding a bit here I think
mikeldking
left a comment
There was a problem hiding this comment.
Let's re-name the param
Also we need to think about the Test case first - e.g. the types of filters that need to work. This is things like sessionID, metadata
We also need to think about UI encoding, numbers, boolean values, lists of primitives and the like.
…e comparison Rename the `attribute_filter` query parameter to `attribute` on GET /v1/spans endpoints per reviewer feedback. Add type-aware equality comparison via json.loads() dispatch — integers use .as_integer(), floats use .as_float(), booleans use .as_boolean(), and strings use .as_string() (with fallback for bare values). This fixes a correctness bug where non-string attribute values silently failed to match. - Server: rename _parse_attribute_filter → _parse_attribute, add type dispatch - OpenAPI schema: rename param, update description with type-aware examples - Python client: widen type to dict[str, Union[str, int, float, bool]], add smart-quoting serialization, bump server requirement to GET_SPANS_ATTRIBUTE_V2 - TS client + CLI: rename attributeFilter → attribute, update version constant - Tests: rename all param references, add 9 type-aware comparison tests, add 8 smart-quoting serialization tests
…ing quoting tests Fix GET_SPANS_ATTRIBUTE_FILTER minServerVersion from 13.24.0 to 13.25.0 to match the Python client. Remove vestigial GET_SPANS_ATTRIBUTE_FILTERS constant. Add sync and async tests for string "true" vs bool True serialization distinction.
- Fix ruff import sorting in integration test_spans.py - Remove unnecessary isinstance(v, str) check in _serialize_attribute_value (pyright correctly narrows type after bool/int/float checks) - Regenerate schemas/openapi.json with updated attribute description
…ltering The previous approach using type-specific accessors (as_integer, as_float, etc.) lost JSON type information on PostgreSQL because JSONB text extraction (#>>) strips type info before casting, causing "42" (string) and 42 (int) to match interchangeably. Switch to direct JSON-level comparison via sa.type_coerce which preserves type semantics on both backends. Also regenerate stale TypeScript types from updated OpenAPI schema.
…ering The previous type_coerce approach generated invalid SQL on PostgreSQL. Switch to CAST(col AS TEXT) == json.dumps(parsed), which: - PostgreSQL: CAST(JSONB AS TEXT) preserves JSON format (42 vs "42") - SQLite: JSON_QUOTE(JSON_EXTRACT(...)) also preserves JSON format - Booleans use as_boolean() since SQLite represents JSON bools as 0/1
…ption Adds Phase 1 unit tests pinning the current `attribute` query param contract across the OpenInference context-attribute shapes (user.id, session.id, metadata.*, tag.tags, ISO timestamps): type-aware dispatch, forced-string quoting, colon-in-value, nested-object paths, and the list-valued-attribute silent-zero-rows footgun. Adds an integration test pinning the OpenInference SDK-emitted `metadata` JSON-string round-trip through `load_json_strings` ingestion and the dotted-path attribute filter. Enriches the `attribute` OpenAPI description with the rules agents need to pick the right encoding: JSON-parsed values, `key:"value"` escape hatch for numeric-/boolean-looking strings, first-colon-only split, AND across repeated params, and the list-valued-attribute caveat. Extracted to a shared constant so `/spans` and `/spans/otlpv1` stay in sync. Adds scripts/agent_api_testing/span_filtering/ — an automated harness that measures whether independent Claude and Codex sessions can build correct filter URLs from schema alone (Phase 2 agent-usability trial).
…sumption Strips internal plan/task references, removes the URL-extraction / summary-aggregation layer in `run_trial.py` (raw JSONL transcripts are the ground truth anyway), deletes the redundant `manifest.json`, and condenses the `prompts.md` rubrics. Net reduction: ~1024 → ~496 lines.
| parameterName: "attribute", | ||
| parameterLocation: "query", | ||
| route: "GET /v1/projects/{id}/spans", | ||
| minServerVersion: [13, 25, 0], |
There was a problem hiding this comment.
we're in the 14 versions now. So this is going to be wrong.
| parameter_name="attribute", | ||
| parameter_location="query", | ||
| route="GET /v1/projects/{id}/spans", | ||
| min_server_version=Version(13, 25, 0), |
There was a problem hiding this comment.
same, version is wrong
| repeated ``attribute=key:value`` query params. Type-aware: int/float use | ||
| str(v), bool uses json.dumps(v), str values that parse as non-string JSON | ||
| are quoted to force string comparison. | ||
| Requires Phoenix server >= 13.25.0. |
There was a problem hiding this comment.
string for version is incorrect
| "attributes": { | ||
| "metadata.tier": "premium", | ||
| "metadata.count": 5, | ||
| "metadata.ratio": 0.7, | ||
| "metadata.flag": True, | ||
| "openinference.span.kind": "CHAIN", |
There was a problem hiding this comment.
can we testa a flat dump of metadata as this is the common pattern from our instrumentors
Upstream SDK rename in 64e3c53 changed the `get_spans` keyword from `attribute` (singular) to `attributes` (plural). Update the integration test call site to match so Type Check and Integration Tests CI pass.
Resolves #12522
Adds a repeatable
attributequery parameter toGET /v1/projects/{id}/spansandGET /v1/projects/{id}/spans/otlpv1for filtering spans by stored attribute values, with matching Python, TypeScript, and CLI support.The REST wire format remains singular
attribute=key:value. The Python and TypeScript clients expose anattributesmap and serialize it into repeatedattributequery params.Usage
REST
Python client (
arize-phoenix-client >= 13.25.0)TypeScript client (
@arizeai/phoenix-client)The Python and TypeScript clients preserve type intent. For example,
{ "user.id": "12345" }(string) and{ "user.id": 12345 }(int) emit distinct filters on the wire.CLI
Filter semantics
user.id,metadata.tier,llm.token_count.prompt). Nested paths traverse JSON objects at any depth. OpenInference attributes that SDKs emit as JSON blobs (metadata,tool.parameters, etc.) are normalized at ingestion, sometadata.tier:premiummatches regardless of the SDK's wire shape.key:12345matches integer12345,key:truematches boolean, otherwise string. To match a numeric- or boolean-looking string, JSON-quote it:user.id:"12345"(URL-encoded%2212345%22).key:"". The Python and TypeScript clients handle this quoting automatically.:only.session.id:sess:abc:123and ISO timestamps work without pre-escaping.tag.tags) cannot be matched by this filter; use the query DSL for list membership.Test plan
422s across SQLite and PostgreSQL.metadataJSON-string ingestion path.attributesmap API.attributesmap API.