diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 3b8e0762f1..c41eddf745 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -57,8 +57,9 @@ "@ptc-org/nestjs-query-typeorm": "4.2.1-alpha.2", "@react-email/components": "0.0.12", "@react-email/render": "0.0.10", - "@sentry/node": "^7.66.0", + "@sentry/node": "^7.98.0", "@sentry/profiling-node": "^1.3.4", + "@sentry/tracing": "^7.98.0", "axios": "^1.6.2", "bcrypt": "^5.1.1", "bullmq": "^4.14.0", diff --git a/packages/twenty-server/src/command.ts b/packages/twenty-server/src/command.ts index ffee901dea..bbc07bec9d 100644 --- a/packages/twenty-server/src/command.ts +++ b/packages/twenty-server/src/command.ts @@ -2,8 +2,34 @@ import { CommandFactory } from 'nest-commander'; import { CommandModule } from './command.module'; +import { LoggerService } from './integrations/logger/logger.service'; +import { ExceptionHandlerService } from './integrations/exception-handler/exception-handler.service'; +import { filterException } from './filters/utils/global-exception-handler.util'; + async function bootstrap() { - // TODO: inject our own logger service to handle the output (Sentry, etc.) - await CommandFactory.run(CommandModule, ['warn', 'error', 'log']); + const errorHandler = (err: Error) => { + loggerService.error(err?.message, err?.name); + + if (filterException(err)) { + return; + } + + exceptionHandlerService.captureExceptions([err]); + }; + + const app = await CommandFactory.createWithoutRunning(CommandModule, { + bufferLogs: true, + errorHandler, + serviceErrorHandler: errorHandler, + }); + const loggerService = app.get(LoggerService); + const exceptionHandlerService = app.get(ExceptionHandlerService); + + // Inject our logger + app.useLogger(loggerService); + + await CommandFactory.runApplication(app); + + app.close(); } bootstrap(); diff --git a/packages/twenty-server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts b/packages/twenty-server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts index df8360cb9f..2fa25abfe1 100644 --- a/packages/twenty-server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts +++ b/packages/twenty-server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts @@ -40,7 +40,7 @@ export class ApiRestQueryBuilderFactory { objectMetadataItems: ObjectMetadataEntity[]; objectMetadataItem: ObjectMetadataEntity; }> { - const workspace = await this.tokenService.validateToken(request); + const { workspace } = await this.tokenService.validateToken(request); const objectMetadataItems = await this.objectMetadataService.findManyWithinWorkspace(workspace.id); diff --git a/packages/twenty-server/src/core/auth/services/token.service.ts b/packages/twenty-server/src/core/auth/services/token.service.ts index 8fbc41bfb0..96a87d7bf2 100644 --- a/packages/twenty-server/src/core/auth/services/token.service.ts +++ b/packages/twenty-server/src/core/auth/services/token.service.ts @@ -192,7 +192,10 @@ export class TokenService { return !!token; } - async validateToken(request: Request): Promise { + async validateToken(request: Request): Promise<{ + user?: User; + workspace: Workspace; + }> { const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); if (!token) { @@ -203,11 +206,11 @@ export class TokenService { this.environmentService.getAccessTokenSecret(), ); - const { workspace } = await this.jwtStrategy.validate( + const { user, workspace } = await this.jwtStrategy.validate( decoded as JwtPayload, ); - return workspace; + return { user, workspace }; } async verifyLoginToken(loginToken: string): Promise { diff --git a/packages/twenty-server/src/core/open-api/open-api.service.ts b/packages/twenty-server/src/core/open-api/open-api.service.ts index 80409e429c..c9426c0ee0 100644 --- a/packages/twenty-server/src/core/open-api/open-api.service.ts +++ b/packages/twenty-server/src/core/open-api/open-api.service.ts @@ -30,7 +30,7 @@ export class OpenApiService { let objectMetadataItems; try { - const workspace = await this.tokenService.validateToken(request); + const { workspace } = await this.tokenService.validateToken(request); objectMetadataItems = await this.objectMetadataService.findManyWithinWorkspace(workspace.id); diff --git a/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts b/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts index ed8037a433..d57619fe0f 100644 --- a/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts +++ b/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts @@ -1,5 +1,7 @@ import { HttpException } from '@nestjs/common'; +import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface'; + import { AuthenticationError, BaseGraphQLError, @@ -21,20 +23,31 @@ const graphQLPredefinedExceptions = { export const handleExceptionAndConvertToGraphQLError = ( exception: Error, exceptionHandlerService: ExceptionHandlerService, + user?: ExceptionHandlerUser, ): BaseGraphQLError => { - handleException(exception, exceptionHandlerService); + handleException(exception, exceptionHandlerService, user); return convertExceptionToGraphQLError(exception); }; +export const filterException = (exception: Error): boolean => { + if (exception instanceof HttpException && exception.getStatus() < 500) { + return true; + } + + return false; +}; + export const handleException = ( exception: Error, exceptionHandlerService: ExceptionHandlerService, + user?: ExceptionHandlerUser, ): void => { - if (exception instanceof HttpException && exception.getStatus() < 500) { + if (filterException(exception)) { return; } - exceptionHandlerService.captureException(exception); + + exceptionHandlerService.captureExceptions([exception], { user }); }; export const convertExceptionToGraphQLError = ( @@ -62,8 +75,11 @@ export const convertHttpExceptionToGraphql = (exception: HttpException) => { ); } - error.stack = exception.stack; - error.extensions['response'] = exception.getResponse(); + // Only show the stack trace in development mode + if (process.env.NODE_ENV === 'development') { + error.stack = exception.stack; + error.extensions['response'] = exception.getResponse(); + } return error; }; diff --git a/packages/twenty-server/src/graphql-config.service.ts b/packages/twenty-server/src/graphql-config.service.ts index aa543c8e4c..46d62c1a82 100644 --- a/packages/twenty-server/src/graphql-config.service.ts +++ b/packages/twenty-server/src/graphql-config.service.ts @@ -9,11 +9,7 @@ import { import { GraphQLSchema, GraphQLError } from 'graphql'; import GraphQLJSON from 'graphql-type-json'; import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken'; -import { - GraphQLSchemaWithContext, - YogaInitialContext, - maskError, -} from 'graphql-yoga'; +import { GraphQLSchemaWithContext, YogaInitialContext } from 'graphql-yoga'; import { TokenService } from 'src/core/auth/services/token.service'; import { CoreModule } from 'src/core/core.module'; @@ -24,6 +20,9 @@ import { handleExceptionAndConvertToGraphQLError } from 'src/filters/utils/globa import { renderApolloPlayground } from 'src/workspace/utils/render-apollo-playground.util'; import { EnvironmentService } from 'src/integrations/environment/environment.service'; +import { useExceptionHandler } from './integrations/exception-handler/hooks/use-exception-handler.hook'; +import { User } from './core/user/user.entity'; + @Injectable() export class GraphQLConfigService implements GqlOptionsFactory> @@ -36,33 +35,24 @@ export class GraphQLConfigService ) {} createGqlOptions(): YogaDriverConfig { - const exceptionHandlerService = this.exceptionHandlerService; const isDebugMode = this.environmentService.isDebugMode(); const config: YogaDriverConfig = { context: ({ req }) => ({ req }), autoSchemaFile: true, include: [CoreModule], - maskedErrors: { - maskError(error: GraphQLError, message, isDev) { - if (error.originalError) { - return handleExceptionAndConvertToGraphQLError( - error.originalError, - exceptionHandlerService, - ); - } - - return maskError(error, message, isDev); - }, - }, conditionalSchema: async (context) => { + let user: User | undefined; + try { if (!this.tokenService.isTokenPresent(context.req)) { return new GraphQLSchema({}); } - const workspace = await this.tokenService.validateToken(context.req); + const data = await this.tokenService.validateToken(context.req); - return await this.createSchema(context, workspace); + user = data.user; + + return await this.createSchema(context, data.workspace); } catch (error) { if (error instanceof UnauthorizedException) { throw new GraphQLError('Unauthenticated', { @@ -92,11 +82,22 @@ export class GraphQLConfigService throw handleExceptionAndConvertToGraphQLError( error, this.exceptionHandlerService, + user + ? { + id: user.id, + email: user.email, + } + : undefined, ); } }, resolvers: { JSON: GraphQLJSON }, - plugins: [], + plugins: [ + useExceptionHandler({ + exceptionHandlerService: this.exceptionHandlerService, + tokenService: this.tokenService, + }), + ], }; if (isDebugMode) { diff --git a/packages/twenty-server/src/integrations/exception-handler/drivers/console.driver.ts b/packages/twenty-server/src/integrations/exception-handler/drivers/console.driver.ts index 4dfbf4d172..cd38ef827a 100644 --- a/packages/twenty-server/src/integrations/exception-handler/drivers/console.driver.ts +++ b/packages/twenty-server/src/integrations/exception-handler/drivers/console.driver.ts @@ -1,16 +1,26 @@ +import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface'; +import { ExceptionHandlerOptions } from 'src/integrations/exception-handler/interfaces/exception-handler-options.interface'; + import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces'; export class ExceptionHandlerConsoleDriver implements ExceptionHandlerDriverInterface { - captureException(exception: unknown) { + captureExceptions( + exceptions: ReadonlyArray, + options?: ExceptionHandlerOptions, + ) { console.group('Exception Captured'); - console.error(exception); + console.info(options); + console.error(exceptions); console.groupEnd(); + + return []; } - captureMessage(message: string): void { + captureMessage(message: string, user?: ExceptionHandlerUser): void { console.group('Message Captured'); + console.info(user); console.info(message); console.groupEnd(); } diff --git a/packages/twenty-server/src/integrations/exception-handler/drivers/sentry.driver.ts b/packages/twenty-server/src/integrations/exception-handler/drivers/sentry.driver.ts index b274910e85..6c6add8ba0 100644 --- a/packages/twenty-server/src/integrations/exception-handler/drivers/sentry.driver.ts +++ b/packages/twenty-server/src/integrations/exception-handler/drivers/sentry.driver.ts @@ -1,6 +1,9 @@ import * as Sentry from '@sentry/node'; import { ProfilingIntegration } from '@sentry/profiling-node'; +import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface'; +import { ExceptionHandlerOptions } from 'src/integrations/exception-handler/interfaces/exception-handler-options.interface'; + import { ExceptionHandlerDriverInterface, ExceptionHandlerSentryDriverFactoryOptions, @@ -30,11 +33,69 @@ export class ExceptionHandlerSentryDriver }); } - captureException(exception: Error) { - Sentry.captureException(exception); + captureExceptions( + exceptions: ReadonlyArray, + options?: ExceptionHandlerOptions, + ) { + const eventIds: string[] = []; + + Sentry.withScope((scope) => { + if (options?.operation) { + scope.setTag('operation', options.operation.name); + scope.setTag('operationName', options.operation.name); + } + + if (options?.document) { + scope.setExtra('document', options.document); + } + + for (const exception of exceptions) { + const errorPath = (exception.path ?? []) + .map((v: string | number) => (typeof v === 'number' ? '$index' : v)) + .join(' > '); + + if (errorPath) { + scope.addBreadcrumb({ + category: 'execution-path', + message: errorPath, + level: 'debug', + }); + } + + const eventId = Sentry.captureException(exception, { + fingerprint: [ + 'graphql', + errorPath, + options?.operation?.name, + options?.operation?.type, + ], + contexts: { + GraphQL: { + operationName: options?.operation?.name, + operationType: options?.operation?.type, + }, + }, + }); + + eventIds.push(eventId); + } + }); + + return eventIds; } - captureMessage(message: string) { - Sentry.captureMessage(message); + captureMessage(message: string, user?: ExceptionHandlerUser) { + Sentry.captureMessage(message, (scope) => { + if (user) { + scope.setUser({ + id: user.id, + ip_address: user.ipAddress, + email: user.email, + username: user.username, + }); + } + + return scope; + }); } } diff --git a/packages/twenty-server/src/integrations/exception-handler/exception-handler.service.ts b/packages/twenty-server/src/integrations/exception-handler/exception-handler.service.ts index d1a14e3c4f..3dc02cad82 100644 --- a/packages/twenty-server/src/integrations/exception-handler/exception-handler.service.ts +++ b/packages/twenty-server/src/integrations/exception-handler/exception-handler.service.ts @@ -1,8 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; -import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces'; +import { ExceptionHandlerOptions } from 'src/integrations/exception-handler/interfaces/exception-handler-options.interface'; -import { EXCEPTION_HANDLER_DRIVER } from './exception-handler.constants'; +import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces'; +import { EXCEPTION_HANDLER_DRIVER } from 'src/integrations/exception-handler/exception-handler.constants'; @Injectable() export class ExceptionHandlerService { @@ -11,7 +12,10 @@ export class ExceptionHandlerService { private driver: ExceptionHandlerDriverInterface, ) {} - captureException(exception: unknown) { - this.driver.captureException(exception); + captureExceptions( + exceptions: ReadonlyArray, + options?: ExceptionHandlerOptions, + ): string[] { + return this.driver.captureExceptions(exceptions, options); } } diff --git a/packages/twenty-server/src/integrations/exception-handler/hooks/use-exception-handler.hook.ts b/packages/twenty-server/src/integrations/exception-handler/hooks/use-exception-handler.hook.ts new file mode 100644 index 0000000000..7077670721 --- /dev/null +++ b/packages/twenty-server/src/integrations/exception-handler/hooks/use-exception-handler.hook.ts @@ -0,0 +1,150 @@ +import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql'; +import * as express from 'express'; +import { + getDocumentString, + handleStreamOrSingleExecutionResult, + OnExecuteDoneHookResultOnNextHook, + Plugin, +} from '@envelop/core'; + +import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface'; + +import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; +import { TokenService } from 'src/core/auth/services/token.service'; +import { + convertExceptionToGraphQLError, + filterException, +} from 'src/filters/utils/global-exception-handler.util'; + +export type ExceptionHandlerPluginOptions = { + /** + * The driver to use to handle exceptions. + */ + exceptionHandlerService: ExceptionHandlerService; + /** + * The token service to use to get the token from the request. + */ + tokenService: TokenService; + /** + * The key of the event id in the error's extension. `null` to disable. + * @default exceptionEventId + */ + eventIdKey?: string | null; +}; + +export const useExceptionHandler = < + PluginContext extends Record = object, +>( + options: ExceptionHandlerPluginOptions, +): Plugin => { + const exceptionHandlerService = options.exceptionHandlerService; + const tokenService = options.tokenService; + const eventIdKey = options.eventIdKey === null ? null : 'exceptionEventId'; + + function addEventId( + err: GraphQLError, + eventId: string | undefined | null, + ): GraphQLError { + if (eventIdKey !== null && eventId) { + err.extensions[eventIdKey] = eventId; + } + + return err; + } + + return { + async onExecute({ args }) { + const rootOperation = args.document.definitions.find( + (o) => o.kind === Kind.OPERATION_DEFINITION, + ) as OperationDefinitionNode; + const operationType = rootOperation.operation; + const document = getDocumentString(args.document, print); + const request = args.contextValue.req as express.Request; + const opName = + args.operationName || + rootOperation.name?.value || + 'Anonymous Operation'; + let user: ExceptionHandlerUser | undefined; + + if (tokenService.isTokenPresent(request)) { + try { + const data = await tokenService.validateToken(request); + + user = { + id: data.user?.id, + email: data.user?.email, + }; + } catch {} + } + + return { + onExecuteDone(payload) { + const handleResult: OnExecuteDoneHookResultOnNextHook = ({ + result, + setResult, + }) => { + if (result.errors && result.errors.length > 0) { + const exceptions = result.errors.reduce<{ + filtered: any[]; + unfiltered: any[]; + }>( + (acc, err) => { + // Filter out exceptions that we don't want to be captured by exception handler + if (filterException(err)) { + acc.filtered.push(err); + } else { + acc.unfiltered.push(err); + } + + return acc; + }, + { + filtered: [], + unfiltered: [], + }, + ); + + if (exceptions.unfiltered.length > 0) { + const eventIds = exceptionHandlerService.captureExceptions( + exceptions.unfiltered, + { + operation: { + name: opName, + type: operationType, + }, + document, + user, + }, + ); + + exceptions.unfiltered.map((err, i) => + addEventId(err, eventIds?.[i]), + ); + } + + const concatenatedErrors = [ + ...exceptions.filtered, + ...exceptions.unfiltered, + ]; + const errors = concatenatedErrors.map((err) => { + // Properly convert errors to GraphQLErrors + const graphQLError = convertExceptionToGraphQLError( + err.originalError, + ); + + return graphQLError; + }); + + setResult({ + ...result, + errors, + }); + } + }; + + return handleStreamOrSingleExecutionResult(payload, handleResult); + }, + }; + }, + }; +}; diff --git a/packages/twenty-server/src/integrations/exception-handler/interfaces/exception-handler-driver.interface.ts b/packages/twenty-server/src/integrations/exception-handler/interfaces/exception-handler-driver.interface.ts index f1e23ac28c..4c8af5c208 100644 --- a/packages/twenty-server/src/integrations/exception-handler/interfaces/exception-handler-driver.interface.ts +++ b/packages/twenty-server/src/integrations/exception-handler/interfaces/exception-handler-driver.interface.ts @@ -1,4 +1,10 @@ +import { ExceptionHandlerOptions } from './exception-handler-options.interface'; +import { ExceptionHandlerUser } from './exception-handler-user.interface'; + export interface ExceptionHandlerDriverInterface { - captureException(exception: unknown): void; - captureMessage(message: string): void; + captureExceptions( + exceptions: ReadonlyArray, + options?: ExceptionHandlerOptions, + ): string[]; + captureMessage(message: string, user?: ExceptionHandlerUser): void; } diff --git a/packages/twenty-server/src/integrations/exception-handler/interfaces/exception-handler-options.interface.ts b/packages/twenty-server/src/integrations/exception-handler/interfaces/exception-handler-options.interface.ts new file mode 100644 index 0000000000..7cb267d4b8 --- /dev/null +++ b/packages/twenty-server/src/integrations/exception-handler/interfaces/exception-handler-options.interface.ts @@ -0,0 +1,12 @@ +import { OperationTypeNode } from 'graphql'; + +import { ExceptionHandlerUser } from './exception-handler-user.interface'; + +export interface ExceptionHandlerOptions { + operation?: { + type: OperationTypeNode; + name: string; + }; + document?: string; + user?: ExceptionHandlerUser; +} diff --git a/packages/twenty-server/src/integrations/exception-handler/interfaces/exception-handler-user.interface.ts b/packages/twenty-server/src/integrations/exception-handler/interfaces/exception-handler-user.interface.ts new file mode 100644 index 0000000000..eec20ddfe1 --- /dev/null +++ b/packages/twenty-server/src/integrations/exception-handler/interfaces/exception-handler-user.interface.ts @@ -0,0 +1,6 @@ +export interface ExceptionHandlerUser { + id?: string; + ipAddress?: string; + email?: string; + username?: string; +} diff --git a/packages/twenty-server/src/main.ts b/packages/twenty-server/src/main.ts index 56effa3931..ecccb07e8b 100644 --- a/packages/twenty-server/src/main.ts +++ b/packages/twenty-server/src/main.ts @@ -5,6 +5,7 @@ import * as bodyParser from 'body-parser'; import { graphqlUploadExpress } from 'graphql-upload'; import bytes from 'bytes'; import { useContainer } from 'class-validator'; +import '@sentry/tracing'; import { AppModule } from './app.module'; @@ -15,14 +16,16 @@ import { EnvironmentService } from './integrations/environment/environment.servi const bootstrap = async () => { const app = await NestFactory.create(AppModule, { cors: true, - logger: process.env.DEBUG_MODE - ? ['error', 'warn', 'log', 'verbose', 'debug'] - : ['error', 'warn', 'log'], + bufferLogs: true, }); + const logger = app.get(LoggerService); // Apply class-validator container so that we can use injection in validators useContainer(app.select(AppModule), { fallbackOnErrors: true }); + // Use our logger + app.useLogger(logger); + // Apply validation pipes globally app.useGlobalPipes( new ValidationPipe({ diff --git a/packages/twenty-server/src/metadata/metadata.module-factory.ts b/packages/twenty-server/src/metadata/metadata.module-factory.ts index 1753478432..8e76867d11 100644 --- a/packages/twenty-server/src/metadata/metadata.module-factory.ts +++ b/packages/twenty-server/src/metadata/metadata.module-factory.ts @@ -1,17 +1,17 @@ import { YogaDriverConfig } from '@graphql-yoga/nestjs'; -import { GraphQLError } from 'graphql'; import GraphQLJSON from 'graphql-type-json'; -import { maskError } from 'graphql-yoga'; -import { handleExceptionAndConvertToGraphQLError } from 'src/filters/utils/global-exception-handler.util'; +import { TokenService } from 'src/core/auth/services/token.service'; import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; +import { useExceptionHandler } from 'src/integrations/exception-handler/hooks/use-exception-handler.hook'; import { MetadataModule } from 'src/metadata/metadata.module'; import { renderApolloPlayground } from 'src/workspace/utils/render-apollo-playground.util'; export const metadataModuleFactory = async ( environmentService: EnvironmentService, exceptionHandlerService: ExceptionHandlerService, + tokenService: TokenService, ): Promise => { const config: YogaDriverConfig = { context: ({ req }) => ({ req }), @@ -21,20 +21,13 @@ export const metadataModuleFactory = async ( return renderApolloPlayground({ path: 'metadata' }); }, resolvers: { JSON: GraphQLJSON }, - plugins: [], + plugins: [ + useExceptionHandler({ + exceptionHandlerService, + tokenService, + }), + ], path: '/metadata', - maskedErrors: { - maskError(error: GraphQLError, message, isDev) { - if (error.originalError) { - return handleExceptionAndConvertToGraphQLError( - error.originalError, - exceptionHandlerService, - ); - } - - return maskError(error, message, isDev); - }, - }, }; if (environmentService.isDebugMode()) { diff --git a/packages/twenty-server/src/metadata/metadata.module.ts b/packages/twenty-server/src/metadata/metadata.module.ts index 27692ccffc..ff7fac4c37 100644 --- a/packages/twenty-server/src/metadata/metadata.module.ts +++ b/packages/twenty-server/src/metadata/metadata.module.ts @@ -6,8 +6,10 @@ import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs'; import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module'; import { metadataModuleFactory } from 'src/metadata/metadata.module-factory'; -import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; import { EnvironmentService } from 'src/integrations/environment/environment.service'; +import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; +import { TokenService } from 'src/core/auth/services/token.service'; +import { AuthModule } from 'src/core/auth/auth.module'; import { DataSourceModule } from './data-source/data-source.module'; import { FieldMetadataModule } from './field-metadata/field-metadata.module'; @@ -18,7 +20,8 @@ import { RelationMetadataModule } from './relation-metadata/relation-metadata.mo GraphQLModule.forRootAsync({ driver: YogaDriver, useFactory: metadataModuleFactory, - inject: [EnvironmentService, ExceptionHandlerService], + imports: [AuthModule], + inject: [EnvironmentService, ExceptionHandlerService, TokenService], }), DataSourceModule, FieldMetadataModule, diff --git a/packages/twenty-server/src/queue-worker.ts b/packages/twenty-server/src/queue-worker.ts index 5da66f6821..e057f2b1b9 100644 --- a/packages/twenty-server/src/queue-worker.ts +++ b/packages/twenty-server/src/queue-worker.ts @@ -11,20 +11,45 @@ import { MessageQueueService } from 'src/integrations/message-queue/services/mes import { getJobClassName } from 'src/integrations/message-queue/utils/get-job-class-name.util'; import { QueueWorkerModule } from 'src/queue-worker.module'; +import { LoggerService } from './integrations/logger/logger.service'; +import { ExceptionHandlerService } from './integrations/exception-handler/exception-handler.service'; +import { filterException } from './filters/utils/global-exception-handler.util'; + async function bootstrap() { - const app = await NestFactory.createApplicationContext(QueueWorkerModule); + let exceptionHandlerService: ExceptionHandlerService | undefined; + let loggerService: LoggerService | undefined; - for (const queueName of Object.values(MessageQueue)) { - const messageQueueService: MessageQueueService = app.get(queueName); - - await messageQueueService.work(async (jobData: MessageQueueJobData) => { - const jobClassName = getJobClassName(jobData.name); - const job: MessageQueueJob = app - .select(JobsModule) - .get(jobClassName, { strict: true }); - - await job.handle(jobData.data); + try { + const app = await NestFactory.createApplicationContext(QueueWorkerModule, { + bufferLogs: true, }); + + loggerService = app.get(LoggerService); + exceptionHandlerService = app.get(ExceptionHandlerService); + + // Inject our logger + app.useLogger(loggerService!); + + for (const queueName of Object.values(MessageQueue)) { + const messageQueueService: MessageQueueService = app.get(queueName); + + await messageQueueService.work(async (jobData: MessageQueueJobData) => { + const jobClassName = getJobClassName(jobData.name); + const job: MessageQueueJob = app + .select(JobsModule) + .get(jobClassName, { strict: true }); + + await job.handle(jobData.data); + }); + } + } catch (err) { + loggerService?.error(err?.message, err?.name); + + if (!filterException(err)) { + exceptionHandlerService?.captureExceptions([err]); + } + + throw err; } } bootstrap(); diff --git a/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts index 3e455a39aa..bab5862d60 100644 --- a/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts @@ -35,7 +35,6 @@ import { } from 'src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job'; import { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util'; import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; -import { handleExceptionAndConvertToGraphQLError } from 'src/filters/utils/global-exception-handler.util'; import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util'; import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-optionts.interface'; @@ -64,37 +63,28 @@ export class WorkspaceQueryRunnerService { args: FindManyResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise | undefined> { - try { - const { workspaceId, objectMetadataItem } = options; - const start = performance.now(); + const { workspaceId, objectMetadataItem } = options; + const start = performance.now(); - const query = await this.workspaceQueryBuilderFactory.findMany( - args, - options, - ); + const query = await this.workspaceQueryBuilderFactory.findMany( + args, + options, + ); - const result = await this.execute(query, workspaceId); - const end = performance.now(); + const result = await this.execute(query, workspaceId); + const end = performance.now(); - console.log( - `query time: ${end - start} ms on query ${ - options.objectMetadataItem.nameSingular - }`, - ); + console.log( + `query time: ${end - start} ms on query ${ + options.objectMetadataItem.nameSingular + }`, + ); - return this.parseResult>( - result, - objectMetadataItem, - '', - ); - } catch (exception) { - const error = handleExceptionAndConvertToGraphQLError( - exception, - this.exceptionHandlerService, - ); - - return Promise.reject(error); - } + return this.parseResult>( + result, + objectMetadataItem, + '', + ); } async findOne< @@ -104,181 +94,138 @@ export class WorkspaceQueryRunnerService { args: FindOneResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise { - try { - if (!args.filter || Object.keys(args.filter).length === 0) { - throw new BadRequestException('Missing filter argument'); - } - const { workspaceId, objectMetadataItem } = options; - const query = await this.workspaceQueryBuilderFactory.findOne( - args, - options, - ); - const result = await this.execute(query, workspaceId); - const parsedResult = this.parseResult>( - result, - objectMetadataItem, - '', - ); - - return parsedResult?.edges?.[0]?.node; - } catch (exception) { - const error = handleExceptionAndConvertToGraphQLError( - exception, - this.exceptionHandlerService, - ); - - return Promise.reject(error); + if (!args.filter || Object.keys(args.filter).length === 0) { + throw new BadRequestException('Missing filter argument'); } + const { workspaceId, objectMetadataItem } = options; + const query = await this.workspaceQueryBuilderFactory.findOne( + args, + options, + ); + const result = await this.execute(query, workspaceId); + const parsedResult = this.parseResult>( + result, + objectMetadataItem, + '', + ); + + return parsedResult?.edges?.[0]?.node; } async createMany( args: CreateManyResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise { - try { - const { workspaceId, objectMetadataItem } = options; - const query = await this.workspaceQueryBuilderFactory.createMany( - args, - options, - ); + const { workspaceId, objectMetadataItem } = options; + const query = await this.workspaceQueryBuilderFactory.createMany( + args, + options, + ); - const result = await this.execute(query, workspaceId); + const result = await this.execute(query, workspaceId); - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'insertInto', - )?.records; + const parsedResults = this.parseResult>( + result, + objectMetadataItem, + 'insertInto', + )?.records; - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.create, - options, - ); + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.create, + options, + ); - return parsedResults; - } catch (exception) { - const error = handleExceptionAndConvertToGraphQLError( - exception, - this.exceptionHandlerService, - ); - - return Promise.reject(error); - } + return parsedResults; } async createOne( args: CreateOneResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise { - const results = await this.createMany({ data: [args.data] }, options); + await this.createMany({ data: [args.data] }, options); - return results?.[0]; + throw new BadRequestException('Not implemented'); + + // return results?.[0]; } async updateOne( args: UpdateOneResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise { - try { - const { workspaceId, objectMetadataItem } = options; - const query = await this.workspaceQueryBuilderFactory.updateOne( - args, - options, - ); - const result = await this.execute(query, workspaceId); + const { workspaceId, objectMetadataItem } = options; + const query = await this.workspaceQueryBuilderFactory.updateOne( + args, + options, + ); + const result = await this.execute(query, workspaceId); - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'update', - )?.records; + const parsedResults = this.parseResult>( + result, + objectMetadataItem, + 'update', + )?.records; - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.update, - options, - ); + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.update, + options, + ); - return parsedResults?.[0]; - } catch (exception) { - const error = handleExceptionAndConvertToGraphQLError( - exception, - this.exceptionHandlerService, - ); - - return Promise.reject(error); - } + return parsedResults?.[0]; } async deleteOne( args: DeleteOneResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise { - try { - const { workspaceId, objectMetadataItem } = options; - const query = await this.workspaceQueryBuilderFactory.deleteOne( - args, - options, - ); - const result = await this.execute(query, workspaceId); + const { workspaceId, objectMetadataItem } = options; + const query = await this.workspaceQueryBuilderFactory.deleteOne( + args, + options, + ); + const result = await this.execute(query, workspaceId); - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'deleteFrom', - )?.records; + const parsedResults = this.parseResult>( + result, + objectMetadataItem, + 'deleteFrom', + )?.records; - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.delete, - options, - ); + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.delete, + options, + ); - return parsedResults?.[0]; - } catch (exception) { - const error = handleExceptionAndConvertToGraphQLError( - exception, - this.exceptionHandlerService, - ); - - return Promise.reject(error); - } + return parsedResults?.[0]; } async updateMany( args: UpdateManyResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise { - try { - const { workspaceId, objectMetadataItem } = options; - const query = await this.workspaceQueryBuilderFactory.updateMany( - args, - options, - ); - const result = await this.execute(query, workspaceId); + const { workspaceId, objectMetadataItem } = options; + const query = await this.workspaceQueryBuilderFactory.updateMany( + args, + options, + ); + const result = await this.execute(query, workspaceId); - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'update', - )?.records; + const parsedResults = this.parseResult>( + result, + objectMetadataItem, + 'update', + )?.records; - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.update, - options, - ); + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.update, + options, + ); - return parsedResults; - } catch (exception) { - const error = handleExceptionAndConvertToGraphQLError( - exception, - this.exceptionHandlerService, - ); - - return Promise.reject(error); - } + return parsedResults; } async deleteMany< @@ -288,35 +235,26 @@ export class WorkspaceQueryRunnerService { args: DeleteManyResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise { - try { - const { workspaceId, objectMetadataItem } = options; - const query = await this.workspaceQueryBuilderFactory.deleteMany( - args, - options, - ); - const result = await this.execute(query, workspaceId); + const { workspaceId, objectMetadataItem } = options; + const query = await this.workspaceQueryBuilderFactory.deleteMany( + args, + options, + ); + const result = await this.execute(query, workspaceId); - const parsedResults = this.parseResult>( - result, - objectMetadataItem, - 'deleteFrom', - )?.records; + const parsedResults = this.parseResult>( + result, + objectMetadataItem, + 'deleteFrom', + )?.records; - await this.triggerWebhooks( - parsedResults, - CallWebhookJobsJobOperation.delete, - options, - ); + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.delete, + options, + ); - return parsedResults; - } catch (exception) { - const error = handleExceptionAndConvertToGraphQLError( - exception, - this.exceptionHandlerService, - ); - - return Promise.reject(error); - } + return parsedResults; } async execute( diff --git a/yarn.lock b/yarn.lock index fbbc23590f..3b4afd2738 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11177,6 +11177,17 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/tracing@npm:7.98.0": + version: 7.98.0 + resolution: "@sentry-internal/tracing@npm:7.98.0" + dependencies: + "@sentry/core": "npm:7.98.0" + "@sentry/types": "npm:7.98.0" + "@sentry/utils": "npm:7.98.0" + checksum: f2547315376e3bf6dc2e06c823e67d3816260f9ee84b45e8bd1db4b73b9388b48afaf69d3bfec5772cf94b898550494a91301bd3b48e12eb8f54da8529b81ca0 + languageName: node + linkType: hard + "@sentry/browser@npm:6.19.7": version: 6.19.7 resolution: "@sentry/browser@npm:6.19.7" @@ -11236,6 +11247,16 @@ __metadata: languageName: node linkType: hard +"@sentry/core@npm:7.98.0": + version: 7.98.0 + resolution: "@sentry/core@npm:7.98.0" + dependencies: + "@sentry/types": "npm:7.98.0" + "@sentry/utils": "npm:7.98.0" + checksum: e5098bd47a1d05ca888134cc7d0d4ec1aabc51f319deba9f5d83c41c62927347d4b08539cc51fd3e3f6e242b62c8869ea89c468f80610194422c97d2015cdb0a + languageName: node + linkType: hard + "@sentry/hub@npm:6.19.7": version: 6.19.7 resolution: "@sentry/hub@npm:6.19.7" @@ -11271,6 +11292,18 @@ __metadata: languageName: node linkType: hard +"@sentry/node@npm:^7.98.0": + version: 7.98.0 + resolution: "@sentry/node@npm:7.98.0" + dependencies: + "@sentry-internal/tracing": "npm:7.98.0" + "@sentry/core": "npm:7.98.0" + "@sentry/types": "npm:7.98.0" + "@sentry/utils": "npm:7.98.0" + checksum: 4de5dddd60937b0b72781d3f46cf3f075456377a8eb807425c18d4ea53a98c13b055d6b64f17ebcd7cb7d881111c31d855ad964591e4fcc804e6d729ad2503b3 + languageName: node + linkType: hard + "@sentry/profiling-node@npm:^1.3.4": version: 1.3.4 resolution: "@sentry/profiling-node@npm:1.3.4" @@ -11337,6 +11370,15 @@ __metadata: languageName: node linkType: hard +"@sentry/tracing@npm:^7.98.0": + version: 7.98.0 + resolution: "@sentry/tracing@npm:7.98.0" + dependencies: + "@sentry-internal/tracing": "npm:7.98.0" + checksum: 678d7b023138bb3fa7bb95231ac9671e97f2b2fe1b03719dda021a0545d5345fbeb633d3ca4f7fba49ccf6e0534c3a6d1d62e03f32875ae13d083270257fe4fa + languageName: node + linkType: hard + "@sentry/types@npm:6.19.7": version: 6.19.7 resolution: "@sentry/types@npm:6.19.7" @@ -11358,6 +11400,13 @@ __metadata: languageName: node linkType: hard +"@sentry/types@npm:7.98.0": + version: 7.98.0 + resolution: "@sentry/types@npm:7.98.0" + checksum: eafb7ff6a645a40f18cd35e6db08daae7aa86331c208f5b99e774cf2345eb4cb1c58a580d98dde0deb7b51c658fb1621e7f1d702772cb49bf8bfb82054228e9b + languageName: node + linkType: hard + "@sentry/utils@npm:6.19.7": version: 6.19.7 resolution: "@sentry/utils@npm:6.19.7" @@ -11386,6 +11435,15 @@ __metadata: languageName: node linkType: hard +"@sentry/utils@npm:7.98.0": + version: 7.98.0 + resolution: "@sentry/utils@npm:7.98.0" + dependencies: + "@sentry/types": "npm:7.98.0" + checksum: c42148f32377dde69dd4bbe63ab7ac89a7e594f40ecd8bb3748675533f7d27112535cca4aa8103298ddb9a884d961fd50eced1040acbaa832141fcc66225d3e7 + languageName: node + linkType: hard + "@sideway/address@npm:^4.1.3": version: 4.1.4 resolution: "@sideway/address@npm:4.1.4" @@ -43158,8 +43216,9 @@ __metadata: "@ptc-org/nestjs-query-typeorm": "npm:4.2.1-alpha.2" "@react-email/components": "npm:0.0.12" "@react-email/render": "npm:0.0.10" - "@sentry/node": "npm:^7.66.0" + "@sentry/node": "npm:^7.98.0" "@sentry/profiling-node": "npm:^1.3.4" + "@sentry/tracing": "npm:^7.98.0" "@types/lodash.isempty": "npm:^4.4.7" "@types/lodash.isobject": "npm:^3.0.7" "@types/lodash.omit": "npm:^4.5.9"