From bcad519c7717fd51393aaf4cb9da9a99d31efcf7 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 1 May 2026 17:17:29 +0100 Subject: [PATCH 01/10] chore(webapp): downgrade SVE logs in api.v1.projects.background-workers to warn ServiceValidationError and CreateDeclarativeScheduleError are user-facing validation failures returned as 4xx; they shouldn't surface in Sentry. Move type discrimination before the log call and split the SVE/CDSE branches at warn level. Real 5xx errors continue to log at error. Refs the ignoreErrors filter added in dac9c83bd; closes the wrapper-bypass gap on TRIGGER-CLOUD-2S. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...api.v1.projects.$projectRef.background-workers.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts index 872ca6f2f53..bc9842f0afa 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.background-workers.ts @@ -58,14 +58,20 @@ 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) { + logger.warn("Failed to create background worker", { error: e.message }); return json({ error: e.message }, { 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 }); } } From 72b11ff8fdcf898465302e5eca96d00800109477 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 1 May 2026 17:19:11 +0100 Subject: [PATCH 02/10] chore(webapp): downgrade SVE logs in api.v1.deployments.background-workers to warn Same wrapper-bypass pattern as the projects-scoped sibling; both feed TRIGGER-CLOUD-2S. Type-discriminate before the log; warn for SVE and CreateDeclarativeScheduleError, error for unknown. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...1.deployments.$deploymentId.background-workers.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts index edaa1b257e5..c22399ef60e 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.background-workers.ts @@ -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 }); } } From ddd043a6ef5d2e88b5c41fadf730f6b1e7fdb169 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 1 May 2026 17:20:53 +0100 Subject: [PATCH 03/10] chore(webapp): downgrade SVE/OOE logs in api.v1.tasks.batch to warn Closes the wrapper-bypass gap on TRIGGER-CLOUD-38 (Batch trigger error, 755 events in 48h). Customer-facing 422 paths now log at warn; only the 500 fall-through escalates to Sentry. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/webapp/app/routes/api.v1.tasks.batch.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.tasks.batch.ts b/apps/webapp/app/routes/api.v1.tasks.batch.ts index e6ada1a739c..50760b79a6d 100644 --- a/apps/webapp/app/routes/api.v1.tasks.batch.ts +++ b/apps/webapp/app/routes/api.v1.tasks.batch.ts @@ -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" } } From 82ac9284f96bbde79a59765fa617465c8e663618 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 1 May 2026 17:22:12 +0100 Subject: [PATCH 04/10] chore(webapp): downgrade SVE/OOE logs in api.v2.tasks.batch to warn Same wrapper-bypass shape as the v1 sibling. Customer-facing 422 paths now log at warn; only the 500 fall-through escalates to Sentry. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/webapp/app/routes/api.v2.tasks.batch.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/routes/api.v2.tasks.batch.ts b/apps/webapp/app/routes/api.v2.tasks.batch.ts index 8db98b4d343..e45f7508b90 100644 --- a/apps/webapp/app/routes/api.v2.tasks.batch.ts +++ b/apps/webapp/app/routes/api.v2.tasks.batch.ts @@ -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" } } From 350e1d7184bddf8d47135caadeb5a0bac2c81148 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 1 May 2026 17:22:48 +0100 Subject: [PATCH 05/10] chore(webapp): downgrade SVE/OOE logs in api.v3.batches to warn Customer-facing 422 paths now log at warn; only the 500 fall-through escalates to Sentry. Rate-limit (BatchProcessingError 429) branch above is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/webapp/app/routes/api.v3.batches.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/routes/api.v3.batches.ts b/apps/webapp/app/routes/api.v3.batches.ts index 5067eaef06e..b671a8efbd6 100644 --- a/apps/webapp/app/routes/api.v3.batches.ts +++ b/apps/webapp/app/routes/api.v3.batches.ts @@ -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" } } From 270d1727b184507e36b1fb5b4dc0c3b40ea1f6d6 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 1 May 2026 17:24:24 +0100 Subject: [PATCH 06/10] chore(webapp): downgrade SVE/Invalid-JSON logs in api.v3.batches.items to warn Closes the wrapper-bypass gap on TRIGGER-CLOUD-103 (Stream batch items error, 1052 events in 48h). Customer-facing 4xx paths (invalid item shape, invalid JSON in stream body) now log at warn; only the 500 fall-through escalates to Sentry. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../routes/api.v3.batches.$batchId.items.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts b/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts index 2d732d1555a..0e26bae94e3 100644 --- a/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts +++ b/apps/webapp/app/routes/api.v3.batches.$batchId.items.ts @@ -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 }); } From 0346668317cd0a7565a15d7397fd78fd6d8cc3c3 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 1 May 2026 17:25:19 +0100 Subject: [PATCH 07/10] chore(webapp): downgrade QueryError logs in api.v1.query to warn QueryError surfaces customer SQL problems and is returned as 400; not a system bug. Type-discriminate before the log call; warn for QueryError, error for unknown failures. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/webapp/app/routes/api.v1.query.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.query.ts b/apps/webapp/app/routes/api.v1.query.ts index 22500011671..05d92e9726a 100644 --- a/apps/webapp/app/routes/api.v1.query.ts +++ b/apps/webapp/app/routes/api.v1.query.ts @@ -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 } ); } From ee470dc54389c5ef1bc1abb7808f8b6e2f4a7261 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 1 May 2026 17:26:48 +0100 Subject: [PATCH 08/10] chore(webapp): downgrade SVE schedule-sync log in createBackgroundWorker to warn Customer schedule-config failures (typically invalid cron) get rethrown to the route handler as ServiceValidationError; that's a 4xx, not a system bug. Split the SVE branch out and log at warn before rethrowing. Non-SVE failures still log at error before being wrapped, mirroring dac9c83bd's waitpointCompletionPacket.server.ts pattern so visibility survives the SDK-level SVE filter. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../services/createBackgroundWorker.server.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts index c8381327249..8f49dc34aad 100644 --- a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts @@ -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"); } From c01a47d765a0510d6d9a3628313d12e56c7b428f Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 1 May 2026 17:28:11 +0100 Subject: [PATCH 09/10] chore(webapp): downgrade SVE schedule-sync log in createDeploymentBackgroundWorkerV4 to warn Mirrors the createBackgroundWorker fix; same wrap-into-SVE shape, same split: warn + rethrow for SVE, error + wrap-and-throw for unknown. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...eateDeploymentBackgroundWorkerV4.server.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts index cc73a8569d9..16e36b57dff 100644 --- a/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts +++ b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts @@ -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); From 3c06288e6d84b72fa2a2303d0f56ed4c5f4aba62 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 1 May 2026 17:28:32 +0100 Subject: [PATCH 10/10] chore: add server-changes entry for sentry wrapper-bypass fix Co-Authored-By: Claude Opus 4.7 (1M context) --- .server-changes/sentry-wrapper-bypass-fix.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .server-changes/sentry-wrapper-bypass-fix.md diff --git a/.server-changes/sentry-wrapper-bypass-fix.md b/.server-changes/sentry-wrapper-bypass-fix.md new file mode 100644 index 00000000000..b19d90d84c5 --- /dev/null +++ b/.server-changes/sentry-wrapper-bypass-fix.md @@ -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`.