feat(trace): add advanced span filters - #728
Conversation
Add nested `filters` to `Trace.get_spans()` so callers can select spans by type, name,
error state, tags, metadata, and duration with matching local and BTQL semantics.
```python
spans = await trace.get_spans(
filters={
"span_type": ["tool"],
"name": ["search", "lookup"],
"has_error": True,
"tags": {"all": ["production"], "none": ["internal"]},
"metadata": {"model": "gpt-5"},
"duration": {"min": 0.5, "max": 10},
}
)
```
Preserve fields needed to evaluate these filters in the local span cache. Canonical cache
keys and shared tasks avoid duplicate requests while keeping failures retryable:
```text
filters -> normalize -> bt_dumps key -> shared asyncio task
|-> success, including []: retain
`-> failure or cancellation: evict and retry
```
Keep `span_type=[...]` compatible but emit a `DeprecationWarning` directing callers to
`filters={"span_type": [...]}`. Add runtime and type coverage for validation, local and
server filtering, cache coalescing, empty results, and retries.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d95d83cc5c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "Codex (@codex) review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".
| try: | ||
| return list(await task) |
There was a problem hiding this comment.
Shield shared filter fetches from waiter cancellation
When concurrent callers request the same advanced filter, they await the same task directly. If one caller is cancelled—for example by a scorer timeout—asyncio propagates that cancellation to the shared fetch task, so every other caller awaiting the same filter also receives CancelledError even though only one request was cancelled. Await the cached task through asyncio.shield() so an individual waiter cannot terminate the shared operation.
Useful? React with 👍 / 👎.
| filters.append( | ||
| { | ||
| "op": "eq", | ||
| "left": {"op": "ident", "name": ["metadata", *child_path]}, | ||
| "right": {"op": "literal", "value": value}, |
There was a problem hiding this comment.
Handle null metadata leaves with a null predicate
When a metadata filter contains a null leaf, such as filters={"metadata": {"model": None}}, the remote path emits an eq comparison against a null literal. BTQL exposes and already uses isnull for null checks elsewhere in this filter builder; an equality comparison against null does not select null-valued paths, while the local _metadata_matches path does match an explicitly stored None. Consequently the same filter returns different results depending on whether the local cache is populated. Generate a null predicate for these leaves, or reject null metadata filters if they cannot be represented consistently.
Useful? React with 👍 / 👎.
resolves https://linear.app/braintrustdata/issue/SDK-317/add-filters-on-traceget-spans-to-python-sdk
Add
filtersoption totrace.get_spans()so callers can filter spans. Currently we support filtering by span_type, name, has_error, tags, metadata, and duration.span_typepreviously was also a top level option ontrace.get_spans, I deprecated the top level option.Also did some caching refactors to make sure nothing broke. I tested some of it manually, but a lot of it was trusting the llm to take the wheel.