Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 ?? "")
Expand Down Expand Up @@ -264,13 +264,13 @@ export const ProjectLegalBasisSection = ({
) : (
<span />
)}
{/* New tab keeps the editor form the host is filling in */}
<Anchor
size="sm"
fw={600}
onClick={() =>
navigate(`/w/${workspaceId}/settings/general`)
}
style={{ cursor: "pointer" }}
href={`/${language}/w/${workspaceId}/settings/general`}
target="_blank"
rel="noopener noreferrer"
>
<Group gap={4} wrap="nowrap">
<Trans>Workspace settings</Trans>
Expand Down
33 changes: 33 additions & 0 deletions echo/frontend/src/lib/appVersion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
19 changes: 19 additions & 0 deletions echo/frontend/src/lib/appVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
22 changes: 21 additions & 1 deletion echo/frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading