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:
@ -2,7 +2,7 @@ import { CommandFactory } from 'nest-commander';
|
|||||||
|
|
||||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||||
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
|
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
|
||||||
import { shouldFilterException } from 'src/engine/utils/global-exception-handler.util';
|
import { shouldCaptureException } from 'src/engine/utils/global-exception-handler.util';
|
||||||
|
|
||||||
import { CommandModule } from './command.module';
|
import { CommandModule } from './command.module';
|
||||||
|
|
||||||
@ -10,11 +10,9 @@ async function bootstrap() {
|
|||||||
const errorHandler = (err: Error) => {
|
const errorHandler = (err: Error) => {
|
||||||
loggerService.error(err?.message, err?.name);
|
loggerService.error(err?.message, err?.name);
|
||||||
|
|
||||||
if (shouldFilterException(err)) {
|
if (shouldCaptureException(err)) {
|
||||||
return;
|
exceptionHandlerService.captureExceptions([err]);
|
||||||
}
|
}
|
||||||
|
|
||||||
exceptionHandlerService.captureExceptions([err]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const app = await CommandFactory.createWithoutRunning(CommandModule, {
|
const app = await CommandFactory.createWithoutRunning(CommandModule, {
|
||||||
|
|||||||
@ -22,6 +22,5 @@ export enum GraphqlQueryRunnerExceptionCode {
|
|||||||
RELATION_SETTINGS_NOT_FOUND = 'RELATION_SETTINGS_NOT_FOUND',
|
RELATION_SETTINGS_NOT_FOUND = 'RELATION_SETTINGS_NOT_FOUND',
|
||||||
RELATION_TARGET_OBJECT_METADATA_NOT_FOUND = 'RELATION_TARGET_OBJECT_METADATA_NOT_FOUND',
|
RELATION_TARGET_OBJECT_METADATA_NOT_FOUND = 'RELATION_TARGET_OBJECT_METADATA_NOT_FOUND',
|
||||||
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
|
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
|
||||||
OBJECT_METADATA_COLLECTION_NOT_FOUND = 'OBJECT_METADATA_COLLECTION_NOT_FOUND',
|
|
||||||
INVALID_POST_HOOK_PAYLOAD = 'INVALID_POST_HOOK_PAYLOAD',
|
INVALID_POST_HOOK_PAYLOAD = 'INVALID_POST_HOOK_PAYLOAD',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,6 @@ export const graphqlQueryRunnerExceptionHandler = (
|
|||||||
throw new NotFoundError(error.message);
|
throw new NotFoundError(error.message);
|
||||||
case GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND:
|
case GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND:
|
||||||
case GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND:
|
case GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND:
|
||||||
case GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_COLLECTION_NOT_FOUND:
|
|
||||||
case GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD:
|
case GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD:
|
||||||
throw error;
|
throw error;
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@ -5,10 +5,6 @@ import { GraphQLSchema, printSchema } from 'graphql';
|
|||||||
import { gql } from 'graphql-tag';
|
import { gql } from 'graphql-tag';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import {
|
|
||||||
GraphqlQueryRunnerException,
|
|
||||||
GraphqlQueryRunnerExceptionCode,
|
|
||||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
|
||||||
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
|
import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-explorer.service';
|
||||||
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
|
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
|
||||||
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
|
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
|
||||||
@ -18,6 +14,10 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service
|
|||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||||
|
import {
|
||||||
|
WorkspaceMetadataCacheException,
|
||||||
|
WorkspaceMetadataCacheExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception';
|
||||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||||
import {
|
import {
|
||||||
WorkspaceMetadataVersionException,
|
WorkspaceMetadataVersionException,
|
||||||
@ -86,9 +86,9 @@ export class WorkspaceSchemaFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!objectMetadataMaps) {
|
if (!objectMetadataMaps) {
|
||||||
throw new GraphqlQueryRunnerException(
|
throw new WorkspaceMetadataCacheException(
|
||||||
'Object metadata collection not found',
|
'Object metadata collection not found',
|
||||||
GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_COLLECTION_NOT_FOUND,
|
WorkspaceMetadataCacheExceptionCode.OBJECT_METADATA_COLLECTION_NOT_FOUND,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,18 +20,6 @@ export class AuthGraphqlApiExceptionFilter implements ExceptionFilter {
|
|||||||
case AuthExceptionCode.INVALID_INPUT:
|
case AuthExceptionCode.INVALID_INPUT:
|
||||||
throw new UserInputError(exception.message);
|
throw new UserInputError(exception.message);
|
||||||
case AuthExceptionCode.FORBIDDEN_EXCEPTION:
|
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.INSUFFICIENT_SCOPES:
|
||||||
case AuthExceptionCode.OAUTH_ACCESS_DENIED:
|
case AuthExceptionCode.OAUTH_ACCESS_DENIED:
|
||||||
case AuthExceptionCode.SSO_AUTH_FAILED:
|
case AuthExceptionCode.SSO_AUTH_FAILED:
|
||||||
@ -40,6 +28,18 @@ export class AuthGraphqlApiExceptionFilter implements ExceptionFilter {
|
|||||||
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
case AuthExceptionCode.GOOGLE_API_AUTH_DISABLED:
|
||||||
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
case AuthExceptionCode.MICROSOFT_API_AUTH_DISABLED:
|
||||||
case AuthExceptionCode.MISSING_ENVIRONMENT_VARIABLE:
|
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;
|
throw exception;
|
||||||
default: {
|
default: {
|
||||||
const _exhaustiveCheck: never = exception.code;
|
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 { 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 { 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 { 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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { handleException } from 'src/engine/utils/global-exception-handler.util';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
@UseFilters(AuthRestApiExceptionFilter)
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
@ -43,13 +43,13 @@ export class CloudflareController {
|
|||||||
@UseGuards(CloudflareSecretMatchGuard)
|
@UseGuards(CloudflareSecretMatchGuard)
|
||||||
async customHostnameWebhooks(@Req() req: Request, @Res() res: Response) {
|
async customHostnameWebhooks(@Req() req: Request, @Res() res: Response) {
|
||||||
if (!req.body?.data?.data?.hostname) {
|
if (!req.body?.data?.data?.hostname) {
|
||||||
handleException(
|
handleException({
|
||||||
new DomainManagerException(
|
exception: new DomainManagerException(
|
||||||
'Hostname missing',
|
'Hostname missing',
|
||||||
DomainManagerExceptionCode.INVALID_INPUT_DATA,
|
DomainManagerExceptionCode.INVALID_INPUT_DATA,
|
||||||
),
|
),
|
||||||
this.exceptionHandlerService,
|
exceptionHandlerService: this.exceptionHandlerService,
|
||||||
);
|
});
|
||||||
|
|
||||||
return res.status(200).send();
|
return res.status(200).send();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export class ExceptionHandlerSentryDriver
|
|||||||
Sentry.withScope((scope) => {
|
Sentry.withScope((scope) => {
|
||||||
if (options?.operation) {
|
if (options?.operation) {
|
||||||
scope.setExtra('operation', options.operation.name);
|
scope.setExtra('operation', options.operation.name);
|
||||||
|
scope.setExtra('operationType', options.operation.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.document) {
|
if (options?.document) {
|
||||||
@ -57,6 +58,13 @@ export class ExceptionHandlerSentryDriver
|
|||||||
if (exception instanceof CustomException) {
|
if (exception instanceof CustomException) {
|
||||||
scope.setTag('customExceptionCode', exception.code);
|
scope.setTag('customExceptionCode', exception.code);
|
||||||
scope.setFingerprint([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, {
|
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 { 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 { 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';
|
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 {
|
interface RequestAndParams {
|
||||||
request: Request | null;
|
request: Request | null;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -51,9 +41,16 @@ export class HttpExceptionHandlerService {
|
|||||||
workspace = { ...workspace, id: params.workspaceId };
|
workspace = { ...workspace, id: params.workspaceId };
|
||||||
if (params?.userId) user = { ...user, id: params.userId };
|
if (params?.userId) user = { ...user, id: params.userId };
|
||||||
|
|
||||||
handleException(exception, this.exceptionHandlerService, user, workspace);
|
|
||||||
const statusCode = errorCode || 500;
|
const statusCode = errorCode || 500;
|
||||||
|
|
||||||
|
handleException({
|
||||||
|
exception,
|
||||||
|
exceptionHandlerService: this.exceptionHandlerService,
|
||||||
|
user,
|
||||||
|
workspace,
|
||||||
|
statusCode,
|
||||||
|
});
|
||||||
|
|
||||||
return response.status(statusCode).send({
|
return response.status(statusCode).send({
|
||||||
statusCode,
|
statusCode,
|
||||||
error: exception.name || 'Bad Request',
|
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 { 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 { 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 { 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 DEFAULT_EVENT_ID_KEY = 'exceptionEventId';
|
||||||
const SCHEMA_VERSION_HEADER = 'x-schema-version';
|
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,
|
ModuleRef,
|
||||||
createContextId,
|
createContextId,
|
||||||
} from '@nestjs/core';
|
} from '@nestjs/core';
|
||||||
import { Module } from '@nestjs/core/injector/module';
|
|
||||||
import { Injector } from '@nestjs/core/injector/injector';
|
import { Injector } from '@nestjs/core/injector/injector';
|
||||||
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
|
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 {
|
import {
|
||||||
MessageQueueJob,
|
MessageQueueJob,
|
||||||
MessageQueueJobData,
|
MessageQueueJobData,
|
||||||
} from 'src/engine/core-modules/message-queue/interfaces/message-queue-job.interface';
|
} 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 { 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 { 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 { 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 { shouldCaptureException } from 'src/engine/utils/global-exception-handler.util';
|
||||||
import { shouldFilterException } from 'src/engine/utils/global-exception-handler.util';
|
|
||||||
|
|
||||||
interface ProcessorGroup {
|
interface ProcessorGroup {
|
||||||
instance: object;
|
instance: object;
|
||||||
@ -207,7 +207,7 @@ export class MessageQueueExplorer implements OnModuleInit {
|
|||||||
// @ts-expect-error legacy noImplicitAny
|
// @ts-expect-error legacy noImplicitAny
|
||||||
await instance[processMethodName].call(instance, job.data);
|
await instance[processMethodName].call(instance, job.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!shouldFilterException(err)) {
|
if (shouldCaptureException(err)) {
|
||||||
this.exceptionHandlerService.captureExceptions([err]);
|
this.exceptionHandlerService.captureExceptions([err]);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@ -8,4 +8,5 @@ export class WorkspaceMetadataCacheException extends CustomException {
|
|||||||
|
|
||||||
export enum WorkspaceMetadataCacheExceptionCode {
|
export enum WorkspaceMetadataCacheExceptionCode {
|
||||||
OBJECT_METADATA_MAP_NOT_FOUND = 'Object Metadata map not found',
|
OBJECT_METADATA_MAP_NOT_FOUND = 'Object Metadata map not found',
|
||||||
|
OBJECT_METADATA_COLLECTION_NOT_FOUND = 'Object Metadata collection not found',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { AuthExceptionCode } from 'src/engine/core-modules/auth/auth.exception';
|
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
import { getAuthExceptionRestStatus } from 'src/engine/core-modules/auth/utils/get-auth-exception-rest-status.util';
|
||||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||||
@ -53,11 +54,15 @@ export class MiddlewareService {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public writeRestResponseOnExceptionCaught(res: Response, error: any) {
|
public writeRestResponseOnExceptionCaught(res: Response, error: any) {
|
||||||
// capture and handle custom exceptions
|
|
||||||
handleException(error as CustomException, this.exceptionHandlerService);
|
|
||||||
|
|
||||||
const statusCode = this.getStatus(error);
|
const statusCode = this.getStatus(error);
|
||||||
|
|
||||||
|
// capture and handle custom exceptions
|
||||||
|
handleException({
|
||||||
|
exception: error as CustomException,
|
||||||
|
exceptionHandlerService: this.exceptionHandlerService,
|
||||||
|
statusCode,
|
||||||
|
});
|
||||||
|
|
||||||
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
||||||
res.write(
|
res.write(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@ -158,13 +163,8 @@ export class MiddlewareService {
|
|||||||
return error.status;
|
return error.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof CustomException) {
|
if (error instanceof AuthException) {
|
||||||
switch (error.code) {
|
return getAuthExceptionRestStatus(error);
|
||||||
case AuthExceptionCode.UNAUTHENTICATED:
|
|
||||||
return 401;
|
|
||||||
default:
|
|
||||||
return 400;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 500;
|
return 500;
|
||||||
|
|||||||
@ -7,9 +7,7 @@ export class TwentyORMException extends CustomException {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum TwentyORMExceptionCode {
|
export enum TwentyORMExceptionCode {
|
||||||
METADATA_VERSION_NOT_FOUND = 'METADATA_VERSION_NOT_FOUND',
|
|
||||||
METADATA_VERSION_MISMATCH = 'METADATA_VERSION_MISMATCH',
|
METADATA_VERSION_MISMATCH = 'METADATA_VERSION_MISMATCH',
|
||||||
METADATA_COLLECTION_NOT_FOUND = 'METADATA_COLLECTION_NOT_FOUND',
|
|
||||||
WORKSPACE_SCHEMA_NOT_FOUND = 'WORKSPACE_SCHEMA_NOT_FOUND',
|
WORKSPACE_SCHEMA_NOT_FOUND = 'WORKSPACE_SCHEMA_NOT_FOUND',
|
||||||
ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_VERSION_NOT_FOUND',
|
ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_VERSION_NOT_FOUND',
|
||||||
FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND',
|
FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND',
|
||||||
|
|||||||
@ -10,6 +10,10 @@ import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interface
|
|||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
|
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
|
||||||
|
import {
|
||||||
|
WorkspaceMetadataCacheException,
|
||||||
|
WorkspaceMetadataCacheExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/workspace-metadata-cache/exceptions/workspace-metadata-cache.exception';
|
||||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||||
import {
|
import {
|
||||||
WorkspaceMetadataVersionException,
|
WorkspaceMetadataVersionException,
|
||||||
@ -120,9 +124,9 @@ export class WorkspaceDatasourceFactory {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!cachedObjectMetadataMaps) {
|
if (!cachedObjectMetadataMaps) {
|
||||||
throw new TwentyORMException(
|
throw new WorkspaceMetadataCacheException(
|
||||||
`Object metadata collection not found for workspace ${workspaceId}`,
|
`Object metadata collection not found for workspace ${workspaceId}`,
|
||||||
TwentyORMExceptionCode.METADATA_COLLECTION_NOT_FOUND,
|
WorkspaceMetadataCacheExceptionCode.OBJECT_METADATA_COLLECTION_NOT_FOUND,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,7 +333,7 @@ export class WorkspaceDatasourceFactory {
|
|||||||
if (!isDefined(latestWorkspaceMetadataVersion)) {
|
if (!isDefined(latestWorkspaceMetadataVersion)) {
|
||||||
if (shouldFailIfMetadataNotFound) {
|
if (shouldFailIfMetadataNotFound) {
|
||||||
throw new WorkspaceMetadataVersionException(
|
throw new WorkspaceMetadataVersionException(
|
||||||
`Metadata version not found for workspace ${workspaceId}`,
|
`Metadata version not found while fetching datasource for workspace ${workspaceId}`,
|
||||||
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
|
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -345,9 +349,9 @@ export class WorkspaceDatasourceFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isDefined(latestWorkspaceMetadataVersion)) {
|
if (!isDefined(latestWorkspaceMetadataVersion)) {
|
||||||
throw new TwentyORMException(
|
throw new WorkspaceMetadataVersionException(
|
||||||
`Metadata version not found after recompute`,
|
`Metadata version not found after recompute`,
|
||||||
TwentyORMExceptionCode.METADATA_VERSION_NOT_FOUND,
|
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
TimeoutError,
|
TimeoutError,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
|
import { CustomException } from 'src/utils/custom-exception';
|
||||||
|
|
||||||
const graphQLPredefinedExceptions = {
|
const graphQLPredefinedExceptions = {
|
||||||
400: ValidationError,
|
400: ValidationError,
|
||||||
@ -46,44 +47,63 @@ export const handleExceptionAndConvertToGraphQLError = (
|
|||||||
user?: ExceptionHandlerUser,
|
user?: ExceptionHandlerUser,
|
||||||
workspace?: ExceptionHandlerWorkspace,
|
workspace?: ExceptionHandlerWorkspace,
|
||||||
): BaseGraphQLError => {
|
): BaseGraphQLError => {
|
||||||
handleException(exception, exceptionHandlerService, user, workspace);
|
handleException({
|
||||||
|
exception,
|
||||||
|
exceptionHandlerService,
|
||||||
|
user,
|
||||||
|
workspace,
|
||||||
|
});
|
||||||
|
|
||||||
return convertExceptionToGraphQLError(exception);
|
return convertExceptionToGraphQLError(exception);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shouldFilterException = (exception: Error): boolean => {
|
export const shouldCaptureException = (
|
||||||
|
exception: Error,
|
||||||
|
statusCode?: number,
|
||||||
|
): boolean => {
|
||||||
if (
|
if (
|
||||||
exception instanceof GraphQLError &&
|
exception instanceof GraphQLError &&
|
||||||
(exception?.extensions?.http?.status ?? 500) < 500
|
(exception?.extensions?.http?.status ?? 500) < 500
|
||||||
) {
|
) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
exception instanceof BaseGraphQLError &&
|
exception instanceof BaseGraphQLError &&
|
||||||
graphQLErrorCodesToFilter.includes(exception?.extensions?.code)
|
graphQLErrorCodesToFilter.includes(exception?.extensions?.code)
|
||||||
) {
|
) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exception instanceof HttpException && exception.getStatus() < 500) {
|
if (exception instanceof HttpException && exception.getStatus() < 500) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
if (statusCode && statusCode < 500) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleException = (
|
export const handleException = <T extends Error | CustomException>({
|
||||||
exception: Error,
|
exception,
|
||||||
exceptionHandlerService: ExceptionHandlerService,
|
exceptionHandlerService,
|
||||||
user?: ExceptionHandlerUser,
|
user,
|
||||||
workspace?: ExceptionHandlerWorkspace,
|
workspace,
|
||||||
): void => {
|
statusCode,
|
||||||
if (shouldFilterException(exception)) {
|
}: {
|
||||||
return;
|
exception: T;
|
||||||
|
exceptionHandlerService: ExceptionHandlerService;
|
||||||
|
user?: ExceptionHandlerUser;
|
||||||
|
workspace?: ExceptionHandlerWorkspace;
|
||||||
|
statusCode?: number;
|
||||||
|
}): T => {
|
||||||
|
if (shouldCaptureException(exception, statusCode)) {
|
||||||
|
exceptionHandlerService.captureExceptions([exception], { user, workspace });
|
||||||
}
|
}
|
||||||
|
|
||||||
exceptionHandlerService.captureExceptions([exception], { user, workspace });
|
return exception;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const convertExceptionToGraphQLError = (
|
export const convertExceptionToGraphQLError = (
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { NestFactory } from '@nestjs/core';
|
|||||||
|
|
||||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||||
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
|
import { LoggerService } from 'src/engine/core-modules/logger/logger.service';
|
||||||
import { shouldFilterException } from 'src/engine/utils/global-exception-handler.util';
|
import { shouldCaptureException } from 'src/engine/utils/global-exception-handler.util';
|
||||||
import 'src/instrument';
|
import 'src/instrument';
|
||||||
import { QueueWorkerModule } from 'src/queue-worker/queue-worker.module';
|
import { QueueWorkerModule } from 'src/queue-worker/queue-worker.module';
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ async function bootstrap() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
loggerService?.error(err?.message, err?.name);
|
loggerService?.error(err?.message, err?.name);
|
||||||
|
|
||||||
if (!shouldFilterException(err)) {
|
if (shouldCaptureException(err)) {
|
||||||
exceptionHandlerService?.captureExceptions([err]);
|
exceptionHandlerService?.captureExceptions([err]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ describe('Core REST API Authentication', () => {
|
|||||||
path: `/people`,
|
path: `/people`,
|
||||||
bearer: '',
|
bearer: '',
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(403)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
expect(res.body.error).toBe('FORBIDDEN_EXCEPTION');
|
expect(res.body.error).toBe('FORBIDDEN_EXCEPTION');
|
||||||
expect(res.body.messages[0]).toBe('Missing authentication token');
|
expect(res.body.messages[0]).toBe('Missing authentication token');
|
||||||
|
|||||||
Reference in New Issue
Block a user