Skip to content

fix(cli): Treat Events search syntax 400s as --query mistakes - #1936

Merged
BYK merged 3 commits into
mainfrom
fix/cli-events-search-parse-errors
Nov 4, 2026
Merged

BYK merged 3 commits into
mainfrom
fix/cli-events-search-parse-errors

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

toSearchQueryError only recognized the issue-search wrapper Error parsing search query. The Events endpoint (explore, span/log/trace list) returns event_search.py parser failures unwrapped, so a typo in a user's --query came back as a raw 400 Bad Request and was captured as a CLI bug.

That's the largest slice of CLI-10: ~38% of the last 300 events, almost all sentry explore run by coding agents.

The CLI now detects these by the "code": "invalid_search_query" the server returns next to detail (getsentry/sentry#129957, merged), not by matching message text. ApiError keeps the body's code, and its optional enriched403 flag 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 reported ApiError. Issue search keeps its Error parsing search query match.

Before

$ sentry explore my-org/ --dataset spans -q 'span.description:*"POST'
Error: API request failed: 400 Bad Request
  Endpoint: /organizations/my-org/events/
  Invalid quote at '*"POST': quotes must enclose text or be escaped.
# exit 30, captured in CLI-10
$ sentry explore my-org/ --dataset logs -q 'timestamp:>-15m'
Error: API request failed: 400 Bad Request
  Endpoint: /organizations/my-org/events/
  timestamp: Invalid date: >-15m. Expected +/-duration (e.g. +1h) or ISO 8601-like (e.g. 2026-11-04T09:40:33.73).
# exit 30, captured in CLI-10

After

With the server sending "code": "invalid_search_query":

$ sentry explore my-org/ --dataset spans -q 'span.description:*"POST'
Error: Invalid quote at '*"POST': quotes must enclose text or be escaped.

Suggestions:
  • Check your --query syntax (Sentry search reference: https://docs.sentry.io/concepts/search/)
# exit 21 (validation), grouped as ValidationError:query
$ sentry explore my-org/ --dataset logs -q 'timestamp:>-15m'
Error: timestamp: Invalid date: >-15m. Expected +/-duration (e.g. +1h) or ISO 8601-like (e.g. 2026-11-04T09:40:33.73).

Suggestions:
  • Check your --query syntax (Sentry search reference: https://docs.sentry.io/concepts/search/)
# exit 21

via @BYK.

--

View Session [Sentry]

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>
@vercel

vercel Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sentry-local Ready Ready Preview Nov 4, 2026 9:33pm UTC

Request Review

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Nov 4, 2026

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Isn't this the same as #1935 (review) ???

@sentry-junior

sentry-junior Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

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 sentry/api/event_search.py and reach the CLI as a flat {"detail": str} 400, so today the message text is the only signal the CLI has. Miguel is deciding whether to add a deterministic marker in getsentry/sentry that both PRs would read. I'll update this PR once that's settled.

@sentry-junior

sentry-junior Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my comment above: I couldn't close #1935 (my credentials don't allow closing PRs), so it's still open and needs to be closed by hand. #1936 is the one to keep.

@sentry-junior

sentry-junior Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

I opened the backend side for this one: getsentry/sentry#129957. When query fails to parse or validate in event_search, the 400 now includes "code": "invalid_search_query" alongside the unchanged detail. The type comes from an exception subclass, so it doesn't depend on the message text.

Once that merges and deploys, I'll switch this PR to read code instead of matching the message markers. The field-argument companion is getsentry/sentry#129954 (for #1927).

BYK added a commit to getsentry/sentry that referenced this pull request Nov 4, 2026
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
@sentry-junior

sentry-junior Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Updated per review: the regex list is gone. The CLI now detects Events search-syntax 400s by "code": "invalid_search_query", which getsentry/sentry#129957 (merged) adds next to detail. ApiError keeps the body's code. It still only converts the error when the user passed --query. A 400 without the code stays an ApiError, and there's a test for that. Issue search keeps its Error parsing search query match. User-facing output is the same once the server sends code.

@BYK
BYK merged commit 8cc059f into main Nov 4, 2026
28 checks passed
@BYK
BYK deleted the fix/cli-events-search-parse-errors branch November 4, 2026 21:55
BYK pushed a commit that referenced this pull request Nov 4, 2026
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>

This branch was successfully deployed

1 active deployment
Preview — 39641ecc Deployed Nov 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant