Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code c138717ff7 chore: improve monitoring for fix: avoid full metadata rebuild on view group pos
I added targeted monitoring-noise reduction and better Sentry filtering/context:

1) Reduced noisy exception capture for expected client-refresh flows
- Added a dedicated GraphQL error code for schema mismatch (`SCHEMA_VERSION_MISMATCH`) where it was previously missing.
- Updated global exception capture filtering to skip Sentry capture for:
  - `APP_VERSION_MISMATCH`
  - `SCHEMA_VERSION_MISMATCH`
These are expected non-server-fault user flows and were creating noisy error events.

2) Improved filterability by workspace in Sentry traces
- Added `workspaceId` tag in GraphQL Sentry tracing hook when available.
This supports operation/workspace drill-down for latency investigations (including `/metadata` slow-operation triage).
2026-05-12 09:23:42 +00:00
Sonarly Claude Code 109178dbc8 fix: avoid full metadata rebuild on view group position updates
https://sonarly.com/issue/36872?type=bug

Kanban stage reorder calls on

Fix: I changed `ViewGroupService.updateMany` to stop routing simple view-group updates through `validateBuildAndRunWorkspaceMigration` (the expensive full metadata migration path).

What changed:
- Kept existing input validation/sanitization by continuing to build `flatViewGroupsToUpdate` via `fromUpdateViewGroupInputToFlatViewGroupToUpdateOrThrow`.
- Replaced the migration call with direct sequential `viewGroupRepository.update(...)` operations for:
  - `isVisible`
  - `fieldValue`
  - `position`
- Invalidated and recomputed `flatViewGroupMaps` after updates so metadata cache stays consistent.
- Kept output shape identical by still returning DTOs from recomputed flat maps.

Why:
`UpdateManyViewGroups` only edits mutable view-group properties. Running a full metadata migration for those updates adds unnecessary validation/build/run/cache-version work, which caused reorder latency spikes (~1s+). This change makes reorder updates lightweight while preserving behavior and response contracts.
2026-05-12 09:23:42 +00:00
4 changed files with 37 additions and 30 deletions
@@ -29,6 +29,10 @@ export const useSentryTracing = <
operation: operationType,
});
if (workspace?.id) {
Sentry.setTag('workspaceId', workspace.id);
}
const scope = Sentry.getCurrentScope();
scope.setTransactionName(transactionName);
@@ -40,6 +40,7 @@ const SCHEMA_MISMATCH_ERROR = 'Schema version mismatch.';
const APP_VERSION_HEADER = 'x-app-version';
const APP_VERSION_MISMATCH_ERROR = 'App version mismatch.';
const APP_VERSION_MISMATCH_CODE = 'APP_VERSION_MISMATCH';
const SCHEMA_VERSION_MISMATCH_CODE = 'SCHEMA_VERSION_MISMATCH';
type GraphQLErrorHandlerHookOptions = {
metricsService: MetricsService;
@@ -286,6 +287,7 @@ export const useGraphQLErrorHandlerHook = <
throw new GraphQLError(SCHEMA_MISMATCH_ERROR, {
extensions: {
code: SCHEMA_VERSION_MISMATCH_CODE,
userFriendlyMessage: i18n._(
msg`Your workspace has been updated with a new data model. Please refresh the page.`,
),
@@ -179,13 +179,6 @@ export class ViewGroupService {
return [];
}
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const { flatViewGroupMaps: existingFlatViewGroupMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -202,30 +195,25 @@ export class ViewGroupService {
}),
);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
for (const flatViewGroupToUpdate of flatViewGroupsToUpdate) {
await this.viewGroupRepository.update(
{
allFlatEntityOperationByMetadataName: {
viewGroup: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: flatViewGroupsToUpdate,
},
},
id: flatViewGroupToUpdate.id,
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
{
isVisible: flatViewGroupToUpdate.isVisible,
fieldValue: flatViewGroupToUpdate.fieldValue,
position: flatViewGroupToUpdate.position,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating view groups',
);
}
await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({
workspaceId,
flatMapsKeys: ['flatViewGroupMaps'],
});
const { flatViewGroupMaps: recomputedExistingFlatViewGroupMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -20,6 +20,11 @@ import {
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { type CustomException } from 'src/utils/custom-exception';
const GRAPHQL_ERROR_CODES_TO_FILTER = [
'APP_VERSION_MISMATCH',
'SCHEMA_VERSION_MISMATCH',
] as const;
const graphQLPredefinedExceptions = {
400: ValidationError,
401: AuthenticationError,
@@ -62,11 +67,19 @@ export const shouldCaptureException = (
exception: Error,
statusCode?: number,
): boolean => {
if (
exception instanceof GraphQLError &&
(exception?.extensions?.http?.status ?? 500) < 500
) {
return false;
if (exception instanceof GraphQLError) {
const graphQLErrorCode = exception.extensions?.code;
if (
typeof graphQLErrorCode === 'string' &&
GRAPHQL_ERROR_CODES_TO_FILTER.includes(graphQLErrorCode)
) {
return false;
}
if ((exception?.extensions?.http?.status ?? 500) < 500) {
return false;
}
}
if (