From 34362de7b769d83f6f8c4a017b05e71d32dfe61a Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Wed, 17 Jun 2026 12:45:04 +0200 Subject: [PATCH] fix(route-trigger): distinguish user vs platform logic function execution errors (#21715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Splits route trigger logic-function failures into two cases instead of one catch-all: - **User error** — the function's own code threw an uncaught error. Returns `500` and is **not** sent to Sentry. - **Platform error** — an infrastructure/execution failure on our side. Returns `500` and **is** sent to Sentry. A disabled logic function now returns `403`. ## Why User-code failures were flooding Sentry: a single workspace's function hitting a transient upstream error generated tens of thousands of events. #21656 stopped the flood by muting the entire route-trigger execution error bucket — but muting everything also silenced genuine platform failures we *do* want to be alerted on. Splitting the bucket keeps the user-code noise out of Sentry (the original goal) while making sure real platform errors still surface. Users who want to return a specific status/body when their function fails can still catch the error and return a `Response` — that path is unchanged. Review in cubic --- .../drivers/lambda.driver.ts | 2 +- ...-trigger-rest-api-exception-filter.spec.ts | 82 +++++++++++++++++++ ...route-trigger-rest-api-exception-filter.ts | 8 +- .../exceptions/route-trigger.exception.ts | 7 +- .../triggers/route/route-trigger.service.ts | 16 ++-- .../logic-function.exception.ts | 4 +- ...ion-graphql-api-exception-handler.utils.ts | 2 +- 7 files changed, 107 insertions(+), 14 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/__tests__/route-trigger-rest-api-exception-filter.spec.ts diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver.ts index 67976ca211..d676cfc33f 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver.ts @@ -247,7 +247,7 @@ export class LambdaDriver implements LogicFunctionDriver { throw new LogicFunctionException( `Lambda invocation failed for function '${flatLogicFunction.id}' during ${currentPhase}: ${error instanceof Error ? error.message : 'Unknown error'}`, - LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_FAILED, + LogicFunctionExceptionCode.LOGIC_FUNCTION_PLATFORM_EXECUTION_ERROR, ); } } diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/__tests__/route-trigger-rest-api-exception-filter.spec.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/__tests__/route-trigger-rest-api-exception-filter.spec.ts new file mode 100644 index 0000000000..9b1ea409a7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/__tests__/route-trigger-rest-api-exception-filter.spec.ts @@ -0,0 +1,82 @@ +import { type ArgumentsHost } from '@nestjs/common'; + +import { type Response } from 'express'; + +import { type HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service'; +import { RouteTriggerRestApiExceptionFilter } from 'src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger-rest-api-exception-filter'; +import { + RouteTriggerException, + RouteTriggerExceptionCode, +} from 'src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception'; + +describe('RouteTriggerRestApiExceptionFilter', () => { + const handleError = jest.fn(); + const response = {} as Response; + + const httpExceptionHandlerService = { + handleError, + } as unknown as HttpExceptionHandlerService; + + const filter = new RouteTriggerRestApiExceptionFilter( + httpExceptionHandlerService, + ); + + const host = { + switchToHttp: () => ({ getResponse: () => response }), + } as unknown as ArgumentsHost; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns 500 and does NOT capture user-uncaught errors in Sentry', () => { + const exception = new RouteTriggerException( + 'boom', + RouteTriggerExceptionCode.ROUTE_TRIGGER_USER_UNCAUGHT_ERROR, + ); + + filter.catch(exception, host); + + expect(handleError).toHaveBeenCalledWith( + exception, + response, + 500, + undefined, + undefined, + { shouldBeCapturedBySentry: false }, + ); + }); + + it('returns 500 and captures platform errors in Sentry', () => { + const exception = new RouteTriggerException( + 'boom', + RouteTriggerExceptionCode.ROUTE_TRIGGER_PLATFORM_ERROR, + ); + + filter.catch(exception, host); + + expect(handleError).toHaveBeenCalledWith(exception, response, 500); + }); + + it('maps a disabled function to 403', () => { + const exception = new RouteTriggerException( + 'disabled', + RouteTriggerExceptionCode.FORBIDDEN_EXCEPTION, + ); + + filter.catch(exception, host); + + expect(handleError).toHaveBeenCalledWith(exception, response, 403); + }); + + it('maps not-found codes to 404', () => { + const exception = new RouteTriggerException( + 'missing', + RouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND, + ); + + filter.catch(exception, host); + + expect(handleError).toHaveBeenCalledWith(exception, response, 404); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger-rest-api-exception-filter.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger-rest-api-exception-filter.ts index d7c58ee392..d66b0a4cee 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger-rest-api-exception-filter.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger-rest-api-exception-filter.ts @@ -45,7 +45,7 @@ export class RouteTriggerRestApiExceptionFilter implements ExceptionFilter { response, 429, ); - case RouteTriggerExceptionCode.LOGIC_FUNCTION_EXECUTION_ERROR: + case RouteTriggerExceptionCode.ROUTE_TRIGGER_USER_UNCAUGHT_ERROR: return this.httpExceptionHandlerService.handleError( exception as CustomException, response, @@ -54,6 +54,12 @@ export class RouteTriggerRestApiExceptionFilter implements ExceptionFilter { undefined, { shouldBeCapturedBySentry: false }, ); + case RouteTriggerExceptionCode.ROUTE_TRIGGER_PLATFORM_ERROR: + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + 500, + ); case RouteTriggerExceptionCode.ROUTE_ALREADY_EXIST: case RouteTriggerExceptionCode.ROUTE_PATH_ALREADY_EXIST: default: { diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts index b08ca90599..e4d6709662 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/exceptions/route-trigger.exception.ts @@ -12,7 +12,8 @@ export enum RouteTriggerExceptionCode { ROUTE_ALREADY_EXIST = 'ROUTE_ALREADY_EXIST', ROUTE_PATH_ALREADY_EXIST = 'ROUTE_PATH_ALREADY_EXIST', FORBIDDEN_EXCEPTION = 'FORBIDDEN_EXCEPTION', - LOGIC_FUNCTION_EXECUTION_ERROR = 'LOGIC_FUNCTION_EXECUTION_ERROR', + ROUTE_TRIGGER_USER_UNCAUGHT_ERROR = 'ROUTE_TRIGGER_USER_UNCAUGHT_ERROR', + ROUTE_TRIGGER_PLATFORM_ERROR = 'ROUTE_TRIGGER_PLATFORM_ERROR', RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', } @@ -34,8 +35,10 @@ const getRouteTriggerExceptionUserFriendlyMessage = ( return msg`Route path already exists.`; case RouteTriggerExceptionCode.FORBIDDEN_EXCEPTION: return msg`You do not have permission to perform this action.`; - case RouteTriggerExceptionCode.LOGIC_FUNCTION_EXECUTION_ERROR: + case RouteTriggerExceptionCode.ROUTE_TRIGGER_USER_UNCAUGHT_ERROR: return msg`Logic function execution failed.`; + case RouteTriggerExceptionCode.ROUTE_TRIGGER_PLATFORM_ERROR: + return msg`An unexpected error occurred while executing the logic function.`; case RouteTriggerExceptionCode.RATE_LIMIT_EXCEEDED: return msg`Too many requests. Please try again later.`; default: diff --git a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts index 49bd97b27f..af7b5a8e45 100644 --- a/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts +++ b/packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/route/route-trigger.service.ts @@ -163,14 +163,16 @@ export class RouteTriggerService { } } - if ( - error instanceof LogicFunctionException && - error.code === LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND - ) { - return RouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND; + if (error instanceof LogicFunctionException) { + switch (error.code) { + case LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND: + return RouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND; + case LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED: + return RouteTriggerExceptionCode.FORBIDDEN_EXCEPTION; + } } - return RouteTriggerExceptionCode.LOGIC_FUNCTION_EXECUTION_ERROR; + return RouteTriggerExceptionCode.ROUTE_TRIGGER_PLATFORM_ERROR; } async handle({ @@ -249,7 +251,7 @@ export class RouteTriggerService { if (result.error) { throw new RouteTriggerException( result.error.errorMessage, - RouteTriggerExceptionCode.LOGIC_FUNCTION_EXECUTION_ERROR, + RouteTriggerExceptionCode.ROUTE_TRIGGER_USER_UNCAUGHT_ERROR, ); } diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.exception.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.exception.ts index 11ac2ca633..8c8ab0e61d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.exception.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/logic-function.exception.ts @@ -14,7 +14,7 @@ export enum LogicFunctionExceptionCode { LOGIC_FUNCTION_CREATE_FAILED = 'LOGIC_FUNCTION_CREATE_FAILED', LOGIC_FUNCTION_COMPILATION_FAILED = 'LOGIC_FUNCTION_COMPILATION_FAILED', LOGIC_FUNCTION_EXECUTION_TIMEOUT = 'LOGIC_FUNCTION_EXECUTION_TIMEOUT', - LOGIC_FUNCTION_EXECUTION_FAILED = 'LOGIC_FUNCTION_EXECUTION_FAILED', + LOGIC_FUNCTION_PLATFORM_EXECUTION_ERROR = 'LOGIC_FUNCTION_PLATFORM_EXECUTION_ERROR', LOGIC_FUNCTION_LAYER_BUILD_FAILED = 'LOGIC_FUNCTION_LAYER_BUILD_FAILED', LOGIC_FUNCTION_DISABLED = 'LOGIC_FUNCTION_DISABLED', LOGIC_FUNCTION_INVALID_SEED_PROJECT = 'LOGIC_FUNCTION_INVALID_SEED_PROJECT', @@ -44,7 +44,7 @@ const getLogicFunctionExceptionUserFriendlyMessage = ( return msg`Function code failed to compile.`; case LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_TIMEOUT: return msg`Function execution timed out.`; - case LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_FAILED: + case LogicFunctionExceptionCode.LOGIC_FUNCTION_PLATFORM_EXECUTION_ERROR: return msg`Function execution failed.`; case LogicFunctionExceptionCode.LOGIC_FUNCTION_LAYER_BUILD_FAILED: return msg`Failed to build function dependencies.`; diff --git a/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils.ts b/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils.ts index 22d15f549c..9318d69161 100644 --- a/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils.ts @@ -29,7 +29,7 @@ export const logicFunctionGraphQLApiExceptionHandler = (error: any) => { case LogicFunctionExceptionCode.LOGIC_FUNCTION_CODE_UNCHANGED: case LogicFunctionExceptionCode.LOGIC_FUNCTION_CREATE_FAILED: case LogicFunctionExceptionCode.LOGIC_FUNCTION_INVALID_SEED_PROJECT: - case LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_FAILED: + case LogicFunctionExceptionCode.LOGIC_FUNCTION_PLATFORM_EXECUTION_ERROR: case LogicFunctionExceptionCode.LOGIC_FUNCTION_LAYER_BUILD_FAILED: throw error; case LogicFunctionExceptionCode.LOGIC_FUNCTION_COMPILATION_FAILED: