App version mismatch handling between frontend and backend (#13368)

https://github.com/user-attachments/assets/d153f177-4d70-4ec6-8693-15413e550938
This commit is contained in:
Abdul Rahman
2025-07-24 01:37:02 +05:30
committed by GitHub
parent ed36b19af7
commit 30dd457313
14 changed files with 113 additions and 3 deletions
@@ -6,6 +6,8 @@ import {
} from '@envelop/core';
import { t } from '@lingui/core/macro';
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
import semver from 'semver';
import { isDefined } from 'twenty-shared/utils';
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface';
@@ -18,6 +20,7 @@ import {
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import {
graphQLErrorCodesToFilter,
shouldCaptureException,
@@ -26,6 +29,9 @@ import {
const DEFAULT_EVENT_ID_KEY = 'exceptionEventId';
const SCHEMA_VERSION_HEADER = 'x-schema-version';
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';
type GraphQLErrorHandlerHookOptions = {
metricsService: MetricsService;
@@ -34,6 +40,8 @@ type GraphQLErrorHandlerHookOptions = {
* The exception handler service to use.
*/
exceptionHandlerService: ExceptionHandlerService;
twentyConfigService: TwentyConfigService;
/**
* The key of the event id in the error's extension. `null` to disable.
* @default exceptionEventId
@@ -230,17 +238,55 @@ export const useGraphQLErrorHandlerHook = <
const headers = context.req.headers;
const currentMetadataVersion = context.req.workspaceMetadataVersion;
const requestMetadataVersion = headers[SCHEMA_VERSION_HEADER];
const backendAppVersion =
options.twentyConfigService.get('APP_VERSION');
const appVersionHeaderValue = headers[APP_VERSION_HEADER];
const frontEndAppVersion =
appVersionHeaderValue && Array.isArray(appVersionHeaderValue)
? appVersionHeaderValue[0]
: appVersionHeaderValue;
if (
requestMetadataVersion &&
requestMetadataVersion !== `${currentMetadataVersion}`
) {
options.metricsService.incrementCounter({
key: MetricsKeys.SchemaVersionMismatch,
});
throw new GraphQLError(SCHEMA_MISMATCH_ERROR, {
extensions: {
userFriendlyMessage: t`Your workspace has been updated with a new data model. Please refresh the page.`,
},
});
}
if (
!frontEndAppVersion ||
!backendAppVersion ||
!semver.valid(frontEndAppVersion) ||
!semver.valid(backendAppVersion)
) {
return;
}
const frontEndMajor = semver.parse(frontEndAppVersion)?.major;
const backendMajor = semver.parse(backendAppVersion)?.major;
if (
isDefined(frontEndMajor) &&
isDefined(backendMajor) &&
frontEndMajor < backendMajor
) {
options.metricsService.incrementCounter({
key: MetricsKeys.AppVersionMismatch,
});
throw new GraphQLError(APP_VERSION_MISMATCH_ERROR, {
extensions: {
code: APP_VERSION_MISMATCH_CODE,
userFriendlyMessage: t`Your app version is out of date. Please refresh the page to continue.`,
},
});
}
}
},
};