51ded06fde
- F5 [stability/regression]: the round-1 F2 fix re-rendered the root with <PostHogProvider><App/></PostHogProvider> after the analytics chunk loaded. In the ChunkLoadErrorBoundary child slot the element TYPE changes App -> PostHogProvider, so React does NOT reconcile in place — it REMOUNTS the whole App: every mount effect runs twice (websocket connect/disconnect, origin tracking, subscriptions) and local state / focus / scroll / in-progress input is lost on cloud cold-load (e.g. typing in /login before analytics loads). And it was USELESS: the app has ZERO consumers of the PostHog React context (no usePostHog / useFeatureFlag* / PostHogFeature), and PostHogProvider given an initialized client is a no-op — all capture goes through the posthog singleton. Fix: initAnalytics now inits the posthog SINGLETON only (no posthog-js/react import, no second render); renderApp() renders <App/> once. First paint stays instant, cloud analytics behavior unchanged, no remount. - F6 [test]: exported isChunkLoadError + chunk-load-error-boundary.test.ts — pins the detector (ChunkLoadError name + the 3 dynamic-import failure messages, case-insensitive → true; null/undefined/ordinary errors → false) so a false-negative that re-blanks the app on a real chunk-404 is caught. Gate: client tsc 0, chunk-load + sanitize tests 14 passed. Entry chunk unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
import "@mantine/core/styles.css";
|
|
import "@mantine/spotlight/styles.css";
|
|
import "@mantine/notifications/styles.css";
|
|
import '@mantine/dates/styles.css';
|
|
import "@/styles/a11y-overrides.css";
|
|
|
|
import ReactDOM from "react-dom/client";
|
|
import App from "./App.tsx";
|
|
import { mantineCssResolver, theme } from "@/theme";
|
|
import { MantineProvider } from "@mantine/core";
|
|
import { BrowserRouter } from "react-router-dom";
|
|
import { ModalsProvider } from "@mantine/modals";
|
|
import { Notifications } from "@mantine/notifications";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { HelmetProvider } from "react-helmet-async";
|
|
import { ChunkLoadErrorBoundary } from "@/components/chunk-load-error-boundary.tsx";
|
|
import "./i18n";
|
|
import {
|
|
getPostHogHost,
|
|
getPostHogKey,
|
|
isCloud,
|
|
isPostHogEnabled,
|
|
} from "@/lib/config.ts";
|
|
import { initVitals } from "@/lib/telemetry/vitals";
|
|
|
|
export const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
refetchOnMount: false,
|
|
refetchOnWindowFocus: false,
|
|
retry: false,
|
|
staleTime: 5 * 60 * 1000,
|
|
},
|
|
},
|
|
});
|
|
|
|
// #355 — client perf-telemetry. Decides sampling ONCE (25%/session) before
|
|
// subscribing to any observer; non-sampled sessions send nothing.
|
|
initVitals();
|
|
|
|
const container = document.getElementById("root") as HTMLElement;
|
|
const root = (container as any).__reactRoot ??= ReactDOM.createRoot(container);
|
|
|
|
function renderApp() {
|
|
root.render(
|
|
<BrowserRouter>
|
|
<MantineProvider theme={theme} cssVariablesResolver={mantineCssResolver}>
|
|
<ModalsProvider>
|
|
<QueryClientProvider client={queryClient}>
|
|
<Notifications position="bottom-center" limit={3} zIndex={10000} />
|
|
<HelmetProvider>
|
|
{/* Root boundary above every lazy route's Suspense: a stale-chunk
|
|
404 after a deploy is caught and recovered here instead of
|
|
blanking the whole app. */}
|
|
<ChunkLoadErrorBoundary>
|
|
<App />
|
|
</ChunkLoadErrorBoundary>
|
|
</HelmetProvider>
|
|
</QueryClientProvider>
|
|
</ModalsProvider>
|
|
</MantineProvider>
|
|
</BrowserRouter>,
|
|
);
|
|
}
|
|
|
|
async function initAnalytics() {
|
|
// posthog-js is only pulled in for cloud deployments with analytics enabled, so
|
|
// self-hosted builds never download it. The gate is kept identical to the
|
|
// previous eager code so cloud analytics behavior is unchanged; the import is
|
|
// simply deferred behind it.
|
|
//
|
|
// Crucially this runs AFTER the immediate first render below, so first paint is
|
|
// never gated on the analytics chunk. Any failure (network, stale 404, or an
|
|
// ad-blocker blocking a chunk named "posthog") is swallowed so the user keeps a
|
|
// working app without analytics instead of a permanently blank page.
|
|
//
|
|
// NOTE: we init the posthog SINGLETON only and do NOT wrap the tree in
|
|
// <PostHogProvider>. The app has zero consumers of the PostHog React context
|
|
// (no usePostHog / useFeatureFlag* / PostHogFeature), and PostHogProvider given
|
|
// an already-initialized `client` is a no-op — all capture goes through the
|
|
// singleton. Re-rendering to attach the provider would only REMOUNT the whole
|
|
// App (running every mount effect twice and dropping local state / focus /
|
|
// in-progress input on cloud cold-load) for no functional gain.
|
|
if (!(isCloud() && isPostHogEnabled)) return;
|
|
try {
|
|
const { default: posthog } = await import("posthog-js");
|
|
posthog.init(getPostHogKey(), {
|
|
api_host: getPostHogHost(),
|
|
defaults: "2025-05-24",
|
|
disable_session_recording: true,
|
|
capture_pageleave: false,
|
|
});
|
|
} catch {
|
|
// Analytics failed to load — degrade gracefully; the app already rendered.
|
|
}
|
|
}
|
|
|
|
// Paint immediately for everyone (self-hosted stays exactly as instant as before,
|
|
// cloud no longer blocks on the analytics import). The posthog singleton is
|
|
// initialized after, without re-rendering the tree.
|
|
renderApp();
|
|
void initAnalytics();
|