fix(webapp): downgrade expected user-input error logs to warn (#3523)
`dac9c83bd` added `ignoreErrors: /^ServiceValidationError(?::|$)/` in
`apps/webapp/sentry.server.ts` to drop SVEs before they reach Sentry.
The
filter only matches when the captured event's *type* is
`ServiceValidationError`, but nine call sites in the webapp catch SVE
(and
analogous user-input error types — `OutOfEntitlementError`,
`CreateDeclarativeScheduleError`, `QueryError`) and call
`logger.error("wrapper message", { error: e })` *before* the type check.
The captured event is then titled with the wrapper message, with the
inner
error buried in `extra.error` — invisible to the SDK filter. Result: a
steady stream of expected user-input failures escalating as
`error`-level
events when they should be `warn`.
Each catch block now type-discriminates first, logs expected types at
`warn`,
and keeps unknown-error fall-throughs at `error`. For service sites that
wrap into SVE (`createBackgroundWorker`,
`createDeploymentBackgroundWorkerV4`),
the inner error is logged at `error` before wrapping — mirrors the
`waitpointCompletionPacket.server.ts` pattern from `dac9c83bd`.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
---
|
||||
area: webapp
|
||||
type: fix
|
||||
---
|
||||
|
||||
Stop nine catch sites in the webapp from escalating expected user-input
|
||||
failures (`ServiceValidationError`, `OutOfEntitlementError`,
|
||||
`CreateDeclarativeScheduleError`, `QueryError`) as `error`-level events.
|
||||
Type-discriminate before logging; downgrade the user-facing branches to
|
||||
`warn` while keeping unknown-error fall-throughs at `error`.
|
||||
@@ -60,14 +60,20 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error("Failed to create background worker", { error: e });
|
||||
|
||||
// Customer-facing validation failures (invalid task config, customer cron
|
||||
// expression, etc.). The handler returns 4xx with the message; system
|
||||
// handles it gracefully, no alert needed.
|
||||
if (e instanceof ServiceValidationError) {
|
||||
logger.warn("Failed to create background worker", { error: e.message });
|
||||
return json({ error: e.message }, { status: e.status ?? 400 });
|
||||
} else if (e instanceof CreateDeclarativeScheduleError) {
|
||||
}
|
||||
if (e instanceof CreateDeclarativeScheduleError) {
|
||||
logger.warn("Failed to create background worker", { error: e.message });
|
||||
return json({ error: e.message }, { status: 400 });
|
||||
}
|
||||
|
||||
logger.error("Failed to create background worker", { error: e });
|
||||
|
||||
return json({ error: "Failed to create background worker" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,13 +58,19 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error("Failed to create background worker", { error: JSON.stringify(e) });
|
||||
|
||||
// Customer-facing validation failures (invalid task config, customer cron
|
||||
// expression, etc.). The handler returns 4xx with the message; system
|
||||
// handles it gracefully, no alert needed.
|
||||
if (e instanceof ServiceValidationError) {
|
||||
return json({ error: e.message }, { status: 400 });
|
||||
} else if (e instanceof CreateDeclarativeScheduleError) {
|
||||
logger.warn("Failed to create background worker", { error: e.message });
|
||||
return json({ error: e.message }, { status: 400 });
|
||||
}
|
||||
if (e instanceof CreateDeclarativeScheduleError) {
|
||||
logger.warn("Failed to create background worker", { error: e.message });
|
||||
return json({ error: e.message }, { status: 400 });
|
||||
}
|
||||
|
||||
logger.error("Failed to create background worker", { error: e });
|
||||
|
||||
return json({ error: "Failed to create background worker" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -61,10 +61,16 @@ const { action, loader } = createActionApiRoute(
|
||||
});
|
||||
|
||||
if (!queryResult.success) {
|
||||
const message =
|
||||
queryResult.error instanceof QueryError
|
||||
? queryResult.error.message
|
||||
: "An unexpected error occurred while executing the query.";
|
||||
// QueryError surfaces customer SQL problems (invalid syntax,
|
||||
// unsupported construct). Returned to the caller as 400; system
|
||||
// handles it gracefully, no alert needed.
|
||||
if (queryResult.error instanceof QueryError) {
|
||||
logger.warn("Query API error", {
|
||||
error: queryResult.error.message,
|
||||
query,
|
||||
});
|
||||
return json({ error: queryResult.error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
logger.error("Query API error", {
|
||||
error: queryResult.error,
|
||||
@@ -72,8 +78,8 @@ const { action, loader } = createActionApiRoute(
|
||||
});
|
||||
|
||||
return json(
|
||||
{ error: message },
|
||||
{ status: queryResult.error instanceof QueryError ? 400 : 500 }
|
||||
{ error: "An unexpected error occurred while executing the query." },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,18 @@ const { action, loader } = createActionApiRoute(
|
||||
|
||||
return json(batch, { status: 202, headers: $responseHeaders });
|
||||
} catch (error) {
|
||||
// Customer-facing validation/quota failures (invalid batch shape,
|
||||
// entitlements exhausted). The handler returns 422 with the message;
|
||||
// system handles it gracefully, no alert needed.
|
||||
if (error instanceof ServiceValidationError) {
|
||||
logger.warn("Batch trigger error", { error: error.message });
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
}
|
||||
if (error instanceof OutOfEntitlementError) {
|
||||
logger.warn("Batch trigger error", { error: error.message });
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
}
|
||||
|
||||
logger.error("Batch trigger error", {
|
||||
error: {
|
||||
message: (error as Error).message,
|
||||
@@ -134,11 +146,7 @@ const { action, loader } = createActionApiRoute(
|
||||
},
|
||||
});
|
||||
|
||||
if (error instanceof ServiceValidationError) {
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
} else if (error instanceof OutOfEntitlementError) {
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
} else if (error instanceof Error) {
|
||||
if (error instanceof Error) {
|
||||
return json(
|
||||
{ error: "Something went wrong" },
|
||||
{ status: 500, headers: { "x-should-retry": "false" } }
|
||||
|
||||
@@ -144,6 +144,18 @@ const { action, loader } = createActionApiRoute(
|
||||
headers: $responseHeaders,
|
||||
});
|
||||
} catch (error) {
|
||||
// Customer-facing validation/quota failures (invalid batch shape,
|
||||
// entitlements exhausted). The handler returns 422 with the message;
|
||||
// system handles it gracefully, no alert needed.
|
||||
if (error instanceof ServiceValidationError) {
|
||||
logger.warn("Batch trigger error", { error: error.message });
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
}
|
||||
if (error instanceof OutOfEntitlementError) {
|
||||
logger.warn("Batch trigger error", { error: error.message });
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
}
|
||||
|
||||
logger.error("Batch trigger error", {
|
||||
error: {
|
||||
message: (error as Error).message,
|
||||
@@ -151,11 +163,7 @@ const { action, loader } = createActionApiRoute(
|
||||
},
|
||||
});
|
||||
|
||||
if (error instanceof ServiceValidationError) {
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
} else if (error instanceof OutOfEntitlementError) {
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
} else if (error instanceof Error) {
|
||||
if (error instanceof Error) {
|
||||
return json(
|
||||
{ error: error.message },
|
||||
{ status: 500, headers: { "x-should-retry": "false" } }
|
||||
|
||||
@@ -88,6 +88,22 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
|
||||
return json(result, { status: 200 });
|
||||
} catch (error) {
|
||||
// Customer-facing validation failures (invalid item shape, invalid JSON
|
||||
// in the streamed body). The handler returns 4xx with the message;
|
||||
// system handles it gracefully, no alert needed.
|
||||
if (error instanceof ServiceValidationError) {
|
||||
logger.warn("Stream batch items error", { batchId, error: error.message });
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.includes("Invalid JSON")) {
|
||||
logger.warn("Stream batch items error: invalid JSON", {
|
||||
batchId,
|
||||
error: error.message,
|
||||
});
|
||||
return json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
logger.error("Stream batch items error", {
|
||||
batchId,
|
||||
error: {
|
||||
@@ -96,14 +112,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
},
|
||||
});
|
||||
|
||||
if (error instanceof ServiceValidationError) {
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
} else if (error instanceof Error) {
|
||||
// Check for stream parsing errors (e.g. invalid JSON)
|
||||
if (error.message.includes("Invalid JSON")) {
|
||||
return json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
|
||||
@@ -172,6 +172,18 @@ const { action, loader } = createActionApiRoute(
|
||||
);
|
||||
}
|
||||
|
||||
// Customer-facing validation/quota failures (invalid batch shape,
|
||||
// entitlements exhausted). The handler returns 422 with the message;
|
||||
// system handles it gracefully, no alert needed.
|
||||
if (error instanceof ServiceValidationError) {
|
||||
logger.warn("Create batch error", { error: error.message });
|
||||
return json({ error: error.message }, { status: error.status ?? 422 });
|
||||
}
|
||||
if (error instanceof OutOfEntitlementError) {
|
||||
logger.warn("Create batch error", { error: error.message });
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
}
|
||||
|
||||
logger.error("Create batch error", {
|
||||
error: {
|
||||
message: (error as Error).message,
|
||||
@@ -179,11 +191,7 @@ const { action, loader } = createActionApiRoute(
|
||||
},
|
||||
});
|
||||
|
||||
if (error instanceof ServiceValidationError) {
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
} else if (error instanceof OutOfEntitlementError) {
|
||||
return json({ error: error.message }, { status: 422 });
|
||||
} else if (error instanceof Error) {
|
||||
if (error instanceof Error) {
|
||||
return json(
|
||||
{ error: error.message },
|
||||
{ status: 500, headers: { "x-should-retry": "false" } }
|
||||
|
||||
@@ -146,16 +146,27 @@ export class CreateBackgroundWorkerService extends BaseService {
|
||||
);
|
||||
|
||||
if (schedulesError) {
|
||||
if (schedulesError instanceof ServiceValidationError) {
|
||||
// Customer schedule config (typically invalid cron). Surface to
|
||||
// client via the rethrow; system returns gracefully.
|
||||
logger.warn("Error syncing declarative schedules", {
|
||||
error: schedulesError.message,
|
||||
backgroundWorker,
|
||||
environment,
|
||||
});
|
||||
throw schedulesError;
|
||||
}
|
||||
|
||||
// Wrapping the underlying error into a ServiceValidationError below
|
||||
// would otherwise hide it once the SDK-level filter drops SVEs; log at
|
||||
// error so the underlying cause stays visible. Mirrors the
|
||||
// waitpointCompletionPacket.server.ts pattern from dac9c83bd.
|
||||
logger.error("Error syncing declarative schedules", {
|
||||
error: schedulesError,
|
||||
backgroundWorker,
|
||||
environment,
|
||||
});
|
||||
|
||||
if (schedulesError instanceof ServiceValidationError) {
|
||||
throw schedulesError;
|
||||
}
|
||||
|
||||
throw new ServiceValidationError("Error syncing declarative schedules");
|
||||
}
|
||||
|
||||
|
||||
@@ -139,14 +139,26 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
|
||||
);
|
||||
|
||||
if (schedulesError) {
|
||||
if (schedulesError instanceof ServiceValidationError) {
|
||||
// Customer schedule config (typically invalid cron). Surface to
|
||||
// client via the rethrow; system returns gracefully.
|
||||
logger.warn("Error syncing declarative schedules", {
|
||||
error: schedulesError.message,
|
||||
});
|
||||
|
||||
await this.#failBackgroundWorkerDeployment(deployment, schedulesError);
|
||||
throw schedulesError;
|
||||
}
|
||||
|
||||
// Wrapping the underlying error into a ServiceValidationError below
|
||||
// would otherwise hide it once the SDK-level filter drops SVEs; log at
|
||||
// error so the underlying cause stays visible. Mirrors the
|
||||
// waitpointCompletionPacket.server.ts pattern from dac9c83bd.
|
||||
logger.error("Error syncing declarative schedules", {
|
||||
error: schedulesError,
|
||||
});
|
||||
|
||||
const serviceError =
|
||||
schedulesError instanceof ServiceValidationError
|
||||
? schedulesError
|
||||
: new ServiceValidationError("Error syncing declarative schedules");
|
||||
const serviceError = new ServiceValidationError("Error syncing declarative schedules");
|
||||
|
||||
await this.#failBackgroundWorkerDeployment(deployment, serviceError);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user