Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/cli-docs/src/fragments/commands/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ does not save defaults.
Sentry search uses **implicit AND** — space-separated terms are all required.
**AND/OR operators are not supported** for issue search. Use alternatives:
- `key:[val1,val2]` — in-list syntax (matches val1 OR val2 for one key)
- `status:[resolved,ignored]` or `substatus:[regressed,escalating]` — `is:` has no in-list form
- Run separate queries for different terms
- `*term*` — wildcard matching

The CLI rewrites same-key ORs to in-lists automatically, including `is:resolved OR is:ignored` → `status:[resolved,ignored]`.

Full syntax reference: [Sentry Search Docs](https://docs.sentry.io/concepts/search/)
:::

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ List issues in a project
| `[<org/project>]` | &lt;org&gt;/ (all projects), &lt;org&gt;/&lt;project&gt;, or &lt;project&gt; (search) |

**Flags:**
- `-q, --query <value> - Search query (Sentry syntax, implicit AND, no OR operator)`
- `-q, --query <value> - Search query (Sentry syntax, implicit AND; same-key OR becomes an in-list)`
- `-n, --limit <value> - Maximum number of issues to list - (default: "25")`
- `-s, --sort <value> - Sort by: recommended, date, new, freq, user (default: recommended on sentry.io, else date)`
- `-t, --period <value> - Time range: "7d", "2024-01-01..2024-02-01", ">=2024-01-01" - (default: "90d")`
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/feedback/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
type ResolvedOrgOptionalTarget,
toNumericId,
} from "../../lib/resolve-target.js";
import { sanitizeQuery } from "../../lib/search-query.js";
import { sanitizeIssueQuery } from "../../lib/search-query.js";
import {
type OrgOptionalTarget,
orgOptionalTarget,
Expand Down Expand Up @@ -212,7 +212,7 @@ export const listCommand = buildListCommand("feedback", {
},
query: {
kind: "parsed",
parse: sanitizeQuery,
parse: sanitizeIssueQuery,
brief: "Search query (Sentry issue search syntax)",
optional: true,
},
Expand Down
10 changes: 7 additions & 3 deletions packages/cli/src/commands/issue/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ import {
} from "../../lib/resolve-target.js";
import {
SEARCH_SYNTAX_REFERENCE,
sanitizeQuery,
sanitizeIssueQuery,
unscopedCountFilterKeys,
} from "../../lib/search-query.js";
import { isSaaS } from "../../lib/sentry-web-urls.js";
Expand Down Expand Up @@ -1648,7 +1648,10 @@ export const listCommand = buildListCommand("issue", {
" Terms are space-separated and implicitly ANDed together.\n" +
" AND/OR operators are NOT supported. Use alternatives:\n" +
" key:[val1,val2] # in-list: matches val1 OR val2 for one key\n" +
" status:[resolved,ignored] # is: has no in-list; use status/substatus\n" +
" *term* # wildcard matching\n" +
" Same-key ORs are rewritten to in-lists automatically, including\n" +
" is:resolved OR is:ignored → status:[resolved,ignored].\n" +
" Filters: key:value, !key:value (negation), key:>N, key:<N\n" +
' Quoted: message:"exact phrase with spaces"\n' +
" Built-in: is:unresolved, is:resolved, assigned:me, has:user\n" +
Expand Down Expand Up @@ -1711,8 +1714,9 @@ export const listCommand = buildListCommand("issue", {
flags: {
query: {
kind: "parsed",
parse: sanitizeQuery,
brief: "Search query (Sentry syntax, implicit AND, no OR operator)",
parse: sanitizeIssueQuery,
brief:
"Search query (Sentry syntax, implicit AND; same-key OR becomes an in-list)",
optional: true,
},
limit: buildListLimitFlag("issues"),
Expand Down
19 changes: 18 additions & 1 deletion packages/cli/src/lib/search-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* The rewrite rules live in `@sentry/toolkit-core/search-query`, shared with
* the MCP server. This wrapper adds the CLI's reporting: one stderr warning
* per rewritten query, and {@link ValidationError} when a query can't be
* rewritten.
* rewritten. Commands that query the issues endpoint use
* {@link sanitizeIssueQuery}.
*/

import {
Expand Down Expand Up @@ -72,3 +73,19 @@ export function sanitizeQuery(
}
return result.query;
}

/**
* Sanitize a `--query` value sent to the issues endpoint
* (`/organizations/{org}/issues/`).
*
* Like {@link sanitizeQuery}, plus the issue-search rewrites: `is:A OR is:B`
* becomes a `status:[…]` or `substatus:[…]` in-list, and parentheses that
* reduce to plain terms are unwrapped.
*
* @param query - Raw query string from `--query` flag
* @returns The sanitized query
* @throws {ValidationError} When OR cannot be rewritten to in-list syntax
*/
export function sanitizeIssueQuery(query: string): string {
return sanitizeQuery(query, { issueSearch: true });
}
18 changes: 18 additions & 0 deletions packages/cli/test/commands/feedback/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,21 @@ describe("feedback list", () => {
).rejects.toBeInstanceOf(ValidationError);
});
});

describe("feedback list: query flag parse", () => {
const parseQuery = (
listCommand.parameters.flags!.query as { parse: (v: string) => string }
).parse;

test("rewrites is: OR for the issues endpoint", () => {
expect(parseQuery("is:regressed OR is:escalating")).toBe(
"substatus:[regressed,escalating]",
);
});

test("rejects is: OR across parent statuses", () => {
expect(() => parseQuery("is:new OR is:archived_forever")).toThrow(
ValidationError,
);
});
});
12 changes: 12 additions & 0 deletions packages/cli/test/commands/issue/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,18 @@ describe("issue list: cursor flag parse validation", () => {
});
});

describe("issue list: query flag parse", () => {
const parseQuery = (
listCommand.parameters.flags!.query as { parse: (v: string) => string }
).parse;

test("rewrites is: OR into a status in-list for the issues endpoint", () => {
expect(parseQuery("is:unresolved OR is:resolved")).toBe(
"status:[unresolved,resolved]",
);
});
});

describe("issue list: Phase 2 budget redistribution", () => {
// Phase 2 triggers when: totalFetched < limit AND some targets hit their
// quota but have more (nextCursor). The surplus budget redistributes.
Expand Down
28 changes: 28 additions & 0 deletions packages/cli/test/lib/search-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { describe, expect, test } from "vitest";
import { ValidationError } from "../../src/lib/errors.js";
import {
__testing,
sanitizeIssueQuery,
sanitizeQuery,
unscopedCountFilterKeys,
} from "../../src/lib/search-query.js";
Expand Down Expand Up @@ -258,6 +259,33 @@ describe("sanitizeQuery: OR → in-list (success)", () => {
});
});

// ---------------------------------------------------------------------------
// Issue search
// ---------------------------------------------------------------------------

describe("sanitizeIssueQuery", () => {
test("rewrites is: OR into status/substatus in-lists", () => {
expect(sanitizeIssueQuery("is:resolved OR is:ignored")).toBe(
"status:[resolved,ignored]",
);
expect(sanitizeIssueQuery("is:new OR is:ongoing level:error")).toBe(
"substatus:[new,ongoing] level:error",
);
});

test("keeps the generic rewrites", () => {
expect(sanitizeIssueQuery("level:error OR level:warning")).toBe(
"level:[error,warning]",
);
});

test("throws ValidationError for is: OR it cannot rewrite", () => {
expect(() => sanitizeIssueQuery("is:unresolved OR is:assigned")).toThrow(
ValidationError,
);
});
});

// ---------------------------------------------------------------------------
// OR → throws (cannot rewrite)
// ---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions packages/toolkit-core/src/search-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,7 @@ export const SEARCH_SYNTAX_REFERENCE = {
behavior: "Terms are space-separated and implicitly ANDed.",
operators: {
and: "NOT supported — implicit (space-separated terms are all required)",
or: "NOT supported — use key:[val1,val2] in-list syntax instead",
or: "NOT supported — use key:[val1,val2] in-list syntax instead; for is: values use status:[val1,val2] or substatus:[val1,val2]",
not: "!key:value (prefix with !)",
comparison: [">=", "<=", ">", "<", "=", "!="],
wildcard: "* in values (e.g., message:*timeout*)",
Expand All @@ -1009,7 +1009,7 @@ export const SEARCH_SYNTAX_REFERENCE = {
"relative_date (key:-24h, key:+7d)",
"duration (key:>1s, key:<500ms)",
"has (has:key — not null check)",
"is (is:unresolved, is:resolved, is:ignored)",
"is (is:unresolved, is:resolved, is:ignored — no in-list form)",
],
commonFilters: [
"is:unresolved",
Expand Down
Loading