diff --git a/echo/frontend/src/components/project/ProjectLegalBasisSection.tsx b/echo/frontend/src/components/project/ProjectLegalBasisSection.tsx
index e27646ae..7c83e5b4 100644
--- a/echo/frontend/src/components/project/ProjectLegalBasisSection.tsx
+++ b/echo/frontend/src/components/project/ProjectLegalBasisSection.tsx
@@ -30,7 +30,7 @@ import {
type LegalBasisValue,
} from "@/components/settings/LegalBasisCard";
import { API_BASE_URL } from "@/config";
-import { useI18nNavigate } from "@/hooks/useI18nNavigate";
+import { useLanguage } from "@/hooks/useLanguage";
type LegalSource = "project" | "workspace" | "legacy_user" | "default";
@@ -127,7 +127,7 @@ export const ProjectLegalBasisSection = ({
projectId: string;
}) => {
const { workspaceId } = useParams<{ workspaceId: string }>();
- const navigate = useI18nNavigate();
+ const { language } = useLanguage();
const queryClient = useQueryClient();
const { data: user } = useCurrentUser();
const isDembraneUser = (user?.email ?? "")
@@ -264,13 +264,13 @@ export const ProjectLegalBasisSection = ({
) : (
)}
+ {/* New tab keeps the editor form the host is filling in */}
- navigate(`/w/${workspaceId}/settings/general`)
- }
- style={{ cursor: "pointer" }}
+ href={`/${language}/w/${workspaceId}/settings/general`}
+ target="_blank"
+ rel="noopener noreferrer"
>
Workspace settings
diff --git a/echo/frontend/src/lib/appVersion.test.ts b/echo/frontend/src/lib/appVersion.test.ts
index 12bb1c20..98438f32 100644
--- a/echo/frontend/src/lib/appVersion.test.ts
+++ b/echo/frontend/src/lib/appVersion.test.ts
@@ -199,6 +199,39 @@ it("does not refetch the manifest on every navigation", async () => {
await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
});
+// --- chunk-error message class ---
+
+it("recognises the stale-deploy chunk-error messages across browsers", async () => {
+ const { isChunkLoadErrorMessage } = await loadModule();
+
+ expect(
+ isChunkLoadErrorMessage(
+ "Unable to preload CSS for /assets/WorkspaceSelectorRoute-abc123.css",
+ ),
+ ).toBe(true);
+ expect(
+ isChunkLoadErrorMessage(
+ "Failed to fetch dynamically imported module: https://app/assets/Route-x.js",
+ ),
+ ).toBe(true);
+ expect(
+ isChunkLoadErrorMessage(
+ "error loading dynamically imported module: https://app/assets/Route-x.js",
+ ),
+ ).toBe(true);
+ expect(isChunkLoadErrorMessage("Importing a module script failed.")).toBe(
+ true,
+ );
+});
+
+it("leaves unrelated exceptions untouched", async () => {
+ const { isChunkLoadErrorMessage } = await loadModule();
+
+ expect(isChunkLoadErrorMessage("TypeError: x is not a function")).toBe(false);
+ expect(isChunkLoadErrorMessage(undefined)).toBe(false);
+ expect(isChunkLoadErrorMessage(null)).toBe(false);
+});
+
// --- dev ---
it("is inert in dev, where no manifest is emitted", async () => {
diff --git a/echo/frontend/src/lib/appVersion.ts b/echo/frontend/src/lib/appVersion.ts
index eeb4bf51..17f4e0d6 100644
--- a/echo/frontend/src/lib/appVersion.ts
+++ b/echo/frontend/src/lib/appVersion.ts
@@ -16,6 +16,25 @@ const targetKey = (target: string) => `dembrane:version-reload:${target}`;
export type ReloadReason = "new_version_detected" | "chunk_load_failed";
+// The messages Vite's `vite:preloadError` carries for a stale-deploy chunk miss,
+// across browsers. `recoverFromChunkFailure` already reloads the tab for these,
+// but the listener rethrows on purpose, so autocapture files each one as an
+// exception. Drop that noise in `before_send`; `app_version_reloaded` stays the
+// health metric.
+const CHUNK_LOAD_ERROR_PATTERNS = [
+ "unable to preload css",
+ "failed to fetch dynamically imported module",
+ "error loading dynamically imported module",
+ "importing a module script failed",
+];
+
+/** True when an exception message is a handled stale-deploy chunk miss. */
+export const isChunkLoadErrorMessage = (message: unknown): boolean => {
+ if (typeof message !== "string") return false;
+ const lower = message.toLowerCase();
+ return CHUNK_LOAD_ERROR_PATTERNS.some((pattern) => lower.includes(pattern));
+};
+
/** Dev emits no manifest, so there is nothing to compare against. */
const isEnabled = (): boolean => !import.meta.env.DEV && BUILD_ID !== "unknown";
diff --git a/echo/frontend/src/main.tsx b/echo/frontend/src/main.tsx
index ea07d8c2..37124ff7 100644
--- a/echo/frontend/src/main.tsx
+++ b/echo/frontend/src/main.tsx
@@ -12,10 +12,30 @@ import {
POSTHOG_UI_HOST,
USE_PARTICIPANT_ROUTER,
} from "./config";
-import { recoverFromChunkFailure } from "./lib/appVersion";
+import {
+ isChunkLoadErrorMessage,
+ recoverFromChunkFailure,
+} from "./lib/appVersion";
posthog.init(POSTHOG_TOKEN, {
api_host: POSTHOG_HOST,
+ // Stale-deploy chunk misses are already recovered by recoverFromChunkFailure
+ // (a tab on an old build reloads onto the new one). Vite still rethrows them,
+ // so drop the autocaptured exception here to keep error tracking clean.
+ before_send: (event) => {
+ if (event?.event === "$exception") {
+ const exceptions = event.properties?.$exception_list;
+ if (
+ Array.isArray(exceptions) &&
+ exceptions.some((exception) =>
+ isChunkLoadErrorMessage(exception?.value),
+ )
+ ) {
+ return null;
+ }
+ }
+ return event;
+ },
// Error tracking: autocapture unhandled errors and promise rejections.
// React render errors are reported separately via ErrorBoundary.
capture_exceptions: {