Improve userFriendlyMessage devX (#16815)

Two challenges with error messages
- always provide a useful/meaningful error message for the end user
instead of the generic one. eg: show "Wrong password" and not "An error
occured"
- avoid technical details unless error regards a technical feature. eg:
show "An error occured" and not "Invalid post-hook payload."; but do
show "Invalid issuer URL." as it occurs while configuring SSO

What this PR does
- Make userFriendlyMessage mandatory for widely used
GraphqlQueryRunnerException and CommonQueryRunnerException, so that
developers are forced to ask themselves what the error message should
be, and as it contains very wide error codes (eg: "Bad request") which
should not be mapped to just one default message
- Keep userFriendlyMessage optional for service-specific exceptions (eg:
workflowStepExecutorException), but convert the error code to
userFriendlyMessage mapper to a switch case function with a typecheck
ensuring that all codes are mapped to a message. These default messages
are still overridable where they are thrown.
This commit is contained in:
Marie
2025-12-30 10:08:43 +01:00
committed by GitHub
parent 7522ff6675
commit 19c9f957b1
135 changed files with 1672 additions and 845 deletions
@@ -1,5 +1,6 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
@@ -7,11 +8,15 @@ export enum WorkflowCommonExceptionCode {
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
}
const workflowCommonExceptionUserFriendlyMessages: Record<
WorkflowCommonExceptionCode,
MessageDescriptor
> = {
[WorkflowCommonExceptionCode.OBJECT_METADATA_NOT_FOUND]: msg`Object metadata not found.`,
const getWorkflowCommonExceptionUserFriendlyMessage = (
code: WorkflowCommonExceptionCode,
) => {
switch (code) {
case WorkflowCommonExceptionCode.OBJECT_METADATA_NOT_FOUND:
return msg`Object metadata not found.`;
default:
assertUnreachable(code);
}
};
export class WorkflowCommonException extends CustomException<WorkflowCommonExceptionCode> {
@@ -23,7 +28,7 @@ export class WorkflowCommonException extends CustomException<WorkflowCommonExcep
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
workflowCommonExceptionUserFriendlyMessages[code],
getWorkflowCommonExceptionUserFriendlyMessage(code),
});
}
}
@@ -1,5 +1,6 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
@@ -7,11 +8,15 @@ export enum WorkflowQueryValidationExceptionCode {
FORBIDDEN = 'FORBIDDEN',
}
const workflowQueryValidationExceptionUserFriendlyMessages: Record<
WorkflowQueryValidationExceptionCode,
MessageDescriptor
> = {
[WorkflowQueryValidationExceptionCode.FORBIDDEN]: msg`You do not have permission to perform this workflow action.`,
const getWorkflowQueryValidationExceptionUserFriendlyMessage = (
code: WorkflowQueryValidationExceptionCode,
) => {
switch (code) {
case WorkflowQueryValidationExceptionCode.FORBIDDEN:
return msg`You do not have permission to perform this workflow action.`;
default:
assertUnreachable(code);
}
};
export class WorkflowQueryValidationException extends CustomException<WorkflowQueryValidationExceptionCode> {
@@ -23,7 +28,7 @@ export class WorkflowQueryValidationException extends CustomException<WorkflowQu
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
workflowQueryValidationExceptionUserFriendlyMessages[code],
getWorkflowQueryValidationExceptionUserFriendlyMessage(code),
});
}
}
@@ -1,5 +1,6 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
@@ -8,12 +9,17 @@ export enum WorkflowVersionEdgeExceptionCode {
INVALID_REQUEST = 'INVALID_REQUEST',
}
const workflowVersionEdgeExceptionUserFriendlyMessages: Record<
WorkflowVersionEdgeExceptionCode,
MessageDescriptor
> = {
[WorkflowVersionEdgeExceptionCode.NOT_FOUND]: msg`Workflow edge not found.`,
[WorkflowVersionEdgeExceptionCode.INVALID_REQUEST]: msg`Invalid workflow edge request.`,
const getWorkflowVersionEdgeExceptionUserFriendlyMessage = (
code: WorkflowVersionEdgeExceptionCode,
) => {
switch (code) {
case WorkflowVersionEdgeExceptionCode.NOT_FOUND:
return msg`Workflow edge not found.`;
case WorkflowVersionEdgeExceptionCode.INVALID_REQUEST:
return msg`Invalid workflow edge request.`;
default:
assertUnreachable(code);
}
};
export class WorkflowVersionEdgeException extends CustomException<WorkflowVersionEdgeExceptionCode> {
@@ -25,7 +31,7 @@ export class WorkflowVersionEdgeException extends CustomException<WorkflowVersio
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
workflowVersionEdgeExceptionUserFriendlyMessages[code],
getWorkflowVersionEdgeExceptionUserFriendlyMessage(code),
});
}
}
@@ -1,5 +1,6 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
@@ -10,14 +11,21 @@ export enum WorkflowVersionStepExceptionCode {
AI_AGENT_STEP_FAILURE = 'AI_AGENT_STEP_FAILURE',
}
const workflowVersionStepExceptionUserFriendlyMessages: Record<
WorkflowVersionStepExceptionCode,
MessageDescriptor
> = {
[WorkflowVersionStepExceptionCode.INVALID_REQUEST]: msg`Invalid workflow step request.`,
[WorkflowVersionStepExceptionCode.NOT_FOUND]: msg`Workflow step not found.`,
[WorkflowVersionStepExceptionCode.CODE_STEP_FAILURE]: msg`Code step execution failed.`,
[WorkflowVersionStepExceptionCode.AI_AGENT_STEP_FAILURE]: msg`AI agent step execution failed.`,
const getWorkflowVersionStepExceptionUserFriendlyMessage = (
code: WorkflowVersionStepExceptionCode,
) => {
switch (code) {
case WorkflowVersionStepExceptionCode.INVALID_REQUEST:
return msg`Invalid workflow step request.`;
case WorkflowVersionStepExceptionCode.NOT_FOUND:
return msg`Workflow step not found.`;
case WorkflowVersionStepExceptionCode.CODE_STEP_FAILURE:
return msg`Code step execution failed.`;
case WorkflowVersionStepExceptionCode.AI_AGENT_STEP_FAILURE:
return msg`AI agent step execution failed.`;
default:
assertUnreachable(code);
}
};
export class WorkflowVersionStepException extends CustomException<WorkflowVersionStepExceptionCode> {
@@ -29,7 +37,7 @@ export class WorkflowVersionStepException extends CustomException<WorkflowVersio
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
workflowVersionStepExceptionUserFriendlyMessages[code],
getWorkflowVersionStepExceptionUserFriendlyMessage(code),
});
}
}