🩺(project) reload app if front and back unsync

We observe some cases where the frontend and
backend versions can get out of sync, which can
cause issues.
To mitigate this, we want to implement a mechanism
that detects when the frontend and backend
versions are mismatched and triggers a
reload of the application to ensure they are in sync.
This commit is contained in:
Anthony LC
2026-05-07 10:53:06 +02:00
parent 1268bbe5ea
commit 67773ef2d9
8 changed files with 68 additions and 4 deletions

View File

@@ -17,6 +17,7 @@ and this project adheres to
### Fixed
- 🩺(project) reload app if front and back unsync #2276
- 🐛(frontend) fix patch and comments #2273
- 🐛(frontend) interlinking are exported correctly in print mode #2269
- 💬(frontend) add missing link in onboarding description #2233

View File

@@ -2841,6 +2841,7 @@ class ConfigView(drf.views.APIView):
dict_settings[setting] = getattr(settings, setting)
dict_settings["theme_customization"] = self._load_theme_customization()
dict_settings["RELEASE_VERSION"] = settings.RELEASE
return drf.response.Response(dict_settings)

View File

@@ -34,6 +34,7 @@ pytestmark = pytest.mark.django_db
FRONTEND_THEME="test-theme",
MEDIA_BASE_URL="http://testserver/",
POSTHOG_KEY={"id": "132456", "host": "https://eu.i.posthog-test.com"},
RELEASE="1.0.0",
SENTRY_DSN="https://sentry.test/123",
THEME_CUSTOMIZATION_FILE_PATH="",
)
@@ -77,6 +78,7 @@ def test_api_config(is_authenticated):
"LANGUAGE_CODE": "en-us",
"MEDIA_BASE_URL": "http://testserver/",
"POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"},
"RELEASE_VERSION": "1.0.0",
"SENTRY_DSN": "https://sentry.test/123",
"TRASHBIN_CUTOFF_DAYS": 30,
"theme_customization": {},

View File

@@ -1,6 +1,11 @@
import { expect, test } from '@playwright/test';
import { createDoc, getCurrentConfig, verifyDocName } from './utils-common';
import {
createDoc,
getCurrentConfig,
overrideConfig,
verifyDocName,
} from './utils-common';
import { writeInEditor } from './utils-editor';
import { SignIn, expectLoginPage } from './utils-signin';
import { createRootSubPage } from './utils-sub-pages';
@@ -145,6 +150,29 @@ test.describe('Doc Routing', () => {
);
await expect(page).toHaveTitle(/401 Unauthorized - Docs/);
});
test('checks redirect if unsync version', async ({ page }) => {
await overrideConfig(page, {
RELEASE_VERSION: '0.0.0',
});
let counterReload = 0;
await page.route(/.*\/users\/me\/$/, async (route) => {
counterReload += 1;
await route.continue();
});
await page.waitForTimeout(1000);
// The sessionStorage guard should be set to the mismatched backend version.
const reloadVersion = await page.evaluate(() =>
sessionStorage.getItem('reload-version'),
);
expect(reloadVersion).toBe('0.0.0');
// The page should have reloaded once
expect(counterReload).toBe(2);
});
});
test.describe('Doc Routing: Not logged', () => {

View File

@@ -4,6 +4,7 @@ import path from 'path';
import { Locator, Page, TestInfo, expect } from '@playwright/test';
import theme_customization from '../../../../../backend/impress/configuration/theme/default.json';
import { version as packageJsonVersion } from '../../package.json';
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
@@ -40,6 +41,7 @@ export const CONFIG = {
],
LANGUAGE_CODE: 'en-us',
POSTHOG_KEY: {},
RELEASE_VERSION: packageJsonVersion,
SENTRY_DSN: null,
TRASHBIN_CUTOFF_DAYS: 30,
theme_customization,

View File

@@ -2,6 +2,8 @@ const crypto = require('crypto');
const { InjectManifest } = require('workbox-webpack-plugin');
const { version } = require('./package.json');
const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
/** @type {import('next').NextConfig} */
@@ -25,6 +27,7 @@ const nextConfig = {
generateBuildId: () => buildId,
env: {
NEXT_PUBLIC_BUILD_ID: buildId,
NEXT_PUBLIC_APP_VERSION: version,
},
/**
* In dev mode, Next.js doesn't use Webpack, but Turbopack.

View File

@@ -85,6 +85,32 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
});
}, [conf?.CRISP_WEBSITE_ID]);
useEffect(() => {
const frontendVersion = process.env.NEXT_PUBLIC_APP_VERSION;
if (
!conf?.RELEASE_VERSION ||
!frontendVersion ||
conf.RELEASE_VERSION === frontendVersion
) {
return;
}
// Avoid infinite reload loops: only reload once per backend version
const RELOAD_VERSION_KEY = 'reload-version';
try {
const reloadedForVersion = sessionStorage.getItem(RELOAD_VERSION_KEY);
if (reloadedForVersion === conf.RELEASE_VERSION) {
return;
}
sessionStorage.setItem(RELOAD_VERSION_KEY, conf.RELEASE_VERSION);
window.location.reload();
} catch {
console.warn('Failed to access sessionStorage for version reload logic');
}
}, [conf?.RELEASE_VERSION]);
if (!conf) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">

View File

@@ -57,6 +57,7 @@ export interface ConfigResponse {
LANGUAGE_CODE: string;
MEDIA_BASE_URL?: string;
POSTHOG_KEY?: PostHogConf;
RELEASE_VERSION: string;
SENTRY_DSN?: string;
TRASHBIN_CUTOFF_DAYS?: number;
theme_customization?: ThemeCustomization;
@@ -94,13 +95,13 @@ export const KEY_CONFIG = 'config';
export function useConfig() {
const cachedData = getCachedTranslation();
const oneHour = 1000 * 60 * 60;
const staleTime = 1000 * 60 * 5;
return useQuery<ConfigResponse, APIError, ConfigResponse>({
queryKey: [KEY_CONFIG],
queryFn: () => getConfig(),
initialData: cachedData,
staleTime: oneHour,
initialDataUpdatedAt: Date.now() - oneHour, // Force initial data to be considered stale
staleTime,
initialDataUpdatedAt: Date.now() - staleTime, // Force initial data to be considered stale
});
}