Add graphql queries error codes metrics (#12833)
## Context Added to the existing useGraphQLErrorHandlerHook yoga hook to increment metrics after all query executions based on their error codes. I originally wanted to create a new useMetrics hook but most of the error handling was done in useGraphQLErrorHandlerHook so we decided to keep it there for now. <img width="1310" alt="Screenshot 2025-06-24 at 15 58 26" src="https://github.com/user-attachments/assets/498d3754-851a-4051-a5c2-23ac8253aa6a" />
This commit is contained in:
@ -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 { GraphQLConfigService } from 'src/engine/api/graphql/graphql-config/graphql-config.service';
|
||||||
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
|
import { MetadataGraphQLApiModule } from 'src/engine/api/graphql/metadata-graphql-api.module';
|
||||||
import { RestApiModule } from 'src/engine/api/rest/rest-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 { 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 { 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';
|
import { GraphQLHydrateRequestFromTokenMiddleware } from 'src/engine/middlewares/graphql-hydrate-request-from-token.middleware';
|
||||||
@ -50,7 +51,7 @@ const MIGRATED_REST_METHODS = [
|
|||||||
}),
|
}),
|
||||||
GraphQLModule.forRootAsync<YogaDriverConfig>({
|
GraphQLModule.forRootAsync<YogaDriverConfig>({
|
||||||
driver: YogaDriver,
|
driver: YogaDriver,
|
||||||
imports: [GraphQLConfigModule],
|
imports: [GraphQLConfigModule, MetricsModule],
|
||||||
useClass: GraphQLConfigService,
|
useClass: GraphQLConfigService,
|
||||||
}),
|
}),
|
||||||
TwentyORMModule,
|
TwentyORMModule,
|
||||||
|
|||||||
@ -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 { 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 { 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 { 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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
@ -40,6 +41,7 @@ export class GraphQLConfigService
|
|||||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||||
private readonly twentyConfigService: TwentyConfigService,
|
private readonly twentyConfigService: TwentyConfigService,
|
||||||
private readonly moduleRef: ModuleRef,
|
private readonly moduleRef: ModuleRef,
|
||||||
|
private readonly metricsService: MetricsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
createGqlOptions(): YogaDriverConfig {
|
createGqlOptions(): YogaDriverConfig {
|
||||||
@ -54,6 +56,7 @@ export class GraphQLConfigService
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
useGraphQLErrorHandlerHook({
|
useGraphQLErrorHandlerHook({
|
||||||
|
metricsService: this.metricsService,
|
||||||
exceptionHandlerService: this.exceptionHandlerService,
|
exceptionHandlerService: this.exceptionHandlerService,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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 { metadataModuleFactory } from 'src/engine/api/graphql/metadata.module-factory';
|
||||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
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 { 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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import { DataloaderModule } from 'src/engine/dataloaders/dataloader.module';
|
import { DataloaderModule } from 'src/engine/dataloaders/dataloader.module';
|
||||||
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
|
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
|
||||||
@ -19,12 +21,13 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor
|
|||||||
GraphQLModule.forRootAsync<YogaDriverConfig>({
|
GraphQLModule.forRootAsync<YogaDriverConfig>({
|
||||||
driver: YogaDriver,
|
driver: YogaDriver,
|
||||||
useFactory: metadataModuleFactory,
|
useFactory: metadataModuleFactory,
|
||||||
imports: [GraphQLConfigModule, DataloaderModule],
|
imports: [GraphQLConfigModule, DataloaderModule, MetricsModule],
|
||||||
inject: [
|
inject: [
|
||||||
TwentyConfigService,
|
TwentyConfigService,
|
||||||
ExceptionHandlerService,
|
ExceptionHandlerService,
|
||||||
DataloaderService,
|
DataloaderService,
|
||||||
CacheStorageNamespace.EngineWorkspace,
|
CacheStorageNamespace.EngineWorkspace,
|
||||||
|
MetricsService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
MetadataEngineModule,
|
MetadataEngineModule,
|
||||||
|
|||||||
@ -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 { 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 { 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 { 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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
|
import { DataloaderService } from 'src/engine/dataloaders/dataloader.service';
|
||||||
import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util';
|
import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util';
|
||||||
@ -18,6 +19,7 @@ export const metadataModuleFactory = async (
|
|||||||
exceptionHandlerService: ExceptionHandlerService,
|
exceptionHandlerService: ExceptionHandlerService,
|
||||||
dataloaderService: DataloaderService,
|
dataloaderService: DataloaderService,
|
||||||
cacheStorageService: CacheStorageService,
|
cacheStorageService: CacheStorageService,
|
||||||
|
metricsService: MetricsService,
|
||||||
): Promise<YogaDriverConfig> => {
|
): Promise<YogaDriverConfig> => {
|
||||||
const config: YogaDriverConfig = {
|
const config: YogaDriverConfig = {
|
||||||
autoSchemaFile: true,
|
autoSchemaFile: true,
|
||||||
@ -35,6 +37,7 @@ export const metadataModuleFactory = async (
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
useGraphQLErrorHandlerHook({
|
useGraphQLErrorHandlerHook({
|
||||||
|
metricsService: metricsService,
|
||||||
exceptionHandlerService,
|
exceptionHandlerService,
|
||||||
}),
|
}),
|
||||||
useCachedMetadata({
|
useCachedMetadata({
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
OnExecuteDoneHookResultOnNextHook,
|
|
||||||
Plugin,
|
|
||||||
getDocumentString,
|
getDocumentString,
|
||||||
handleStreamOrSingleExecutionResult,
|
handleStreamOrSingleExecutionResult,
|
||||||
|
OnExecuteDoneHookResultOnNextHook,
|
||||||
|
Plugin,
|
||||||
} from '@envelop/core';
|
} from '@envelop/core';
|
||||||
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
|
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
|
||||||
|
|
||||||
@ -13,8 +13,14 @@ import { generateGraphQLErrorFromError } from 'src/engine/core-modules/graphql/u
|
|||||||
import {
|
import {
|
||||||
BaseGraphQLError,
|
BaseGraphQLError,
|
||||||
convertGraphQLErrorToBaseGraphQLError,
|
convertGraphQLErrorToBaseGraphQLError,
|
||||||
|
ErrorCode,
|
||||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
} 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 DEFAULT_EVENT_ID_KEY = 'exceptionEventId';
|
||||||
const SCHEMA_VERSION_HEADER = 'x-schema-version';
|
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.';
|
'Your workspace has been updated with a new data model. Please refresh the page.';
|
||||||
|
|
||||||
type GraphQLErrorHandlerHookOptions = {
|
type GraphQLErrorHandlerHookOptions = {
|
||||||
|
metricsService: MetricsService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The exception handler service to use.
|
* The exception handler service to use.
|
||||||
*/
|
*/
|
||||||
@ -81,6 +89,10 @@ export const useGraphQLErrorHandlerHook = <
|
|||||||
setResult,
|
setResult,
|
||||||
}) => {
|
}) => {
|
||||||
if (!result.errors || result.errors.length === 0) {
|
if (!result.errors || result.errors.length === 0) {
|
||||||
|
options.metricsService.incrementCounter({
|
||||||
|
key: MetricsKeys.GraphqlOperation200,
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +117,48 @@ export const useGraphQLErrorHandlerHook = <
|
|||||||
return originalError;
|
return originalError;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Error metrics
|
||||||
|
const codeToMetricKey: Partial<Record<ErrorCode, MetricsKeys>> = {
|
||||||
|
[ErrorCode.UNAUTHENTICATED]: MetricsKeys.GraphqlOperation401,
|
||||||
|
[ErrorCode.FORBIDDEN]: MetricsKeys.GraphqlOperation403,
|
||||||
|
[ErrorCode.NOT_FOUND]: MetricsKeys.GraphqlOperation404,
|
||||||
|
[ErrorCode.INTERNAL_SERVER_ERROR]:
|
||||||
|
MetricsKeys.GraphqlOperation500,
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusToMetricKey: Record<number, MetricsKeys> = {
|
||||||
|
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)
|
// Step 2: Send errors to monitoring service (with stack traces)
|
||||||
const errorsToCapture = processedErrors.filter(
|
const errorsToCapture = processedErrors.filter(
|
||||||
shouldCaptureException,
|
shouldCaptureException,
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export class MetricsService {
|
|||||||
shouldStoreInCache = true,
|
shouldStoreInCache = true,
|
||||||
}: {
|
}: {
|
||||||
key: MetricsKeys;
|
key: MetricsKeys;
|
||||||
eventId: string;
|
eventId?: string;
|
||||||
shouldStoreInCache?: boolean;
|
shouldStoreInCache?: boolean;
|
||||||
}) {
|
}) {
|
||||||
//TODO : Define meter name usage in monitoring
|
//TODO : Define meter name usage in monitoring
|
||||||
@ -24,7 +24,7 @@ export class MetricsService {
|
|||||||
|
|
||||||
counter.add(1);
|
counter.add(1);
|
||||||
|
|
||||||
if (shouldStoreInCache) {
|
if (shouldStoreInCache && eventId) {
|
||||||
this.metricsCacheService.updateCounter(key, [eventId]);
|
this.metricsCacheService.updateCounter(key, [eventId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,13 @@ export enum MetricsKeys {
|
|||||||
CalendarEventSyncJobFailedInsufficientPermissions = 'calendar-event-sync-job/failed-insufficient-permissions',
|
CalendarEventSyncJobFailedInsufficientPermissions = 'calendar-event-sync-job/failed-insufficient-permissions',
|
||||||
CalendarEventSyncJobFailedUnknown = 'calendar-event-sync-job/failed-unknown',
|
CalendarEventSyncJobFailedUnknown = 'calendar-event-sync-job/failed-unknown',
|
||||||
InvalidCaptcha = 'invalid-captcha',
|
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',
|
WorkflowRunStartedDatabaseEventTrigger = 'workflow-run/started/database-event-trigger',
|
||||||
WorkflowRunStartedCronTrigger = 'workflow-run/started/cron-trigger',
|
WorkflowRunStartedCronTrigger = 'workflow-run/started/cron-trigger',
|
||||||
WorkflowRunStartedWebhookTrigger = 'workflow-run/started/webhook-trigger',
|
WorkflowRunStartedWebhookTrigger = 'workflow-run/started/webhook-trigger',
|
||||||
|
|||||||
Reference in New Issue
Block a user