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
14 changes: 9 additions & 5 deletions packages/cli/src/lib/api/infrastructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ export type ApiRequestOptions<T = unknown> = {

/**
* 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;
Expand All @@ -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 =
Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;

Expand Down
10 changes: 5 additions & 5 deletions packages/cli/src/lib/explore-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 <org>/ ${argument} --dataset ${dataset}`
Expand Down
23 changes: 18 additions & 5 deletions packages/cli/test/commands/explore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -1395,7 +1396,7 @@ describe("sentry explore", () => {
"tags[cart.total,number]; to check the key and its type, run: sentry attribute list <org>/ cart.total --dataset spans",
],
[
"transaction.duration on transactions",
"the server's hint for transaction.duration on transactions",
{
dataset: "transactions",
field: ["transaction", "sum(transaction.duration)"],
Expand All @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/test/lib/api-client.coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.",
},
});
});

Expand Down
7 changes: 6 additions & 1 deletion packages/cli/test/lib/api/infrastructure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
}),
);
});
Expand Down
Loading