Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code cb05c538b3 chore: improve monitoring for Missing exception filter causes expected FORBIDDEN
Registered the new `WorkflowQueryValidationGraphqlApiExceptionFilter` on two resolvers that call code paths throwing `WorkflowQueryValidationException`:

1. **`workflow-version-step.resolver.ts`** — handles `UpdateWorkflowVersionStep` (the mutation in this Sentry error)
2. **`workflow-version-edge.resolver.ts`** — handles edge operations that also call `getValidatedDraftWorkflowVersion`

With the filter registered, `WorkflowQueryValidationException` is now converted to a `ForbiddenError` (`BaseGraphQLError` with `ErrorCode.FORBIDDEN`). The `shouldCaptureException` function in the GraphQL error handler hook will now correctly filter this out (since `FORBIDDEN` is in `graphQLErrorCodesToFilter`), eliminating the Sentry noise from this expected validation error.
2026-03-06 10:54:05 +00:00
Sonarly Claude Code 0d151571c8 Missing exception filter causes expected FORBIDDEN validation error to be captured by Sentry
https://sonarly.com/issue/3892?type=bug

The `WorkflowQueryValidationException` thrown by `assertWorkflowVersionIsDraft` has no NestJS exception filter to convert it to a proper GraphQL error, causing it to bypass Sentry's error filtering and be incorrectly reported as an unhandled error. The underlying trigger is a frontend race condition where stale React state sends a step update mutation for a workflow version that is no longer in DRAFT status.

Fix: Created a new `WorkflowQueryValidationGraphqlApiExceptionFilter` that catches `WorkflowQueryValidationException` and converts it to a proper `ForbiddenError` (a `BaseGraphQLError`). This follows the exact same pattern as the three existing workflow exception filters in the codebase.

**Why this fixes the issue:** The `WorkflowQueryValidationException` (thrown by `assertWorkflowVersionIsDraft`) was not caught by any exception filter on the workflow resolvers. Without a filter, the exception bypassed the `shouldCaptureException` logic in the global error handler (which only filters `HttpException`, `GraphQLError`, and `BaseGraphQLError` types), causing every instance to be captured by Sentry as if it were an unhandled server error.

The new filter converts `WorkflowQueryValidationException` → `ForbiddenError` (which extends `BaseGraphQLError` with `ErrorCode.FORBIDDEN`). Since `ErrorCode.FORBIDDEN` is in `graphQLErrorCodesToFilter`, the `shouldCaptureException` function will now correctly return `false`, preventing Sentry capture.

The user-facing behavior remains correct: the `ForbiddenError` is still returned as a GraphQL error with the user-friendly message, so the frontend still receives and handles it properly.

**Note:** The underlying frontend race condition (stale React closure in `useGetUpdatableWorkflowVersionOrThrow`) still exists. This fix correctly classifies the exception as an expected validation error rather than masking it.
2026-03-06 10:54:05 +00:00
3 changed files with 34 additions and 0 deletions
@@ -0,0 +1,30 @@
import { Catch, type ExceptionFilter } from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
WorkflowQueryValidationException,
WorkflowQueryValidationExceptionCode,
} from 'src/modules/workflow/common/exceptions/workflow-query-validation.exception';
export const handleWorkflowQueryValidationException = (
exception: WorkflowQueryValidationException,
) => {
switch (exception.code) {
case WorkflowQueryValidationExceptionCode.FORBIDDEN:
throw new ForbiddenError(exception);
default: {
assertUnreachable(exception.code);
}
}
};
@Catch(WorkflowQueryValidationException)
export class WorkflowQueryValidationGraphqlApiExceptionFilter
implements ExceptionFilter
{
catch(exception: WorkflowQueryValidationException) {
handleWorkflowQueryValidationException(exception);
}
}
@@ -5,6 +5,7 @@ import { PermissionFlagType } from 'twenty-shared/constants';
import { CoreResolver } from 'src/engine/api/graphql/graphql-config/decorators/core-resolver.decorator';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { WorkflowQueryValidationGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-query-validation-graphql-api-exception.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { CreateWorkflowVersionEdgeInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-edge.input';
import { WorkflowVersionStepChangesDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version-step-changes.dto';
@@ -27,6 +28,7 @@ import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workfl
@UseFilters(
PermissionsGraphqlApiExceptionFilter,
PreventNestToAutoLogGraphqlErrorsFilter,
WorkflowQueryValidationGraphqlApiExceptionFilter,
WorkflowVersionEdgeGraphqlApiExceptionFilter,
)
export class WorkflowVersionEdgeResolver {
@@ -7,6 +7,7 @@ import { FeatureFlagKey } from 'twenty-shared/types';
import { CoreResolver } from 'src/engine/api/graphql/graphql-config/decorators/core-resolver.decorator';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { WorkflowQueryValidationGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-query-validation-graphql-api-exception.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step.input';
@@ -41,6 +42,7 @@ import { WorkflowRunnerWorkspaceService } from 'src/modules/workflow/workflow-ru
@UseFilters(
PermissionsGraphqlApiExceptionFilter,
PreventNestToAutoLogGraphqlErrorsFilter,
WorkflowQueryValidationGraphqlApiExceptionFilter,
WorkflowVersionStepGraphqlApiExceptionFilter,
)
export class WorkflowVersionStepResolver {