diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2e41e3b20..ad5c734a4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,6 +8,7 @@ import "./globals.css"; import { Providers } from "@/components/providers"; import { AuthShell } from "@/components/layout/auth-shell"; import { MonitoringBootstrap } from "@/components/monitoring/bootstrap"; +import { getPublicMonitoringSettings } from "@/lib/monitoring-settings"; import { WebVitalsReporter } from "@/components/monitoring/web-vitals-reporter"; import { resolveInitialLocale } from "@/lib/i18n/resolve-initial-locale"; import { isKeyMismatch } from "@/lib/boot/key-mismatch-state"; @@ -86,7 +87,12 @@ export default async function RootLayout({ ? ((await headers()).get("x-nonce") ?? undefined) : undefined; - const initialLocale = await resolveInitialLocale(); + // The monitoring switches ride the document render instead of a request + // from every page load; the key-mismatch page reads no data. + const [initialLocale, monitoring] = await Promise.all([ + resolveInitialLocale(), + isKeyMismatch() ? null : getPublicMonitoringSettings(), + ]); return ( @@ -139,7 +145,15 @@ export default async function RootLayout({ ) : ( - + {/* `DEMO_MODE` is a server-only env var; the proxy uses it to block mutations. Resolve it here (server component) and diff --git a/src/components/monitoring/__tests__/bootstrap.test.tsx b/src/components/monitoring/__tests__/bootstrap.test.tsx new file mode 100644 index 000000000..e0a8c8fdb --- /dev/null +++ b/src/components/monitoring/__tests__/bootstrap.test.tsx @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +/** + * The monitoring switches come from the root layout's render, not from a + * request every page load sent to `/api/monitoring/settings` for three + * instance-wide values. The first render already mounts what they turn on. + */ + +const apiGet = vi.fn(); +vi.mock("@/lib/api/api-fetch", () => ({ + apiGet: (...args: unknown[]) => apiGet(...args), +})); +vi.mock("@/components/monitoring/umami-script", () => ({ + UmamiScript: (props: { enabled: boolean; websiteId: string | null }) => ( + + ), +})); +vi.mock("@/components/monitoring/glitchtip-reporter", () => ({ + GlitchtipReporter: (props: { enabled: boolean }) => ( + + ), +})); + +import { MonitoringBootstrap } from "../bootstrap"; + +describe("MonitoringBootstrap", () => { + it("mounts what the layout's switches turn on, without asking the server", () => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain('data-umami="true:site-1"'); + expect(html).toContain('data-glitchtip="true"'); + expect(apiGet).not.toHaveBeenCalled(); + }); + + it("mounts nothing without settings", () => { + expect(renderToStaticMarkup()).toBe( + "", + ); + }); +}); diff --git a/src/components/monitoring/bootstrap.tsx b/src/components/monitoring/bootstrap.tsx index 0598dcff5..1f6c790ae 100644 --- a/src/components/monitoring/bootstrap.tsx +++ b/src/components/monitoring/bootstrap.tsx @@ -1,43 +1,21 @@ -"use client"; - -import { useEffect, useState } from "react"; import { UmamiScript } from "@/components/monitoring/umami-script"; import { GlitchtipReporter } from "@/components/monitoring/glitchtip-reporter"; -import { apiGet } from "@/lib/api/api-fetch"; - -interface MonitoringSettings { - umamiEnabled: boolean; - umamiWebsiteId: string | null; - glitchtipEnabled: boolean; -} - -export function MonitoringBootstrap() { - const [settings, setSettings] = useState(null); - - useEffect(() => { - let active = true; - - apiGet("/api/monitoring/settings", { - cache: "no-store", - }) - .then((data) => { - if (!active || !data) return; - setSettings(data); - }) - .catch(() => { - if (!active) return; - setSettings({ - umamiEnabled: false, - umamiWebsiteId: null, - glitchtipEnabled: false, - }); - }); - - return () => { - active = false; - }; - }, []); +import type { PublicMonitoringSettings } from "@/lib/monitoring-settings"; +/** + * Mounts the operator's analytics script and error reporter. The root layout + * reads the instance-wide switches on the server and passes them in, so no + * page load asks `/api/monitoring/settings` for three values the document + * render already had. `null` (the key-mismatch page) mounts nothing. + */ +export function MonitoringBootstrap({ + settings, +}: { + settings: Pick< + PublicMonitoringSettings, + "umamiEnabled" | "umamiWebsiteId" | "glitchtipEnabled" + > | null; +}) { if (!settings) return null; return (