// IMPORTANT: OTel MUST initialise BEFORE Sentry. The Sentry browser SDK // plugs into @opentelemetry/api's global TracerProvider; initialising the // WebTracerProvider first installs that provider so the two stacks share // the trace_id. No-op when VITE_OTEL_EXPORTER_OTLP_ENDPOINT is unset. const publicEnv = ((import.meta as unknown as { env?: Record }).env ?? {}) as Record; const OBSERVABILITY_HYDRATION_GRACE_MS = 250; function hasRuntimeSentryDsn() { return typeof globalThis !== "undefined" && typeof (globalThis as { __SENTRY_WEB_DSN__?: unknown }).__SENTRY_WEB_DSN__ === "string"; } async function initClientObservability() { if (publicEnv.VITE_OTEL_EXPORTER_OTLP_ENDPOINT) { const { initClientOtel } = await import("./otel.client"); initClientOtel(); } if (publicEnv.VITE_SENTRY_DSN_WEB || hasRuntimeSentryDsn()) { const { initSentryClient } = await import("./sentry.client"); initSentryClient(); // Core Web Vitals → Sentry (#70). Fire-and-forget; the helper is SSR-safe // and silently no-ops when Sentry/web-vitals aren't available. Keep it out // of observabilityReady so hydration never waits for the web-vitals chunk. void import("./lib/observability/web-vitals") .then(({ reportWebVitals }) => reportWebVitals()) .catch(() => {}); } } // Initialize configured observability before hydration so Sentry can install // global error/unhandledrejection handlers before React mounts. Builds without // client observability config avoid loading the heavy SDK chunks on startup. const observabilityReady = initClientObservability().catch(() => { // Observability must never block hydration. The individual modules keep // their own no-op behavior, and a failed dynamic import should degrade to // the browser/React defaults instead of blanking the app. }); function waitForObservabilityHydrationGrace(): Promise { let timeoutId: number | undefined; const timeout = new Promise((resolve) => { timeoutId = window.setTimeout(resolve, OBSERVABILITY_HYDRATION_GRACE_MS); }); return Promise.race([observabilityReady, timeout]).finally(() => { if (timeoutId !== undefined) { window.clearTimeout(timeoutId); } }); } import { HydratedRouter } from "react-router/dom"; import { StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; import { sentryHydrationErrorHandler } from "./lib/sentry/routeTracing"; import { getInitialNamespaces } from "remix-i18next/client"; import { I18nextProvider, initReactI18next } from "react-i18next"; import i18next from "i18next"; import Backend from "i18next-http-backend"; import LanguageDetector from "i18next-browser-languagedetector"; import { i18nConfig } from "./locale/i18n.js"; async function hydrate() { await waitForObservabilityHydrationGrace(); i18next // Use the react-i18next plugin. .use(initReactI18next) // Setup client-side language detector. .use(LanguageDetector) // Setup backend. .use(Backend) .init({ // Spread configuration. ...i18nConfig, // Detects the namespaces your routes rendered while SSR use // and pass them here to load the translations. ns: getInitialNamespaces(), backend: { requestOptions: { cache: "no-store" }, loadPath: "/locales/{{lng}}/{{ns}}.json", }, detection: { // We'll detect the language only server-side with remix-i18next. // By using `` attribute we communicate to the Client. order: ["htmlTag"], }, }) .then(() => { hydrateRoot( document, ); // Signal hydration completion so end-to-end tests can wait for the app to // be interactive before driving JS-only controls (sidebar drawer, command // palette, dropdowns). React 18 attaches event handlers during the // hydrateRoot commit; flag after two animation frames so the marker only // appears once that commit has painted. Progressive-enhanced
s work // before this, but pure-JS buttons do not — hence the explicit signal. requestAnimationFrame(() => { requestAnimationFrame(() => { document.documentElement.setAttribute("data-hydrated", "true"); }); }); }); } if (window.requestIdleCallback) { window.requestIdleCallback(hydrate); } else { // Safari doesn't support requestIdleCallback // https://caniuse.com/requestidlecallback window.setTimeout(hydrate, 1); }