diff --git a/packages/twenty-server/src/app.module.ts b/packages/twenty-server/src/app.module.ts index f422d2026..a0f5d2eb3 100644 --- a/packages/twenty-server/src/app.module.ts +++ b/packages/twenty-server/src/app.module.ts @@ -19,6 +19,7 @@ import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graph import { GraphQLConfigService } from 'src/engine/api/graphql/graphql-config/graphql-config.service'; import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module'; import { RestApiModule } from 'src/engine/api/rest/rest-api.module'; +import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { GraphQLHydrateRequestFromTokenMiddleware } from 'src/engine/middlewares/graphql-hydrate-request-from-token.middleware'; @@ -50,7 +51,7 @@ const MIGRATED_REST_METHODS = [ }), GraphQLModule.forRootAsync({ driver: YogaDriver, - imports: [GraphQLConfigModule], + imports: [GraphQLConfigModule, MetricsModule], useClass: GraphQLConfigService, }), TwentyORMModule, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts index d426553c8..1d36c2a41 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts @@ -21,6 +21,7 @@ import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { useSentryTracing } from 'src/engine/core-modules/exception-handler/hooks/use-sentry-tracing'; import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook'; +import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -40,6 +41,7 @@ export class GraphQLConfigService private readonly exceptionHandlerService: ExceptionHandlerService, private readonly twentyConfigService: TwentyConfigService, private readonly moduleRef: ModuleRef, + private readonly metricsService: MetricsService, ) {} createGqlOptions(): YogaDriverConfig { @@ -54,6 +56,7 @@ export class GraphQLConfigService }, }), useGraphQLErrorHandlerHook({ + metricsService: this.metricsService, exceptionHandlerService: this.exceptionHandlerService, }), ]; diff --git a/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts b/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts index d9962cdb0..71ad75959 100644 --- a/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts @@ -7,6 +7,8 @@ import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graph import { metadataModuleFactory } from 'src/engine/api/graphql/metadata.module-factory'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; +import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; +import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { DataloaderModule } from 'src/engine/dataloaders/dataloader.module'; import { DataloaderService } from 'src/engine/dataloaders/dataloader.service'; @@ -19,12 +21,13 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor GraphQLModule.forRootAsync({ driver: YogaDriver, useFactory: metadataModuleFactory, - imports: [GraphQLConfigModule, DataloaderModule], + imports: [GraphQLConfigModule, DataloaderModule, MetricsModule], inject: [ TwentyConfigService, ExceptionHandlerService, DataloaderService, CacheStorageNamespace.EngineWorkspace, + MetricsService, ], }), MetadataEngineModule, diff --git a/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts b/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts index 429e677fd..6f5abd9e4 100644 --- a/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/metadata.module-factory.ts @@ -9,6 +9,7 @@ import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphq import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook'; +import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { DataloaderService } from 'src/engine/dataloaders/dataloader.service'; import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util'; @@ -18,6 +19,7 @@ export const metadataModuleFactory = async ( exceptionHandlerService: ExceptionHandlerService, dataloaderService: DataloaderService, cacheStorageService: CacheStorageService, + metricsService: MetricsService, ): Promise => { const config: YogaDriverConfig = { autoSchemaFile: true, @@ -35,6 +37,7 @@ export const metadataModuleFactory = async ( }, }), useGraphQLErrorHandlerHook({ + metricsService: metricsService, exceptionHandlerService, }), useCachedMetadata({ diff --git a/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts b/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts index 4bdc9860d..ff2f5d304 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts @@ -1,8 +1,8 @@ import { - OnExecuteDoneHookResultOnNextHook, - Plugin, getDocumentString, handleStreamOrSingleExecutionResult, + OnExecuteDoneHookResultOnNextHook, + Plugin, } from '@envelop/core'; import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql'; @@ -13,8 +13,14 @@ import { generateGraphQLErrorFromError } from 'src/engine/core-modules/graphql/u import { BaseGraphQLError, convertGraphQLErrorToBaseGraphQLError, + ErrorCode, } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { shouldCaptureException } from 'src/engine/utils/global-exception-handler.util'; +import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; +import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; +import { + graphQLErrorCodesToFilter, + shouldCaptureException, +} from 'src/engine/utils/global-exception-handler.util'; const DEFAULT_EVENT_ID_KEY = 'exceptionEventId'; const SCHEMA_VERSION_HEADER = 'x-schema-version'; @@ -22,6 +28,8 @@ const SCHEMA_MISMATCH_ERROR = 'Your workspace has been updated with a new data model. Please refresh the page.'; type GraphQLErrorHandlerHookOptions = { + metricsService: MetricsService; + /** * The exception handler service to use. */ @@ -81,6 +89,10 @@ export const useGraphQLErrorHandlerHook = < setResult, }) => { if (!result.errors || result.errors.length === 0) { + options.metricsService.incrementCounter({ + key: MetricsKeys.GraphqlOperation200, + }); + return; } @@ -105,6 +117,48 @@ export const useGraphQLErrorHandlerHook = < return originalError; }); + // Error metrics + const codeToMetricKey: Partial> = { + [ErrorCode.UNAUTHENTICATED]: MetricsKeys.GraphqlOperation401, + [ErrorCode.FORBIDDEN]: MetricsKeys.GraphqlOperation403, + [ErrorCode.NOT_FOUND]: MetricsKeys.GraphqlOperation404, + [ErrorCode.INTERNAL_SERVER_ERROR]: + MetricsKeys.GraphqlOperation500, + }; + + const statusToMetricKey: Record = { + 400: MetricsKeys.GraphqlOperation400, + 401: MetricsKeys.GraphqlOperation401, + 403: MetricsKeys.GraphqlOperation403, + 404: MetricsKeys.GraphqlOperation404, + 500: MetricsKeys.GraphqlOperation500, + }; + + processedErrors.forEach((error) => { + let metricKey: MetricsKeys | undefined; + + if (error instanceof BaseGraphQLError) { + const code = error.extensions?.code as ErrorCode; + + metricKey = codeToMetricKey[code]; + if (!metricKey && graphQLErrorCodesToFilter.includes(code)) { + metricKey = MetricsKeys.GraphqlOperation400; + } + } else if (error instanceof GraphQLError) { + const status = error.extensions?.http?.status as number; + + metricKey = statusToMetricKey[status]; + } + + if (metricKey) { + options.metricsService.incrementCounter({ key: metricKey }); + } else { + options.metricsService.incrementCounter({ + key: MetricsKeys.GraphqlOperationUnknown, + }); + } + }); + // Step 2: Send errors to monitoring service (with stack traces) const errorsToCapture = processedErrors.filter( shouldCaptureException, diff --git a/packages/twenty-server/src/engine/core-modules/metrics/metrics.service.ts b/packages/twenty-server/src/engine/core-modules/metrics/metrics.service.ts index 0556c4bbd..eda81959a 100644 --- a/packages/twenty-server/src/engine/core-modules/metrics/metrics.service.ts +++ b/packages/twenty-server/src/engine/core-modules/metrics/metrics.service.ts @@ -15,7 +15,7 @@ export class MetricsService { shouldStoreInCache = true, }: { key: MetricsKeys; - eventId: string; + eventId?: string; shouldStoreInCache?: boolean; }) { //TODO : Define meter name usage in monitoring @@ -24,7 +24,7 @@ export class MetricsService { counter.add(1); - if (shouldStoreInCache) { + if (shouldStoreInCache && eventId) { this.metricsCacheService.updateCounter(key, [eventId]); } } diff --git a/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts b/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts index a2e0fcd7a..179742b04 100644 --- a/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts +++ b/packages/twenty-server/src/engine/core-modules/metrics/types/metrics-keys.type.ts @@ -6,6 +6,13 @@ export enum MetricsKeys { CalendarEventSyncJobFailedInsufficientPermissions = 'calendar-event-sync-job/failed-insufficient-permissions', CalendarEventSyncJobFailedUnknown = 'calendar-event-sync-job/failed-unknown', InvalidCaptcha = 'invalid-captcha', + GraphqlOperation200 = 'graphql-operation/200', + GraphqlOperation400 = 'graphql-operation/400', + GraphqlOperation401 = 'graphql-operation/401', + GraphqlOperation403 = 'graphql-operation/403', + GraphqlOperation404 = 'graphql-operation/404', + GraphqlOperation500 = 'graphql-operation/500', + GraphqlOperationUnknown = 'graphql-operation/unknown', WorkflowRunStartedDatabaseEventTrigger = 'workflow-run/started/database-event-trigger', WorkflowRunStartedCronTrigger = 'workflow-run/started/cron-trigger', WorkflowRunStartedWebhookTrigger = 'workflow-run/started/webhook-trigger',