diff --git a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts index f4a4353a6d8..361322ca2bb 100644 --- a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts +++ b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts @@ -156,6 +156,64 @@ export class ApolloFactory implements ApolloManager { ).flatMap(() => forward(operation)); }; + const sendToSentry = ({ + graphQLError, + operation, + }: { + graphQLError: GraphQLFormattedError; + operation: Operation; + }) => { + if (isDebugMode === true) { + logDebug( + `[GraphQL error]: Message: ${graphQLError.message}, Location: ${ + graphQLError.locations + ? JSON.stringify(graphQLError.locations) + : graphQLError.locations + }, Path: ${graphQLError.path}`, + ); + } + import('@sentry/react') + .then(({ captureException, withScope }) => { + withScope((scope) => { + const error = new Error(graphQLError.message); + + error.name = graphQLError.message; + + const fingerPrint: string[] = []; + if (isDefined(graphQLError.extensions)) { + scope.setExtra('extensions', graphQLError.extensions); + if (isDefined(graphQLError.extensions.subCode)) { + fingerPrint.push(graphQLError.extensions.subCode as string); + } + } + + if (isDefined(operation.operationName)) { + scope.setExtra('operation', operation.operationName); + const genericOperationName = getGenericOperationName( + operation.operationName, + ); + + if (isDefined(genericOperationName)) { + fingerPrint.push(genericOperationName); + } + } + + if (!isEmpty(fingerPrint)) { + scope.setFingerprint(fingerPrint); + } + + captureException(error); // Sentry expects a JS error + }); + }) + .catch((sentryError) => { + // eslint-disable-next-line no-console + console.error( + 'Failed to capture GraphQL error with Sentry:', + sentryError, + ); + }); + }; + const errorLink = onError( ({ graphQLErrors, networkError, forward, operation }) => { if (isDefined(graphQLErrors)) { @@ -179,63 +237,18 @@ export class ApolloFactory implements ApolloManager { case 'FORBIDDEN': { return; } + case 'USER_INPUT_ERROR': { + if (graphQLError.extensions?.isExpected === true) { + return; + } + sendToSentry({ graphQLError, operation }); + return; + } case 'INTERNAL_SERVER_ERROR': { return; // already caught in BE } default: - if (isDebugMode === true) { - logDebug( - `[GraphQL error]: Message: ${ - graphQLError.message - }, Location: ${ - graphQLError.locations - ? JSON.stringify(graphQLError.locations) - : graphQLError.locations - }, Path: ${graphQLError.path}`, - ); - } - import('@sentry/react') - .then(({ captureException, withScope }) => { - withScope((scope) => { - const error = new Error(graphQLError.message); - - error.name = graphQLError.message; - - const fingerPrint: string[] = []; - if (isDefined(graphQLError.extensions)) { - scope.setExtra('extensions', graphQLError.extensions); - if (isDefined(graphQLError.extensions.subCode)) { - fingerPrint.push( - graphQLError.extensions.subCode as string, - ); - } - } - - if (isDefined(operation.operationName)) { - scope.setExtra('operation', operation.operationName); - const genericOperationName = getGenericOperationName( - operation.operationName, - ); - - if (isDefined(genericOperationName)) { - fingerPrint.push(genericOperationName); - } - } - - if (!isEmpty(fingerPrint)) { - scope.setFingerprint(fingerPrint); - } - - captureException(error); // Sentry expects a JS error - }); - }) - .catch((sentryError) => { - // eslint-disable-next-line no-console - console.error( - 'Failed to capture GraphQL error with Sentry:', - sentryError, - ); - }); + sendToSentry({ graphQLError, operation }); } } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts index 59f504f497c..98cc842bc38 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts @@ -54,6 +54,7 @@ export const handleDuplicateKeyError = ( `Duplicate ${columnNames} ${duplicatedValues ? `with value ${duplicatedValues}` : ''}. Please set a unique one.`, { userFriendlyMessage: `This ${columnNames.toLowerCase()} ${duplicatedValues ? `with value ${duplicatedValues}` : ''} is already taken. Please choose a different value.`, + isExpected: true, }, ); } diff --git a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts index 5ca324b187e..20ccbac8690 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts @@ -173,11 +173,14 @@ export class ForbiddenError extends BaseGraphQLError { export class UserInputError extends BaseGraphQLError { constructor(exception: CustomException); - constructor(message: string, extensions?: RestrictedGraphQLErrorExtensions); + constructor( + message: string, + extensions?: RestrictedGraphQLErrorExtensions & { isExpected?: boolean }, + ); constructor( messageOrException: string | CustomException, - extensions?: RestrictedGraphQLErrorExtensions, + extensions?: RestrictedGraphQLErrorExtensions & { isExpected?: boolean }, ) { super(messageOrException, ErrorCode.BAD_USER_INPUT, extensions); Object.defineProperty(this, 'name', { value: 'UserInputError' });