diff --git a/apps/cli-docs/src/fragments/commands/issue.md b/apps/cli-docs/src/fragments/commands/issue.md index e832ea944..7a0ca0f91 100644 --- a/apps/cli-docs/src/fragments/commands/issue.md +++ b/apps/cli-docs/src/fragments/commands/issue.md @@ -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/) ::: diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md index ebd25f996..ac185be9b 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md @@ -22,7 +22,7 @@ List issues in a project | `[]` | <org>/ (all projects), <org>/<project>, or <project> (search) | **Flags:** -- `-q, --query - Search query (Sentry syntax, implicit AND, no OR operator)` +- `-q, --query - Search query (Sentry syntax, implicit AND; same-key OR becomes an in-list)` - `-n, --limit - Maximum number of issues to list - (default: "25")` - `-s, --sort - Sort by: recommended, date, new, freq, user (default: recommended on sentry.io, else date)` - `-t, --period - Time range: "7d", "2024-01-01..2024-02-01", ">=2024-01-01" - (default: "90d")` diff --git a/packages/cli/src/commands/feedback/list.ts b/packages/cli/src/commands/feedback/list.ts index cb6bf1985..5c53351d6 100644 --- a/packages/cli/src/commands/feedback/list.ts +++ b/packages/cli/src/commands/feedback/list.ts @@ -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, @@ -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, }, diff --git a/packages/cli/src/commands/issue/list.ts b/packages/cli/src/commands/issue/list.ts index 10e48b499..c14871071 100644 --- a/packages/cli/src/commands/issue/list.ts +++ b/packages/cli/src/commands/issue/list.ts @@ -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"; @@ -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: { ).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, + ); + }); +}); diff --git a/packages/cli/test/commands/issue/list.test.ts b/packages/cli/test/commands/issue/list.test.ts index 66ed0aaf8..cb65b522a 100644 --- a/packages/cli/test/commands/issue/list.test.ts +++ b/packages/cli/test/commands/issue/list.test.ts @@ -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. diff --git a/packages/cli/test/lib/search-query.test.ts b/packages/cli/test/lib/search-query.test.ts index 721778094..9de8b7040 100644 --- a/packages/cli/test/lib/search-query.test.ts +++ b/packages/cli/test/lib/search-query.test.ts @@ -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"; @@ -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) // --------------------------------------------------------------------------- diff --git a/packages/toolkit-core/src/search-query.ts b/packages/toolkit-core/src/search-query.ts index e392130a1..d05b55c3d 100644 --- a/packages/toolkit-core/src/search-query.ts +++ b/packages/toolkit-core/src/search-query.ts @@ -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*)", @@ -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",