Enable filtering out of expected 400 errors (#14371)
We decided to send 4xx errors to sentry from the FE, except for ForbiddenErrors. In some cases, we also need not to send to sentry some 4xx errors, like user input error, because the error stemming from the user input is unpredictable (ex: duplicate entry for a record with unicity constraints) and thus acceptable. Let's add a isExpected extension on UserInputErrors to introduce a specific behaviour for them (do not send to sentry).
This commit is contained in:
@@ -156,6 +156,64 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
).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<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
|
||||
Reference in New Issue
Block a user