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 { 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';
|
||||
|
||||
@ -10,11 +10,9 @@ async function bootstrap() {
|
||||
const errorHandler = (err: Error) => {
|
||||
loggerService.error(err?.message, err?.name);
|
||||
|
||||
if (shouldFilterException(err)) {
|
||||
return;
|
||||
if (shouldCaptureException(err)) {
|
||||
exceptionHandlerService.captureExceptions([err]);
|
||||
}
|
||||
|
||||
exceptionHandlerService.captureExceptions([err]);
|
||||
};
|
||||
|
||||
const app = await CommandFactory.createWithoutRunning(CommandModule, {
|
||||
|
||||
@ -22,6 +22,5 @@ export enum GraphqlQueryRunnerExceptionCode {
|
||||
RELATION_SETTINGS_NOT_FOUND = 'RELATION_SETTINGS_NOT_FOUND',
|
||||
RELATION_TARGET_OBJECT_METADATA_NOT_FOUND = 'RELATION_TARGET_OBJECT_METADATA_NOT_FOUND',
|
||||
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
|
||||
OBJECT_METADATA_COLLECTION_NOT_FOUND = 'OBJECT_METADATA_COLLECTION_NOT_FOUND',
|
||||
INVALID_POST_HOOK_PAYLOAD = 'INVALID_POST_HOOK_PAYLOAD',
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@ export const graphqlQueryRunnerExceptionHandler = (
|
||||
throw new NotFoundError(error.message);
|
||||
case GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND:
|
||||
case GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND:
|
||||
case GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_COLLECTION_NOT_FOUND:
|
||||
case GraphqlQueryRunnerExceptionCode.INVALID_POST_HOOK_PAYLOAD:
|
||||
throw error;
|
||||
default: {
|
||||
|
||||
@ -5,10 +5,6 @@ import { GraphQLSchema, printSchema } from 'graphql';
|
||||
import { gql } from 'graphql-tag';
|
||||
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 { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
|
||||
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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
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 {
|
||||
WorkspaceMetadataVersionException,
|
||||
@ -86,9 +86,9 @@ export class WorkspaceSchemaFactory {
|
||||
}
|
||||
|
||||
if (!objectMetadataMaps) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
throw new WorkspaceMetadataCacheException(
|
||||
'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:
|
||||
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;
|
||||
|
||||
@ -8,4 +8,5 @@ export class WorkspaceMetadataCacheException extends CustomException {
|
||||
|
||||
export enum WorkspaceMetadataCacheExceptionCode {
|
||||
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 { 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 { 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 { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
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
|
||||
public writeRestResponseOnExceptionCaught(res: Response, error: any) {
|
||||
// capture and handle custom exceptions
|
||||
handleException(error as CustomException, this.exceptionHandlerService);
|
||||
|
||||
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.write(
|
||||
JSON.stringify({
|
||||
@ -158,13 +163,8 @@ export class MiddlewareService {
|
||||
return error.status;
|
||||
}
|
||||
|
||||
if (error instanceof CustomException) {
|
||||
switch (error.code) {
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
return 401;
|
||||
default:
|
||||
return 400;
|
||||
}
|
||||
if (error instanceof AuthException) {
|
||||
return getAuthExceptionRestStatus(error);
|
||||
}
|
||||
|
||||
return 500;
|
||||
|
||||
@ -7,9 +7,7 @@ export class TwentyORMException extends CustomException {
|
||||
}
|
||||
|
||||
export enum TwentyORMExceptionCode {
|
||||
METADATA_VERSION_NOT_FOUND = 'METADATA_VERSION_NOT_FOUND',
|
||||
METADATA_VERSION_MISMATCH = 'METADATA_VERSION_MISMATCH',
|
||||
METADATA_COLLECTION_NOT_FOUND = 'METADATA_COLLECTION_NOT_FOUND',
|
||||
WORKSPACE_SCHEMA_NOT_FOUND = 'WORKSPACE_SCHEMA_NOT_FOUND',
|
||||
ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_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 { 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 {
|
||||
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 {
|
||||
WorkspaceMetadataVersionException,
|
||||
@ -120,9 +124,9 @@ export class WorkspaceDatasourceFactory {
|
||||
);
|
||||
|
||||
if (!cachedObjectMetadataMaps) {
|
||||
throw new TwentyORMException(
|
||||
throw new WorkspaceMetadataCacheException(
|
||||
`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 (shouldFailIfMetadataNotFound) {
|
||||
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,
|
||||
);
|
||||
} else {
|
||||
@ -345,9 +349,9 @@ export class WorkspaceDatasourceFactory {
|
||||
}
|
||||
|
||||
if (!isDefined(latestWorkspaceMetadataVersion)) {
|
||||
throw new TwentyORMException(
|
||||
throw new WorkspaceMetadataVersionException(
|
||||
`Metadata version not found after recompute`,
|
||||
TwentyORMExceptionCode.METADATA_VERSION_NOT_FOUND,
|
||||
WorkspaceMetadataVersionExceptionCode.METADATA_VERSION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
TimeoutError,
|
||||
ValidationError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
const graphQLPredefinedExceptions = {
|
||||
400: ValidationError,
|
||||
@ -46,44 +47,63 @@ export const handleExceptionAndConvertToGraphQLError = (
|
||||
user?: ExceptionHandlerUser,
|
||||
workspace?: ExceptionHandlerWorkspace,
|
||||
): BaseGraphQLError => {
|
||||
handleException(exception, exceptionHandlerService, user, workspace);
|
||||
handleException({
|
||||
exception,
|
||||
exceptionHandlerService,
|
||||
user,
|
||||
workspace,
|
||||
});
|
||||
|
||||
return convertExceptionToGraphQLError(exception);
|
||||
};
|
||||
|
||||
export const shouldFilterException = (exception: Error): boolean => {
|
||||
export const shouldCaptureException = (
|
||||
exception: Error,
|
||||
statusCode?: number,
|
||||
): boolean => {
|
||||
if (
|
||||
exception instanceof GraphQLError &&
|
||||
(exception?.extensions?.http?.status ?? 500) < 500
|
||||
) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
exception instanceof BaseGraphQLError &&
|
||||
graphQLErrorCodesToFilter.includes(exception?.extensions?.code)
|
||||
) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exception instanceof HttpException && exception.getStatus() < 500) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (statusCode && statusCode < 500) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const handleException = (
|
||||
exception: Error,
|
||||
exceptionHandlerService: ExceptionHandlerService,
|
||||
user?: ExceptionHandlerUser,
|
||||
workspace?: ExceptionHandlerWorkspace,
|
||||
): void => {
|
||||
if (shouldFilterException(exception)) {
|
||||
return;
|
||||
export const handleException = <T extends Error | CustomException>({
|
||||
exception,
|
||||
exceptionHandlerService,
|
||||
user,
|
||||
workspace,
|
||||
statusCode,
|
||||
}: {
|
||||
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 = (
|
||||
|
||||
@ -2,7 +2,7 @@ import { NestFactory } from '@nestjs/core';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.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 { QueueWorkerModule } from 'src/queue-worker/queue-worker.module';
|
||||
|
||||
@ -23,7 +23,7 @@ async function bootstrap() {
|
||||
} catch (err) {
|
||||
loggerService?.error(err?.message, err?.name);
|
||||
|
||||
if (!shouldFilterException(err)) {
|
||||
if (shouldCaptureException(err)) {
|
||||
exceptionHandlerService?.captureExceptions([err]);
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ describe('Core REST API Authentication', () => {
|
||||
path: `/people`,
|
||||
bearer: '',
|
||||
})
|
||||
.expect(400)
|
||||
.expect(403)
|
||||
.expect((res) => {
|
||||
expect(res.body.error).toBe('FORBIDDEN_EXCEPTION');
|
||||
expect(res.body.messages[0]).toBe('Missing authentication token');
|
||||
|
||||
Reference in New Issue
Block a user