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
67 changes: 44 additions & 23 deletions packages/cli/src/lib/search-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,19 +362,7 @@ export function sanitizeQuery(query: string | undefined): string | undefined {
return withNumericProject;
}

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(" "));
}
const notes = preParseRewriteNotes(query, normalized, withNumericProject);

// Check for OR inside paren groups first — these are opaque and can't
// be rewritten. Must throw even if top-level OR would be rewritable,
Expand All @@ -395,37 +383,70 @@ export function sanitizeQuery(query: string | undefined): string | undefined {
if (hasOr) {
// Strip AND nodes before OR rewrite
const withoutAnd = hasAnd ? stripAndNodes(nodes) : nodes;
return handleOr(withoutAnd, hasAnd);
const result = handleOr(withoutAnd, hasAnd, notes);
warnRunningQuery(notes, result);
return result;
}

if (hasAnd) {
const sanitized = serializeNodes(stripAndNodes(nodes));
log.warn(
"Sentry search implicitly ANDs terms — removed explicit AND operator. " +
`Running query: "${sanitized}"`
notes.push(
"Sentry search implicitly ANDs terms — removed explicit AND operator."
);
warnRunningQuery(notes, sanitized);
return sanitized;
}

warnRunningQuery(notes, withNumericProject);
return withNumericProject;
}

/** Notes from text-layer rewrites that run before PEG parse. */
function preParseRewriteNotes(
query: string,
normalized: string,
withNumericProject: string
): string[] {
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."
);
}
return notes;
}

/**
* One warning after every successful rewrite. Reasons on the first
* line; the query that will actually be sent on the second. Skip if
* nothing changed.
*/
function warnRunningQuery(notes: string[], result: string): void {
if (notes.length === 0) {
return;
}
log.warn(`${notes.join(" ")}\nRunning query: "${result}"`);
}

/**
* Handle the OR rewrite path — extracted to keep `sanitizeQuery` under
* the cognitive complexity limit.
*/
function handleOr(nodes: SearchNode[], hasAnd: boolean): string {
function handleOr(
nodes: SearchNode[],
hasAnd: boolean,
notes: string[]
): string {
const rewritten = tryRewriteOr(nodes);
if (rewritten) {
const result = serializeNodes(rewritten);
const notes: string[] = [];
notes.push("Rewrote OR using in-list syntax: key:[val1,val2].");
if (hasAnd) {
notes.push("Also removed explicit AND (implicit in Sentry search).");
}
notes.push(`Running query: "${result}"`);
log.warn(notes.join(" "));
return result;
return serializeNodes(rewritten);
}

throw new ValidationError(
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/test/lib/search-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ describe("sanitizeQuery: numeric project:", () => {
"project:[frontend,6442225]"
);
});

test("rewrites numeric project: then OR in one step", () => {
expect(sanitizeQuery("project:123 OR project:456")).toBe(
"project_id:[123,456]"
);
});
});

// ---------------------------------------------------------------------------
Expand Down
86 changes: 86 additions & 0 deletions packages/cli/test/lib/search-query.warn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Warning copy for stacked search-query rewrites.
*
* `sanitizeQuery` must emit one warn: reasons, then a newline, then
* `Running query:` quoting the string that is actually sent.
*/

import { beforeEach, describe, expect, test, vi } from "vitest";

const { fakeLog } = vi.hoisted(() => {
const log = {
warn: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
error: vi.fn(),
withTag() {
return log;
},
};
return { fakeLog: log };
});

vi.mock("../../src/lib/logger.js", () => ({
logger: fakeLog,
}));

const { sanitizeQuery } = await import("../../src/lib/search-query.js");

function runningQueries(): string[] {
return fakeLog.warn.mock.calls
.map((call) => String(call[0]))
.filter((msg) => msg.includes("Running query:"));
}

describe("sanitizeQuery: rewrite warnings", () => {
beforeEach(() => {
fakeLog.warn.mockClear();
});

test("numeric project: plus OR warns once with the final in-list", () => {
expect(sanitizeQuery("project:123 OR project:456")).toBe(
"project_id:[123,456]"
);
const warns = runningQueries();
expect(warns).toHaveLength(1);
expect(warns[0].split("\n")).toEqual([
"`project` is the slug; numeric ids use project_id. Rewrote numeric project: filters. Rewrote OR using in-list syntax: key:[val1,val2].",
'Running query: "project_id:[123,456]"',
]);
expect(warns[0]).not.toContain("project_id:123 OR project_id:456");
});

test("numeric project: plus AND warns once with the stripped query", () => {
expect(sanitizeQuery("project:123 AND is:unresolved")).toBe(
"project_id:123 is:unresolved"
);
const warns = runningQueries();
expect(warns).toHaveLength(1);
expect(warns[0].split("\n")).toEqual([
"`project` is the slug; numeric ids use project_id. Rewrote numeric project: filters. Sentry search implicitly ANDs terms — removed explicit AND operator.",
'Running query: "project_id:123 is:unresolved"',
]);
});

test("OR-only still warns once with the in-list", () => {
expect(sanitizeQuery("level:error OR level:warning")).toBe(
"level:[error,warning]"
);
const warns = runningQueries();
expect(warns).toHaveLength(1);
expect(warns[0].split("\n")).toEqual([
"Rewrote OR using in-list syntax: key:[val1,val2].",
'Running query: "level:[error,warning]"',
]);
});

test("does not warn Running query: when OR rewrite fails", () => {
expect(() => sanitizeQuery("level:error OR assigned:me")).toThrow();
expect(runningQueries()).toHaveLength(0);
});

test("does not warn Running query: when numeric rewrite is followed by a failed OR", () => {
expect(() => sanitizeQuery("project:123 OR assigned:me")).toThrow();
expect(runningQueries()).toHaveLength(0);
});
});
Loading