Skip to content
Closed
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
42 changes: 41 additions & 1 deletion packages/cli/src/lib/error-reporting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ import {
SeerError,
TimeoutError,
UpgradeError,
USER_QUERY_SYNTAX_FIELD,
ValidationError,
WizardError,
} from "./errors.js";
import { logger } from "./logger.js";
import { isSaaS } from "./sentry-web-urls.js";

// ---------------------------------------------------------------------------
Expand All @@ -74,6 +76,7 @@ type SilenceReason =
| "process_exit"
| "user_validation"
| "user_input_error"
| "user_query_syntax"
| "seer_unavailable_self_hosted";

/**
Expand Down Expand Up @@ -136,6 +139,19 @@ export function classifySilenced(error: unknown): SilenceReason | null {
if (error instanceof ValidationError && error.field === "directory") {
return "user_input_error";
}
// The Sentry server rejected a search query the user typed (e.g.
// `issue.category:csp`, `status:401`). toSearchQueryError only uses this
// field when the user actually supplied `--query`, so it is a user input
// mistake that the CLI has no way to fix. The user already sees the
// server's detail plus a syntax hint. Queries the CLI builds itself still
// come through as raw 400 ApiErrors and stay captured. Volume stays visible
// through the `cli.error.silenced` metric and a structured log (CLI-1CZ).
if (
error instanceof ValidationError &&
error.field === USER_QUERY_SYNTAX_FIELD
) {
return "user_query_syntax";
}
// A ResolutionError means the user provided a value (event ID, project slug,
// etc.) that was looked up but not found. This is pure user-input noise, not
// a CLI bug — the user sees a clear "not found" message (CLI-RP).
Expand Down Expand Up @@ -206,6 +222,21 @@ function recordSilencedError(error: unknown, reason: SilenceReason): void {
// Logger may not be initialized — ignore.
}
}

// Structured log for rejected user queries — keeps the server's parse
// detail searchable in Sentry Logs now that these are not issues.
if (reason === "user_query_syntax" && error instanceof Error) {
try {
Sentry.logger.info("cli.query_syntax_silenced", {
detail: redactCredentialText(error.message).split("\n", 1)[0] ?? "",
});
} catch (logError) {
// Logger may not be initialized; never block error handling.
logger
.withTag("error-reporting")
.debug("Failed to emit cli.query_syntax_silenced log", logError);
}
}
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -319,7 +350,16 @@ function deriveErrorKind(error: Error): string | undefined {
// (e.g. validateHexId throws with no `field`, so using field would
// collapse every unfielded ValidationError into one group).
if (error instanceof ValidationError) {
return error.field ?? extractMessagePrefix(error.message);
if (error.field === undefined) {
return extractMessagePrefix(error.message);
}
// "query" is shared by unrelated validators (aggregate names, empty alert
// queries, client-side query sanitizing). Keying on the field alone put
// all of them in one issue (CLI-1CZ), so add the message prefix.
if (error.field === "query") {
return `query: ${extractMessagePrefix(error.message)}`;
}
return error.field;
}
if (error instanceof ApiError) {
return String(error.status);
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,14 @@ export function isSearchQueryParseError(error: ApiError): boolean {
export const SEARCH_QUERY_HELP =
"Check your --query syntax (Sentry search reference: https://docs.sentry.io/concepts/search/)";

/**
* `ValidationError.field` used when the Sentry server rejects a search query
* the user typed. It differs from the generic `"query"` field so error
* reporting can tell a user's own query mistake apart from a CLI-side query
* validation defect (CLI-1CZ).
*/
export const USER_QUERY_SYNTAX_FIELD = "query.user_syntax";

/**
* Convert a Sentry search-query parse failure into a {@link ValidationError}
* when the user actually supplied the query; otherwise return the error
Expand Down Expand Up @@ -899,7 +907,7 @@ export function toSearchQueryError(
for (const suggestion of [SEARCH_QUERY_HELP, ...extraSuggestions]) {
lines.push(` • ${suggestion}`);
}
return new ValidationError(lines.join("\n"), "query");
return new ValidationError(lines.join("\n"), USER_QUERY_SYNTAX_FIELD);
}
return error;
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/types/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export const SPAN_AGGREGATE_FUNCTIONS = [
"any",
"min",
"max",
"failure_rate",
] as const;

export type SpanAggregateFunction = (typeof SPAN_AGGREGATE_FUNCTIONS)[number];
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/commands/issue/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ describe("issue list: error propagation", () => {
expect.unreachable("Should have thrown");
} catch (error) {
expect(error).toBeInstanceOf(ValidationError);
expect((error as ValidationError).field).toBe("query");
expect((error as ValidationError).field).toBe("query.user_syntax");
expect((error as Error).message).toContain("Error parsing search query");
}
});
Expand Down
44 changes: 44 additions & 0 deletions packages/cli/test/lib/error-reporting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
OutputError,
ResolutionError,
SeerError,
USER_QUERY_SYNTAX_FIELD,
ValidationError,
WizardError,
} from "../../src/lib/errors.js";
Expand Down Expand Up @@ -355,6 +356,25 @@ describe("classifySilenced", () => {
).toBe("user_input_error");
});

test("silences a user's server-rejected --query (CLI-1CZ)", () => {
expect(
classifySilenced(
new ValidationError(
"Error parsing search query: Invalid category value of 'csp'",
USER_QUERY_SYNTAX_FIELD,
),
),
).toBe("user_query_syntax");
});

test("does NOT silence a CLI-side ValidationError with field 'query'", () => {
expect(
classifySilenced(
new ValidationError('Unknown aggregate function "bogus".', "query"),
),
).toBeNull();
});

test("silences ResolutionError (user provided a value that wasn't found)", () => {
expect(
classifySilenced(
Expand Down Expand Up @@ -559,6 +579,30 @@ describe("reportCliError integration", () => {
return { tags, contexts };
}

test("records a user query syntax error as a silenced metric, not an issue", () => {
reportCliError(
new ValidationError(
"Error parsing search query: Invalid category value of 'csp'",
USER_QUERY_SYNTAX_FIELD,
),
);
expect(captureSpy).not.toHaveBeenCalled();
expect(metricSpy).toHaveBeenCalledWith(
"cli.error.silenced",
1,
expect.objectContaining({
attributes: expect.objectContaining({ reason: "user_query_syntax" }),
}),
);
});

test("groups 'query' ValidationErrors by message prefix (CLI-1CZ)", () => {
const { tags } = capturedScopeTags(
new ValidationError('Unknown aggregate function "bogus".', "query"),
);
expect(tags["cli_error.kind"]).toBe("query: Unknown aggregate function");
});

test("captures ContextError (no longer silenced) so its volume stays visible", () => {
reportCliError(
new ContextError("Organization and project", "sentry org view <slug>"),
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/lib/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ describe("toSearchQueryError", () => {
test("converts a parse 400 to a ValidationError when the user supplied --query", () => {
const result = toSearchQueryError(parseError(), "is:403");
expect(result).toBeInstanceOf(ValidationError);
expect((result as ValidationError).field).toBe("query");
expect((result as ValidationError).field).toBe("query.user_syntax");
});

test("includes the server detail and a search-syntax suggestion in the message", () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/test/types/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ describe("SPAN_AGGREGATE_FUNCTIONS", () => {
expect(SPAN_AGGREGATE_FUNCTIONS).not.toContain("spm");
});

test("contains failure_rate (CLI-1CZ)", () => {
expect(SPAN_AGGREGATE_FUNCTIONS).toContain("failure_rate");
});

test("valibot schema validates known functions", () => {
expect(safeParse(SpanAggregateFunctionSchema, "count").success).toBe(true);
expect(safeParse(SpanAggregateFunctionSchema, "p95").success).toBe(true);
Expand Down Expand Up @@ -967,6 +971,12 @@ describe("mapWidgetTypeToDataset", () => {
});

describe("validateAggregateNames", () => {
test("accepts failure_rate() for the spans dataset (CLI-1CZ)", () => {
expect(() =>
validateAggregateNames(["count()", "failure_rate()"], "spans"),
).not.toThrow();
});

test("accepts comma-separated tracemetrics aggregates", () => {
expect(() =>
validateAggregateNames(
Expand Down
Loading