Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/entry.client.tsx
T

123 lines
4.6 KiB
TypeScript

// 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<string, string | undefined> }).env ?? {}) as Record<string, string | undefined>;
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<void> {
let timeoutId: number | undefined;
const timeout = new Promise<void>((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 `<html lang>` attribute we communicate to the Client.
order: ["htmlTag"],
},
})
.then(() => {
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<StrictMode>
<HydratedRouter onError={sentryHydrationErrorHandler} />
</StrictMode>
</I18nextProvider>
);
// 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 <Form>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);
}