Improve sentry filtering and grouping (#12071)
Follow-up on https://github.com/twentyhq/twenty/pull/12007 In this PR - adding a filter on HttpExceptionHandlerService to filter out 4xx errors from driver handling (as we do for graphQL errors: see useGraphQLErrorHandler hook - only filteredIssues are sent to` exceptionHandlerService.captureExceptions()`.) - grouping together more missing metadata issues - attempting to use error codes as issues names in sentry to improve UI; for now it says "Error" all the time
This commit is contained in:
@ -20,18 +20,6 @@ export class AuthGraphqlApiExceptionFilter implements ExceptionFilter {
|
||||
case AuthExceptionCode.INVALID_INPUT:
|
||||
throw new UserInputError(exception.message);
|
||||
case AuthExceptionCode.FORBIDDEN_EXCEPTION:
|
||||
throw new ForbiddenError(exception.message);
|
||||
case AuthExceptionCode.EMAIL_NOT_VERIFIED:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: AuthExceptionCode.EMAIL_NOT_VERIFIED,
|
||||
});
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
case AuthExceptionCode.USER_NOT_FOUND:
|
||||
case AuthExceptionCode.WORKSPACE_NOT_FOUND:
|
||||
throw new AuthenticationError(exception.message);
|
||||
case AuthExceptionCode.INVALID_DATA:
|
||||
case AuthExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
case AuthExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
||||
case AuthExceptionCode.INSUFFICIENT_SCOPES:
|
||||
case AuthExceptionCode.OAUTH_ACCESS_DENIED:
|
||||
case AuthExceptionCode.SSO_AUTH_FAILED:
|
||||
@ -40,6 +28,18 @@ export class AuthGraphqlApiExceptionFilter implements ExceptionFilter {
|
||||
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE:
|
||||
throw new ForbiddenError(exception.message);
|
||||
case AuthExceptionCode.EMAIL_NOT_VERIFIED:
|
||||
case AuthExceptionCode.INVALID_DATA:
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: AuthExceptionCode.EMAIL_NOT_VERIFIED,
|
||||
});
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
case AuthExceptionCode.USER_NOT_FOUND:
|
||||
case AuthExceptionCode.WORKSPACE_NOT_FOUND:
|
||||
throw new AuthenticationError(exception.message);
|
||||
case AuthExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
case AuthExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
||||
throw exception;
|
||||
default: {
|
||||
const _exhaustiveCheck: never = exception.code;
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
|
||||
export const getAuthExceptionRestStatus = (exception: AuthException) => {
|
||||
switch (exception.code) {
|
||||
case AuthExceptionCode.CLIENT_NOT_FOUND:
|
||||
return 404;
|
||||
case AuthExceptionCode.INVALID_INPUT:
|
||||
return 400;
|
||||
case AuthExceptionCode.FORBIDDEN_EXCEPTION:
|
||||
case AuthExceptionCode.INSUFFICIENT_SCOPES:
|
||||
case AuthExceptionCode.OAUTH_ACCESS_DENIED:
|
||||
case AuthExceptionCode.SSO_AUTH_FAILED:
|
||||
case AuthExceptionCode.USE_SSO_AUTH:
|
||||
case AuthExceptionCode.SIGNUP_DISABLED:
|
||||
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
||||
case AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE:
|
||||
case AuthExceptionCode.EMAIL_NOT_VERIFIED:
|
||||
return 403;
|
||||
case AuthExceptionCode.INVALID_DATA:
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
case AuthExceptionCode.USER_NOT_FOUND:
|
||||
case AuthExceptionCode.WORKSPACE_NOT_FOUND:
|
||||
return 401;
|
||||
case AuthExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
case AuthExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
||||
return 500;
|
||||
default: {
|
||||
const _exhaustiveCheck: never = exception.code;
|
||||
|
||||
return 500;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -24,8 +24,8 @@ import { CloudflareSecretMatchGuard } from 'src/engine/core-modules/domain-manag
|
||||
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { handleException } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { handleException } from 'src/engine/utils/global-exception-handler.util';
|
||||
|
||||
@Controller()
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -43,13 +43,13 @@ export class CloudflareController {
|
||||
@UseGuards(CloudflareSecretMatchGuard)
|
||||
async customHostnameWebhooks(@Req() req: Request, @Res() res: Response) {
|
||||
if (!req.body?.data?.data?.hostname) {
|
||||
handleException(
|
||||
new DomainManagerException(
|
||||
handleException({
|
||||
exception: new DomainManagerException(
|
||||
'Hostname missing',
|
||||
DomainManagerExceptionCode.INVALID_INPUT_DATA,
|
||||
),
|
||||
this.exceptionHandlerService,
|
||||
);
|
||||
exceptionHandlerService: this.exceptionHandlerService,
|
||||
});
|
||||
|
||||
return res.status(200).send();
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ export class ExceptionHandlerSentryDriver
|
||||
Sentry.withScope((scope) => {
|
||||
if (options?.operation) {
|
||||
scope.setExtra('operation', options.operation.name);
|
||||
scope.setExtra('operationType', options.operation.type);
|
||||
}
|
||||
|
||||
if (options?.document) {
|
||||
@ -57,6 +58,13 @@ export class ExceptionHandlerSentryDriver
|
||||
if (exception instanceof CustomException) {
|
||||
scope.setTag('customExceptionCode', exception.code);
|
||||
scope.setFingerprint([exception.code]);
|
||||
exception.name = exception.code
|
||||
.split('_')
|
||||
.map(
|
||||
(word) =>
|
||||
word.charAt(0)?.toUpperCase() + word.slice(1)?.toLowerCase(),
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
const eventId = Sentry.captureException(exception, {
|
||||
|
||||
@ -7,19 +7,9 @@ import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/
|
||||
import { ExceptionHandlerWorkspace } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-workspace.interface';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { handleException } from 'src/engine/utils/global-exception-handler.util';
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export const handleException = (
|
||||
exception: CustomException,
|
||||
exceptionHandlerService: ExceptionHandlerService,
|
||||
user?: ExceptionHandlerUser,
|
||||
workspace?: ExceptionHandlerWorkspace,
|
||||
): CustomException => {
|
||||
exceptionHandlerService.captureExceptions([exception], { user, workspace });
|
||||
|
||||
return exception;
|
||||
};
|
||||
|
||||
interface RequestAndParams {
|
||||
request: Request | null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -51,9 +41,16 @@ export class HttpExceptionHandlerService {
|
||||
workspace = { ...workspace, id: params.workspaceId };
|
||||
if (params?.userId) user = { ...user, id: params.userId };
|
||||
|
||||
handleException(exception, this.exceptionHandlerService, user, workspace);
|
||||
const statusCode = errorCode || 500;
|
||||
|
||||
handleException({
|
||||
exception,
|
||||
exceptionHandlerService: this.exceptionHandlerService,
|
||||
user,
|
||||
workspace,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
return response.status(statusCode).send({
|
||||
statusCode,
|
||||
error: exception.name || 'Bad Request',
|
||||
|
||||
@ -11,7 +11,7 @@ import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { generateGraphQLErrorFromError } from 'src/engine/core-modules/graphql/utils/generate-graphql-error-from-error.util';
|
||||
import { BaseGraphQLError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { shouldCaptureException } from 'src/engine/core-modules/graphql/utils/should-capture-exception.util';
|
||||
import { shouldCaptureException } from 'src/engine/utils/global-exception-handler.util';
|
||||
|
||||
const DEFAULT_EVENT_ID_KEY = 'exceptionEventId';
|
||||
const SCHEMA_VERSION_HEADER = 'x-schema-version';
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import { HttpException } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
BaseGraphQLError,
|
||||
ErrorCode,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
export const graphQLErrorCodesToFilterOut = [
|
||||
ErrorCode.GRAPHQL_VALIDATION_FAILED,
|
||||
ErrorCode.UNAUTHENTICATED,
|
||||
ErrorCode.FORBIDDEN,
|
||||
ErrorCode.NOT_FOUND,
|
||||
ErrorCode.METHOD_NOT_ALLOWED,
|
||||
ErrorCode.TIMEOUT,
|
||||
ErrorCode.CONFLICT,
|
||||
ErrorCode.BAD_USER_INPUT,
|
||||
];
|
||||
|
||||
export const shouldCaptureException = (exception: Error): boolean => {
|
||||
if (
|
||||
exception instanceof BaseGraphQLError &&
|
||||
graphQLErrorCodesToFilterOut.includes(exception?.extensions?.code)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
exception instanceof HttpException &&
|
||||
exception.getStatus() >= 400 &&
|
||||
exception.getStatus() < 500
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@ -5,21 +5,21 @@ import {
|
||||
ModuleRef,
|
||||
createContextId,
|
||||
} from '@nestjs/core';
|
||||
import { Module } from '@nestjs/core/injector/module';
|
||||
import { Injector } from '@nestjs/core/injector/injector';
|
||||
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
|
||||
import { Module } from '@nestjs/core/injector/module';
|
||||
|
||||
import { MessageQueueWorkerOptions } from 'src/engine/core-modules/message-queue/interfaces/message-queue-worker-options.interface';
|
||||
import {
|
||||
MessageQueueJob,
|
||||
MessageQueueJobData,
|
||||
} from 'src/engine/core-modules/message-queue/interfaces/message-queue-job.interface';
|
||||
import { MessageQueueWorkerOptions } from 'src/engine/core-modules/message-queue/interfaces/message-queue-worker-options.interface';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { MessageQueueMetadataAccessor } from 'src/engine/core-modules/message-queue/message-queue-metadata.accessor';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { shouldFilterException } from 'src/engine/utils/global-exception-handler.util';
|
||||
import { shouldCaptureException } from 'src/engine/utils/global-exception-handler.util';
|
||||
|
||||
interface ProcessorGroup {
|
||||
instance: object;
|
||||
@ -207,7 +207,7 @@ export class MessageQueueExplorer implements OnModuleInit {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
await instance[processMethodName].call(instance, job.data);
|
||||
} catch (err) {
|
||||
if (!shouldFilterException(err)) {
|
||||
if (shouldCaptureException(err)) {
|
||||
this.exceptionHandlerService.captureExceptions([err]);
|
||||
}
|
||||
throw err;
|
||||
|
||||
Reference in New Issue
Block a user