Skip to content
Open
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
53 changes: 46 additions & 7 deletions packages/cli/src/lib/search-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:<digits>`**: `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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -394,7 +407,7 @@ export function sanitizeQuery(query: string | undefined): string | undefined {
return sanitized;
}

return normalized;
return withNumericProject;
}

/**
Expand Down Expand Up @@ -511,6 +524,15 @@ const BALANCED_BRACKET_RE = /\[[^\]]*\]/g;
/** Trailing comma before closing bracket: `,]` */
const TRAILING_LIST_COMMA_RE = /,\s*\]$/;

/**
* `project:<digits>` 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.
*
Expand All @@ -519,6 +541,23 @@ const TRAILING_LIST_COMMA_RE = /,\s*\]$/;
*/
const QUOTED_SEGMENT_RE = /"(?:[^"\\]|\\.)*"/g;

/**
* Rewrite `project:<digits>` / `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.
Comment thread
betegon marked this conversation as resolved.
*
Expand Down
57 changes: 57 additions & 0 deletions packages/cli/test/lib/search-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,63 @@ describe("sanitizeQuery: AND", () => {
});
});

// ---------------------------------------------------------------------------
// project:<digits> → 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)
// ---------------------------------------------------------------------------
Expand Down
Loading