123 lines
4.6 KiB
TypeScript
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);
|
|
}
|