fix(route-trigger): distinguish user vs platform logic function execution errors (#21715)
## 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. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21715?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+1
-1
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+82
@@ -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);
|
||||
});
|
||||
});
|
||||
+7
-1
@@ -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: {
|
||||
|
||||
+5
-2
@@ -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:
|
||||
|
||||
+9
-7
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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.`;
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user