fix: sentry doesn't catch exceptions from flexible backend (#3074)
* fix: sentry doesn't catch exceptions from flexible backend * fix: send remaining errors to Sentry * fix: missing debug * feat: use an util exception handler instead of Nest.JS class
This commit is contained in:
@ -1,19 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { GraphQLModule } from '@nestjs/graphql';
|
import { GraphQLModule } from '@nestjs/graphql';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { APP_FILTER, ContextIdFactory, ModuleRef } from '@nestjs/core';
|
import { APP_FILTER } from '@nestjs/core';
|
||||||
|
|
||||||
import { GraphQLError, GraphQLSchema } from 'graphql';
|
|
||||||
import { YogaDriver, YogaDriverConfig } from '@graphql-yoga/nestjs';
|
import { YogaDriver, YogaDriverConfig } from '@graphql-yoga/nestjs';
|
||||||
import GraphQLJSON from 'graphql-type-json';
|
|
||||||
import { TokenExpiredError, JsonWebTokenError } from 'jsonwebtoken';
|
|
||||||
|
|
||||||
import { WorkspaceFactory } from 'src/workspace/workspace.factory';
|
import { GraphQLConfigService } from 'src/graphql-config.service';
|
||||||
import { TypeOrmExceptionFilter } from 'src/filters/typeorm-exception.filter';
|
|
||||||
import { HttpExceptionFilter } from 'src/filters/http-exception.filter';
|
|
||||||
import { GlobalExceptionFilter } from 'src/filters/global-exception.filter';
|
import { GlobalExceptionFilter } from 'src/filters/global-exception.filter';
|
||||||
import { TokenService } from 'src/core/auth/services/token.service';
|
|
||||||
import { Workspace } from 'src/core/workspace/workspace.entity';
|
|
||||||
|
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
@ -27,61 +20,10 @@ import { WorkspaceModule } from './workspace/workspace.module';
|
|||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
GraphQLModule.forRoot<YogaDriverConfig>({
|
GraphQLModule.forRootAsync<YogaDriverConfig>({
|
||||||
context: ({ req }) => ({ req }),
|
|
||||||
driver: YogaDriver,
|
driver: YogaDriver,
|
||||||
autoSchemaFile: true,
|
imports: [CoreModule],
|
||||||
include: [CoreModule],
|
useClass: GraphQLConfigService,
|
||||||
conditionalSchema: async (request) => {
|
|
||||||
try {
|
|
||||||
// Get TokenService from AppModule
|
|
||||||
const tokenService = AppModule.moduleRef.get(TokenService, {
|
|
||||||
strict: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let workspace: Workspace;
|
|
||||||
|
|
||||||
try {
|
|
||||||
workspace = await tokenService.validateToken(request.req);
|
|
||||||
} catch (err) {
|
|
||||||
return new GraphQLSchema({});
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextId = ContextIdFactory.create();
|
|
||||||
|
|
||||||
AppModule.moduleRef.registerRequestByContextId(request, contextId);
|
|
||||||
|
|
||||||
// Get the SchemaGenerationService from the AppModule
|
|
||||||
const workspaceFactory = await AppModule.moduleRef.resolve(
|
|
||||||
WorkspaceFactory,
|
|
||||||
contextId,
|
|
||||||
{
|
|
||||||
strict: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return await workspaceFactory.createGraphQLSchema(workspace.id);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof JsonWebTokenError) {
|
|
||||||
//mockedUserJWT
|
|
||||||
throw new GraphQLError('Unauthenticated', {
|
|
||||||
extensions: {
|
|
||||||
code: 'UNAUTHENTICATED',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (error instanceof TokenExpiredError) {
|
|
||||||
throw new GraphQLError('Unauthenticated', {
|
|
||||||
extensions: {
|
|
||||||
code: 'UNAUTHENTICATED',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resolvers: { JSON: GraphQLJSON },
|
|
||||||
plugins: [],
|
|
||||||
}),
|
}),
|
||||||
HealthModule,
|
HealthModule,
|
||||||
IntegrationsModule,
|
IntegrationsModule,
|
||||||
@ -90,27 +32,10 @@ import { WorkspaceModule } from './workspace/workspace.module';
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AppService,
|
AppService,
|
||||||
// Exceptions filters must be ordered from the least specific to the most specific
|
|
||||||
// If TypeOrmExceptionFilter handle something, HttpExceptionFilter will not handle it
|
|
||||||
// GlobalExceptionFilter will handle the rest of the exceptions
|
|
||||||
{
|
{
|
||||||
provide: APP_FILTER,
|
provide: APP_FILTER,
|
||||||
useClass: GlobalExceptionFilter,
|
useValue: GlobalExceptionFilter,
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: APP_FILTER,
|
|
||||||
useClass: HttpExceptionFilter,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: APP_FILTER,
|
|
||||||
useClass: TypeOrmExceptionFilter,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {}
|
||||||
static moduleRef: ModuleRef;
|
|
||||||
|
|
||||||
constructor(private moduleRef: ModuleRef) {
|
|
||||||
AppModule.moduleRef = this.moduleRef;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Catch, Injectable } from '@nestjs/common';
|
import { Catch, Injectable } from '@nestjs/common';
|
||||||
import { GqlExceptionFilter } from '@nestjs/graphql';
|
import { GqlExceptionFilter } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { globalExceptionHandler } from 'src/filters/utils/global-exception-handler.util';
|
||||||
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
|
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
|
||||||
|
|
||||||
@Catch()
|
@Catch()
|
||||||
@ -11,8 +12,6 @@ export class GlobalExceptionFilter implements GqlExceptionFilter {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
catch(exception: unknown) {
|
catch(exception: unknown) {
|
||||||
this.exceptionHandlerService.captureException(exception);
|
return globalExceptionHandler(exception, this.exceptionHandlerService);
|
||||||
|
|
||||||
return exception;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
import { ArgumentsHost, Catch, HttpException } from '@nestjs/common';
|
|
||||||
import { GqlContextType, GqlExceptionFilter } from '@nestjs/graphql';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AuthenticationError,
|
|
||||||
BaseGraphQLError,
|
|
||||||
ForbiddenError,
|
|
||||||
} from 'src/filters/utils/graphql-errors.util';
|
|
||||||
|
|
||||||
const graphQLPredefinedExceptions = {
|
|
||||||
401: AuthenticationError,
|
|
||||||
403: ForbiddenError,
|
|
||||||
};
|
|
||||||
|
|
||||||
@Catch(HttpException)
|
|
||||||
export class HttpExceptionFilter
|
|
||||||
implements GqlExceptionFilter<HttpException, BaseGraphQLError | null>
|
|
||||||
{
|
|
||||||
catch(exception: HttpException, host: ArgumentsHost) {
|
|
||||||
if (host.getType<GqlContextType>() !== 'graphql') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let error: BaseGraphQLError;
|
|
||||||
|
|
||||||
if (exception.getStatus() in graphQLPredefinedExceptions) {
|
|
||||||
error = new graphQLPredefinedExceptions[exception.getStatus()](
|
|
||||||
exception.message,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
error = new BaseGraphQLError(
|
|
||||||
exception.message,
|
|
||||||
exception.getStatus().toString(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
error.stack = exception.stack;
|
|
||||||
error.extensions['response'] = exception.getResponse();
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import { ArgumentsHost, Catch } from '@nestjs/common';
|
|
||||||
import { GqlContextType, GqlExceptionFilter } from '@nestjs/graphql';
|
|
||||||
|
|
||||||
import { TypeORMError } from 'typeorm';
|
|
||||||
|
|
||||||
import { BaseGraphQLError } from 'src/filters/utils/graphql-errors.util';
|
|
||||||
|
|
||||||
@Catch(TypeORMError)
|
|
||||||
export class TypeOrmExceptionFilter
|
|
||||||
implements GqlExceptionFilter<TypeORMError, BaseGraphQLError | null>
|
|
||||||
{
|
|
||||||
catch(exception: TypeORMError, host: ArgumentsHost) {
|
|
||||||
if (host.getType<GqlContextType>() !== 'graphql') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = new BaseGraphQLError(exception.name, 'INTERNAL_SERVER_ERROR');
|
|
||||||
|
|
||||||
error.stack = exception.stack;
|
|
||||||
error.extensions['response'] = exception.message;
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
import { HttpException } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { TypeORMError } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthenticationError,
|
||||||
|
BaseGraphQLError,
|
||||||
|
ForbiddenError,
|
||||||
|
} from 'src/filters/utils/graphql-errors.util';
|
||||||
|
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
|
||||||
|
|
||||||
|
const graphQLPredefinedExceptions = {
|
||||||
|
401: AuthenticationError,
|
||||||
|
403: ForbiddenError,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const globalExceptionHandler = (
|
||||||
|
exception: unknown,
|
||||||
|
exceptionHandlerService: ExceptionHandlerService,
|
||||||
|
) => {
|
||||||
|
if (exception instanceof HttpException) {
|
||||||
|
return httpExceptionHandler(exception, exceptionHandlerService);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception instanceof TypeORMError) {
|
||||||
|
return typeOrmExceptionHandler(exception, exceptionHandlerService);
|
||||||
|
}
|
||||||
|
|
||||||
|
exceptionHandlerService.captureException(exception);
|
||||||
|
|
||||||
|
return exception;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const httpExceptionHandler = (
|
||||||
|
exception: HttpException,
|
||||||
|
exceptionHandlerService: ExceptionHandlerService,
|
||||||
|
) => {
|
||||||
|
const status = exception.getStatus();
|
||||||
|
let error: BaseGraphQLError;
|
||||||
|
|
||||||
|
// Capture all 5xx errors and send them to exception handler
|
||||||
|
if (status >= 500) {
|
||||||
|
exceptionHandlerService.captureException(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status in graphQLPredefinedExceptions) {
|
||||||
|
error = new graphQLPredefinedExceptions[exception.getStatus()](
|
||||||
|
exception.message,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
error = new BaseGraphQLError(
|
||||||
|
exception.message,
|
||||||
|
exception.getStatus().toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
error.stack = exception.stack;
|
||||||
|
error.extensions['response'] = exception.getResponse();
|
||||||
|
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const typeOrmExceptionHandler = (
|
||||||
|
exception: TypeORMError,
|
||||||
|
exceptionHandlerService: ExceptionHandlerService,
|
||||||
|
) => {
|
||||||
|
exceptionHandlerService.captureException(exception);
|
||||||
|
|
||||||
|
const error = new BaseGraphQLError(exception.name, 'INTERNAL_SERVER_ERROR');
|
||||||
|
|
||||||
|
error.stack = exception.stack;
|
||||||
|
error.extensions['response'] = exception.message;
|
||||||
|
|
||||||
|
return error;
|
||||||
|
};
|
||||||
95
packages/twenty-server/src/graphql-config.service.ts
Normal file
95
packages/twenty-server/src/graphql-config.service.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
|
||||||
|
import { GqlOptionsFactory } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import {
|
||||||
|
YogaDriverConfig,
|
||||||
|
YogaDriverServerContext,
|
||||||
|
} from '@graphql-yoga/nestjs';
|
||||||
|
import { GraphQLSchema, GraphQLError } from 'graphql';
|
||||||
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
|
||||||
|
import { GraphQLSchemaWithContext, YogaInitialContext } from 'graphql-yoga';
|
||||||
|
|
||||||
|
import { TokenService } from 'src/core/auth/services/token.service';
|
||||||
|
import { CoreModule } from 'src/core/core.module';
|
||||||
|
import { Workspace } from 'src/core/workspace/workspace.entity';
|
||||||
|
import { WorkspaceFactory } from 'src/workspace/workspace.factory';
|
||||||
|
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
|
||||||
|
import { globalExceptionHandler } from 'src/filters/utils/global-exception-handler.util';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GraphQLConfigService
|
||||||
|
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly tokenService: TokenService,
|
||||||
|
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||||
|
private readonly moduleRef: ModuleRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
createGqlOptions(): YogaDriverConfig {
|
||||||
|
return {
|
||||||
|
context: ({ req }) => ({ req }),
|
||||||
|
autoSchemaFile: true,
|
||||||
|
include: [CoreModule],
|
||||||
|
conditionalSchema: async (context) => {
|
||||||
|
try {
|
||||||
|
let workspace: Workspace;
|
||||||
|
|
||||||
|
// If token is not valid, it will return an empty schema
|
||||||
|
try {
|
||||||
|
workspace = await this.tokenService.validateToken(context.req);
|
||||||
|
} catch (err) {
|
||||||
|
return new GraphQLSchema({});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.createSchema(context, workspace);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof JsonWebTokenError) {
|
||||||
|
//mockedUserJWT
|
||||||
|
throw new GraphQLError('Unauthenticated', {
|
||||||
|
extensions: {
|
||||||
|
code: 'UNAUTHENTICATED',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
throw new GraphQLError('Unauthenticated', {
|
||||||
|
extensions: {
|
||||||
|
code: 'UNAUTHENTICATED',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw globalExceptionHandler(error, this.exceptionHandlerService);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolvers: { JSON: GraphQLJSON },
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSchema(
|
||||||
|
context: YogaDriverServerContext<'express'> & YogaInitialContext,
|
||||||
|
workspace: Workspace,
|
||||||
|
): Promise<GraphQLSchemaWithContext<YogaDriverServerContext<'express'>>> {
|
||||||
|
// Create a new contextId for each request
|
||||||
|
const contextId = ContextIdFactory.create();
|
||||||
|
|
||||||
|
// Register the request in the contextId
|
||||||
|
this.moduleRef.registerRequestByContextId(context.req, contextId);
|
||||||
|
|
||||||
|
// Resolve the WorkspaceFactory for the contextId
|
||||||
|
const workspaceFactory = await this.moduleRef.resolve(
|
||||||
|
WorkspaceFactory,
|
||||||
|
contextId,
|
||||||
|
{
|
||||||
|
strict: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return await workspaceFactory.createGraphQLSchema(workspace.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user