From 9af240d43d25897824dc63a297f5464cef2a7b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Tue, 22 Sep 2026 16:09:33 +0200 Subject: [PATCH] fix(search): rewrite numeric project: filters to project_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue search treats project as a slug, so agents that paste project: hit a 400 (CLI-FA). Same auto-repair path as OR→in-list: rewrite digits to project_id, leave slugs and namespaced keys alone. Co-authored-by: Cursor --- packages/cli/src/lib/search-query.ts | 53 +++++++++++++++++--- packages/cli/test/lib/search-query.test.ts | 57 ++++++++++++++++++++++ 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/lib/search-query.ts b/packages/cli/src/lib/search-query.ts index 88196386de..9ef6ea85c8 100644 --- a/packages/cli/src/lib/search-query.ts +++ b/packages/cli/src/lib/search-query.ts @@ -10,6 +10,10 @@ * - **OR**: Attempted rewrite to in-list syntax (`key:[val1,val2]`) * when all OR operands share the same qualifier key. Throws a * {@link ValidationError} when the rewrite is not possible. + * - **`project:`**: `project` is the slug. Numeric ids belong on + * `project_id`. Agents often paste `project:4511…` (CLI-FA). Rewritten + * with a warning. Slugs, `project_id:…`, and namespaced keys + * (`bolt.project_id`) are left alone. * * Parsing uses a pre-compiled PEG parser generated from * `script/search-query.pegjs` (a simplified version of Sentry's @@ -346,21 +350,30 @@ export function sanitizeQuery(query: string | undefined): string | undefined { // These fix common patterns that agents/users produce, regardless of // whether the PEG parser would accept them. const normalized = normalizeQuery(query); + const withNumericProject = rewriteNumericProjectFilters(normalized); let nodes: SearchNode[]; // biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { - nodes = parse(normalized); + nodes = parse(withNumericProject); } catch { // PEG parse still failed after normalization — pass through to the // API which returns a proper 400 with actionable details. - return normalized; + return withNumericProject; } - if (normalized !== query) { - log.warn( - `Auto-repaired search query syntax. Running query: "${normalized}"` - ); + if (withNumericProject !== query) { + const notes: string[] = []; + if (normalized !== query) { + notes.push("Auto-repaired search query syntax."); + } + if (withNumericProject !== normalized) { + notes.push( + "`project` is the slug; numeric ids use project_id. Rewrote numeric project: filters." + ); + } + notes.push(`Running query: "${withNumericProject}"`); + log.warn(notes.join(" ")); } // Check for OR inside paren groups first — these are opaque and can't @@ -394,7 +407,7 @@ export function sanitizeQuery(query: string | undefined): string | undefined { return sanitized; } - return normalized; + return withNumericProject; } /** @@ -511,6 +524,15 @@ const BALANCED_BRACKET_RE = /\[[^\]]*\]/g; /** Trailing comma before closing bracket: `,]` */ const TRAILING_LIST_COMMA_RE = /,\s*\]$/; +/** + * `project:` as its own filter — not `bolt.project`, not `project_id`. + * Issue search treats `project` as a slug and `project_id` as a numeric id. + */ +const PROJECT_NUMERIC_RE = /(^|\s)(!?)project:(\d+)(?=\s|$)/gi; + +/** `project:[123,456]` — every list value must be digits. */ +const PROJECT_NUMERIC_LIST_RE = /(^|\s)(!?)project:\[(\d+(?:\s*,\s*\d+)*)\]/gi; + /** * Pattern that splits a query into alternating unquoted / quoted segments. * @@ -519,6 +541,23 @@ const TRAILING_LIST_COMMA_RE = /,\s*\]$/; */ const QUOTED_SEGMENT_RE = /"(?:[^"\\]|\\.)*"/g; +/** + * Rewrite `project:` / `project:[digits,…]` to `project_id`. + * + * `project` is the slug; a numeric value is almost always a pasted Sentry + * project id (CLI-FA). Namespaced keys (`bolt.project:…`) and slugs are + * untouched. Quoted regions are preserved via {@link transformUnquoted}. + */ +function rewriteNumericProjectFilters(query: string): string { + return transformUnquoted(query, (segment) => { + PROJECT_NUMERIC_RE.lastIndex = 0; + PROJECT_NUMERIC_LIST_RE.lastIndex = 0; + return segment + .replace(PROJECT_NUMERIC_RE, "$1$2project_id:$3") + .replace(PROJECT_NUMERIC_LIST_RE, "$1$2project_id:[$3]"); + }); +} + /** * Normalize a search query by applying a pipeline of text repairs. * diff --git a/packages/cli/test/lib/search-query.test.ts b/packages/cli/test/lib/search-query.test.ts index 05a37bdfe6..e27d3bf5c7 100644 --- a/packages/cli/test/lib/search-query.test.ts +++ b/packages/cli/test/lib/search-query.test.ts @@ -98,6 +98,63 @@ describe("sanitizeQuery: AND", () => { }); }); +// --------------------------------------------------------------------------- +// project: → project_id +// --------------------------------------------------------------------------- + +describe("sanitizeQuery: numeric project:", () => { + test("rewrites a numeric project: filter to project_id", () => { + expect( + sanitizeQuery("project:4511730126487632 environment:vercel-production") + ).toBe("project_id:4511730126487632 environment:vercel-production"); + }); + + test("rewrites a numeric project: in-list", () => { + expect( + sanitizeQuery("is:unresolved project:[4505521413357568,6442225]") + ).toBe("is:unresolved project_id:[4505521413357568,6442225]"); + }); + + test("rewrites a negated numeric project: filter", () => { + expect(sanitizeQuery("!project:1423462 lastSeen:-1h")).toBe( + "!project_id:1423462 lastSeen:-1h" + ); + }); + + test("leaves project slugs alone", () => { + expect(sanitizeQuery("project:frontend is:unresolved")).toBe( + "project:frontend is:unresolved" + ); + }); + + test("leaves project_id numeric filters alone", () => { + expect(sanitizeQuery("project_id:4511730126487632")).toBe( + "project_id:4511730126487632" + ); + }); + + test("leaves namespaced project keys alone", () => { + expect(sanitizeQuery("bolt.project_id:70054175")).toBe( + "bolt.project_id:70054175" + ); + expect(sanitizeQuery("bolt.project:70054175")).toBe( + "bolt.project:70054175" + ); + }); + + test("does not rewrite a numeric id inside a quoted value", () => { + expect(sanitizeQuery('message:"project:4511730126487632"')).toBe( + 'message:"project:4511730126487632"' + ); + }); + + test("does not rewrite mixed slug/numeric in-lists", () => { + expect(sanitizeQuery("project:[frontend,6442225]")).toBe( + "project:[frontend,6442225]" + ); + }); +}); + // --------------------------------------------------------------------------- // OR → in-list rewrites (successful) // ---------------------------------------------------------------------------