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:
Daniel Sutton
2026-05-05 11:31:39 +01:00
committed by GitHub
parent a8966a40ab
commit 14920ce2c4
10 changed files with 128 additions and 44 deletions
@@ -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 });
}
+12 -6
View File
@@ -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 }
);
}
+13 -5
View File
@@ -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" } }
+13 -5
View File
@@ -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 });
}
+13 -5
View File
@@ -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);