Repository navigation
fix(cli): Treat Events search syntax 400s as --query mistakes - #1936
Conversation
The Events endpoint returns event_search.py parser failures unwrapped
("Invalid quote at ...", "Parse error at ...", '"is:" queries are not
supported ...', "timestamp: Invalid date: ..."). Only the issue-search
wrapper "Error parsing search query" was recognized, so a typo in a
user's explore/span/log/trace --query surfaced as a raw 400 and was
reported to Sentry as a CLI bug (CLI-10).
Recognize the grammar-level parser messages too, so toSearchQueryError
turns them into a query ValidationError when the user supplied --query.
Co-Authored-By: Miguel Betegon <miguel.betegon@sentry.io>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
BYK
left a comment
There was a problem hiding this comment.
Isn't this the same as #1935 (review) ???
|
Yes, sorry. #1935 was an accidental duplicate: GitHub returned a 502 on create, so this PR was opened from a copy of the branch without noticing #1935 already existed. I closed #1935. On your feedback there: agreed, it's the same question as on #1927. These parser errors come from |
|
I opened the backend side for this one: getsentry/sentry#129957. When Once that merges and deploys, I'll switch this PR to read |
When the `query` string fails in `event_search.parse_search_query`, the 400 now includes `"code": "invalid_search_query"` alongside the unchanged `detail`. That covers unmatched parens, invalid quotes, a bad date/number/boolean, an empty value, unsupported `is:`, escapes, and similar failures. `parse_search_query` re-raises the plain `InvalidSearchQuery` it gets from the visitor as a new `SearchQueryParseError` subclass, keeping `extra` intact. `handle_query_errors` then keys off that type, so nothing depends on message text. Errors that are already subclasses pass through untouched, and so do semantic errors raised later in resolution. Context: the Sentry CLI currently regex-matches these messages to tell a typo in `--query` apart from a CLI bug ([getsentry/toolkit#1936](getsentry/toolkit#1936), CLI-10). Once this lands it can read `code` instead. Companion to #129954, which adds `invalid_function_argument` for fields. ### Before ``` GET /api/0/organizations/org-slug/events/?dataset=spans&field=count()&query=span.op:db) 400 {"detail": "Parse error at 'p:db)' (column 11). This is commonly caused by unmatched parentheses. Enclose any text in double quotes."} GET /api/0/organizations/org-slug/events/?dataset=errors&field=count()&query=timestamp:>yesterday 400 {"detail": "timestamp: Invalid date: >yesterday. Expected +/-duration (e.g. +1h) or ISO 8601-like (e.g. 2026-11-04T20:39:58.14)."} ``` ### After ``` GET /api/0/organizations/org-slug/events/?dataset=spans&field=count()&query=span.op:db) 400 {"detail": "Parse error at 'p:db)' (column 11). This is commonly caused by unmatched parentheses. Enclose any text in double quotes.", "code": "invalid_search_query"} GET /api/0/organizations/org-slug/events/?dataset=errors&field=count()&query=timestamp:>yesterday 400 {"detail": "timestamp: Invalid date: >yesterday. Expected +/-duration (e.g. +1h) or ISO 8601-like (e.g. 2026-11-04T20:39:58.14).", "code": "invalid_search_query"} ``` <!-- junior-request-attribution:start --> via @BYK. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3A61f1063c1e971e700aaa2c3a3c12100a --> -- [View Session](https://junior-prod.sentry.dev/conversations/agent%3A61f1063c1e971e700aaa2c3a3c12100a) [[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A61f1063c1e971e700aaa2c3a3c12100a/?project=4510944073809921) <!-- junior-session-footer:end --> --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: BYK <126780+BYK@users.noreply.github.com>
Replace the list of parser message patterns with the "invalid_search_query" code the Events endpoint now returns next to "detail" (getsentry/sentry#129957). ApiError keeps the error body's code; its optional enriched403 flag moves into an options object to make room. Refs CLI-10
…-parse-errors # Conflicts: # packages/cli/src/lib/api/infrastructure.ts
|
Updated per review: the regex list is gone. The CLI now detects Events search-syntax 400s by |
Part of [CLI-10](https://sentry.sentry.io/issues/7238595083/) ("API request failed: 400"). When `sentry explore` sends a user `--field`/`--sort` aggregate that Sentry rejects because of its argument, the CLI shows a raw 400 and reports it as a CLI bug. These are input mistakes: a string attribute in a numeric aggregate, `transaction.duration` on `--dataset transactions` (which queries spans), a non-numeric errors column, or an invalid `count_if` argument. The CLI now reads the structured fields that getsentry/sentry#129954 (merged) adds to these 400s, `"code": "invalid_function_argument"` plus `function` and `argument`, instead of matching message text. `ApiError` keeps them next to `code`. When they name the same function and argument as a user expression that reached the request, `toExploreFieldError` (#1661) raises a `ValidationError` that keeps the server detail and adds a hint. Rejections of default or generated expressions, and 400s without the code (servers before that deploy), stay `ApiError`s. The #1661 hints are unchanged. Builds on #1936 for `ApiError.code`. Merge that first. ### Before ``` $ sentry explore my-org/ --dataset spans -F 'p75(cart.total)' Error: API request failed: 400 Bad Request Endpoint: /organizations/my-org/events/ cart.total is invalid for parameter 1 in p75. Its a string type field, but it must be one of these types: {'number'} # exit 30 ``` ``` $ sentry explore my-org/ --dataset transactions -F 'sum(transaction.duration)' Error: API request failed: 400 Bad Request Endpoint: /organizations/my-org/events/ transaction.duration is invalid for parameter 1 in sum. Its a string type field, but it must be one of these types: {'number', 'duration'} # exit 30 ``` ### After With the server sending `code`, `function` and `argument`: ``` $ sentry explore my-org/ --dataset spans -F 'p75(cart.total)' Error: Sentry rejected --field "p75(cart.total)" for dataset "spans". cart.total is invalid for parameter 1 in p75. Its a string type field, but it must be one of these types: {'number'} Try: p75() does not accept cart.total on dataset "spans": it may not exist there, or may not have the type p75() needs. Reference a numeric custom attribute with its type, e.g. tags[cart.total,number]; to check the key and its type, run: sentry attribute list <org>/ cart.total --dataset spans # exit 21 ``` ``` $ sentry explore my-org/ --dataset transactions -F 'sum(transaction.duration)' Error: Sentry rejected --field "sum(transaction.duration)" for dataset "spans". transaction.duration is invalid for parameter 1 in sum. Its a string type field, but it must be one of these types: {'number', 'duration'} Try: Use span.duration instead of transaction.duration. Transactions are queried as spans, which store their duration in span.duration. # exit 21 ``` ``` $ sentry explore my-org/ --dataset errors -F 'min(title)' Error: Sentry rejected --field "min(title)" for dataset "errors". min(title): column argument invalid: title is not a numeric column Try: min() does not accept title on dataset "errors": it may not exist there, or may not have the type min() needs. Check the column name and its type. # exit 21 ``` I captured these outputs against a local stub that returns each 400 body. <!-- junior-request-attribution:start --> via @BYK. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3A61f1063c1e971e700aaa2c3a3c12100a --> -- [View Session](https://junior-prod.sentry.dev/conversations/agent%3A61f1063c1e971e700aaa2c3a3c12100a) [[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A61f1063c1e971e700aaa2c3a3c12100a/?project=4510944073809921) <!-- junior-session-footer:end --> --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Miguel Betegon <miguel.betegon@sentry.io>
toSearchQueryErroronly recognized the issue-search wrapperError parsing search query. The Events endpoint (explore, span/log/trace list) returnsevent_search.pyparser failures unwrapped, so a typo in a user's--querycame back as a raw400 Bad Requestand was captured as a CLI bug.That's the largest slice of CLI-10: ~38% of the last 300 events, almost all
sentry explorerun by coding agents.The CLI now detects these by the
"code": "invalid_search_query"the server returns next todetail(getsentry/sentry#129957, merged), not by matching message text.ApiErrorkeeps the body'scode, and its optionalenriched403flag moves into an options object to make room. The existing guard stays: the error is only converted when the user actually passed--query. A 400 without the code (servers before that deploy, or a query the CLI built) stays a reportedApiError. Issue search keeps itsError parsing search querymatch.Before
After
With the server sending
"code": "invalid_search_query":via @BYK.
--
View Session [Sentry]