diff --git a/packages/cli/src/lib/api/infrastructure.ts b/packages/cli/src/lib/api/infrastructure.ts index a3961c3fe..9acb86d95 100644 --- a/packages/cli/src/lib/api/infrastructure.ts +++ b/packages/cli/src/lib/api/infrastructure.ts @@ -233,8 +233,8 @@ export type ApiRequestOptions = { /** * Structured fields of a JSON error body: its string `code` (e.g. - * `invalid_search_query`) and the rejected `function` and `argument` - * (sent with `invalid_function_argument`). + * `invalid_search_query`) and the rejected `function` and `argument`, with an + * optional `hint` (sent with `invalid_function_argument`). */ function readErrorFields(body: unknown): { code?: string; @@ -251,10 +251,14 @@ function readErrorFields(body: unknown): { ) { return { code }; } - return { - code, - functionArgument: { function: fields.function, argument: fields.argument }, + const functionArgument: FunctionArgument = { + function: fields.function, + argument: fields.argument, }; + if (typeof fields.hint === "string") { + functionArgument.hint = fields.hint; + } + return { code, functionArgument }; } const CONNECT_TIMEOUT_HINT = diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts index c2c2e25bb..02d6861a6 100644 --- a/packages/cli/src/lib/errors.ts +++ b/packages/cli/src/lib/errors.ts @@ -169,8 +169,12 @@ export class HostScopeError extends CliError { } } -/** A function argument named by a Sentry 400 error body. */ -export type FunctionArgument = { function: string; argument: string }; +/** A function argument named by a Sentry 400 error body, with the server's suggested fix. */ +export type FunctionArgument = { + function: string; + argument: string; + hint?: string; +}; /** * API request errors from Sentry. @@ -194,7 +198,8 @@ export class ApiError extends CliError { /** * Function and argument the server rejected, sent with code - * `invalid_function_argument` (e.g. `p75` and `cart.total`). + * `invalid_function_argument` (e.g. `p75` and `cart.total`), plus its + * `hint` when the server suggests a fix. */ readonly functionArgument?: FunctionArgument; diff --git a/packages/cli/src/lib/explore-errors.ts b/packages/cli/src/lib/explore-errors.ts index 6846079d8..09f457c2d 100644 --- a/packages/cli/src/lib/explore-errors.ts +++ b/packages/cli/src/lib/explore-errors.ts @@ -28,7 +28,8 @@ const INVALID_FUNCTION_ARGUMENT_CODE = "invalid_function_argument"; /** * Match narrow backend diagnostics without maintaining an aggregate catalog. * Beyond the specific corrections, a 400 whose structured fields name this - * expression's own function and argument gets a generic hint. + * expression's own function and argument gets the server's hint, or a generic + * one when the server sent none. */ function recoveryHint( detail: string, @@ -72,11 +73,10 @@ function recoveryHint( ) { return; } - const { argument } = rejected; - // `--dataset transactions` queries spans, which keep durations in span.duration. - if (dataset === "spans" && argument === "transaction.duration") { - return "Use span.duration instead of transaction.duration. Transactions are queried as spans, which store their duration in span.duration."; + if (rejected.hint) { + return rejected.hint; } + const { argument } = rejected; const problem = `${parsed.name}() does not accept ${argument} on dataset "${dataset}": it may not exist there, or may not have the type ${parsed.name}() needs.`; return ATTRIBUTE_DATASETS.has(dataset) ? `${problem} Reference a numeric custom attribute with its type, e.g. tags[${argument},number]; to check the key and its type, run: sentry attribute list / ${argument} --dataset ${dataset}` diff --git a/packages/cli/test/commands/explore.test.ts b/packages/cli/test/commands/explore.test.ts index cc0188fc6..fda9d44e4 100644 --- a/packages/cli/test/commands/explore.test.ts +++ b/packages/cli/test/commands/explore.test.ts @@ -1365,21 +1365,22 @@ describe("sentry explore", () => { expect((error as Error).message).not.toContain("attribute list"); }); - const rejectedArgument = (fn: string, argument: string) => ({ + const rejectedArgument = (fn: string, argument: string, hint?: string) => ({ code: "invalid_function_argument", - functionArgument: { function: fn, argument }, + functionArgument: { function: fn, argument, hint }, }); const rejectedArgumentError = ( detail: string, fn: string, argument: string, + hint?: string, ) => new ApiError( "Bad request", 400, detail, undefined, - rejectedArgument(fn, argument), + rejectedArgument(fn, argument, hint), ); test.each([ @@ -1395,7 +1396,7 @@ describe("sentry explore", () => { "tags[cart.total,number]; to check the key and its type, run: sentry attribute list / cart.total --dataset spans", ], [ - "transaction.duration on transactions", + "the server's hint for transaction.duration on transactions", { dataset: "transactions", field: ["transaction", "sum(transaction.duration)"], @@ -1405,9 +1406,21 @@ describe("sentry explore", () => { "transaction.duration is invalid for parameter 1 in sum. Its a string type field, but it must be one of these types: {'number', 'duration'}", "sum", "transaction.duration", + "Use span.duration instead of transaction.duration on the spans dataset.", ), '--field "sum(transaction.duration)" for dataset "spans"', - "Use span.duration instead of transaction.duration.", + "Use span.duration instead of transaction.duration on the spans dataset.", + ], + [ + "a generic hint for transaction.duration when the server sends none", + { dataset: "transactions", field: ["sum(transaction.duration)"] }, + rejectedArgumentError( + "transaction.duration is invalid for parameter 1 in sum. Its a string type field, but it must be one of these types: {'number', 'duration'}", + "sum", + "transaction.duration", + ), + '--field "sum(transaction.duration)" for dataset "spans"', + 'sum() does not accept transaction.duration on dataset "spans"', ], [ "a timestamp aggregate on spans", diff --git a/packages/cli/test/lib/api-client.coverage.test.ts b/packages/cli/test/lib/api-client.coverage.test.ts index eda59d83a..44c1ed95b 100644 --- a/packages/cli/test/lib/api-client.coverage.test.ts +++ b/packages/cli/test/lib/api-client.coverage.test.ts @@ -1511,6 +1511,7 @@ describe("infrastructure.ts", () => { code: "invalid_function_argument", function: "p75", argument: "cart.total", + hint: "Use cart.amount instead of cart.total.", }), { status: 400, @@ -1527,7 +1528,11 @@ describe("infrastructure.ts", () => { expect(error).toMatchObject({ code: "invalid_function_argument", - functionArgument: { function: "p75", argument: "cart.total" }, + functionArgument: { + function: "p75", + argument: "cart.total", + hint: "Use cart.amount instead of cart.total.", + }, }); }); diff --git a/packages/cli/test/lib/api/infrastructure.test.ts b/packages/cli/test/lib/api/infrastructure.test.ts index 4ba46a5ae..1a8e535dc 100644 --- a/packages/cli/test/lib/api/infrastructure.test.ts +++ b/packages/cli/test/lib/api/infrastructure.test.ts @@ -336,13 +336,18 @@ describe("throwApiError", () => { code: "invalid_function_argument", function: "p75", argument: "cart.total", + hint: "Use cart.amount instead of cart.total.", }, mockResponse, "Failed to list events", ), ).toThrow( expect.objectContaining({ - functionArgument: { function: "p75", argument: "cart.total" }, + functionArgument: { + function: "p75", + argument: "cart.total", + hint: "Use cart.amount instead of cart.total.", + }, }), ); });