From 8f5f64569c20e823a894ce9c0fa62cab123324f0 Mon Sep 17 00:00:00 2001 From: Matti Nannt <675065+mattinannt@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:31:50 +0200 Subject: [PATCH 1/2] refactor: move Sentry client init to instrumentation-client (#8725) Co-authored-by: Claude --- apps/web/app/layout.tsx | 24 ++-- apps/web/app/sentry/SentryProvider.tsx | 69 ---------- apps/web/instrumentation-client.ts | 9 ++ .../lib/sentry/SentryClientConfigScript.tsx | 37 ++++++ apps/web/lib/sentry/client-runtime-config.ts | 26 ++++ .../web/lib/sentry/init-client-sentry.test.ts | 118 ++++++++++++++++++ apps/web/lib/sentry/init-client-sentry.ts | 97 ++++++++++++++ 7 files changed, 294 insertions(+), 86 deletions(-) delete mode 100644 apps/web/app/sentry/SentryProvider.tsx create mode 100644 apps/web/instrumentation-client.ts create mode 100644 apps/web/lib/sentry/SentryClientConfigScript.tsx create mode 100644 apps/web/lib/sentry/client-runtime-config.ts create mode 100644 apps/web/lib/sentry/init-client-sentry.test.ts create mode 100644 apps/web/lib/sentry/init-client-sentry.ts diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 540c28bc8c4a..cc0fc401fb4d 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,14 +1,8 @@ import { Metadata } from "next"; import React from "react"; import { NoScriptWarning } from "@/app/components/NoScriptWarning"; -import { SentryProvider } from "@/app/sentry/SentryProvider"; -import { - DEFAULT_LOCALE, - IS_PRODUCTION, - SENTRY_DSN, - SENTRY_ENVIRONMENT, - SENTRY_RELEASE, -} from "@/lib/constants"; +import { DEFAULT_LOCALE } from "@/lib/constants"; +import { SentryClientConfigScript } from "@/lib/sentry/SentryClientConfigScript"; import { I18nProvider } from "@/lingodotdev/client"; import { getLocale } from "@/lingodotdev/language"; import "../modules/ui/globals.css"; @@ -27,16 +21,12 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => { return ( + {/* First in the document so instrumentation-client.ts can start Sentry as early as possible. */} + - - - {children} - - + + {children} + ); diff --git a/apps/web/app/sentry/SentryProvider.tsx b/apps/web/app/sentry/SentryProvider.tsx deleted file mode 100644 index ddfe51fccc03..000000000000 --- a/apps/web/app/sentry/SentryProvider.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import * as Sentry from "@sentry/nextjs"; -import { useEffect } from "react"; - -interface SentryProviderProps { - children: React.ReactNode; - sentryDsn?: string; - sentryRelease?: string; - sentryEnvironment?: string; - isEnabled?: boolean; -} - -export const SentryProvider = ({ - children, - sentryDsn, - sentryRelease, - sentryEnvironment, - isEnabled, -}: SentryProviderProps) => { - useEffect(() => { - if (sentryDsn && isEnabled) { - Sentry.init({ - dsn: sentryDsn, - release: sentryRelease, - environment: sentryEnvironment, - - // No tracing while Sentry doesn't update to telemetry 2.0.0 - https://github.com/getsentry/sentry-javascript/issues/15737 - tracesSampleRate: 0, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, - - // Disable telemetry and additional data collection - sendDefaultPii: false, - sendClientReports: false, - - replaysOnErrorSampleRate: 1.0, - - // This sets the sample rate to be 10%. You may want this to be 100% while - // in development and sample at a lower rate in production - replaysSessionSampleRate: 0.1, - - // You can remove this option if you're not planning to use the Sentry Session Replay feature: - integrations: [ - Sentry.replayIntegration({ - // Additional Replay configuration goes in here, for example: - maskAllText: true, - blockAllMedia: true, - }), - ], - - beforeSend(event, hint) { - const error = hint.originalException as Error; - - // @ts-expect-error - if (error && error.digest === "NEXT_NOT_FOUND") { - return null; - } - - return event; - }, - }); - } - // We only want to run this once - }, []); - - return <>{children}; -}; diff --git a/apps/web/instrumentation-client.ts b/apps/web/instrumentation-client.ts new file mode 100644 index 000000000000..c1654be01ba9 --- /dev/null +++ b/apps/web/instrumentation-client.ts @@ -0,0 +1,9 @@ +// Next.js client instrumentation hook: runs in the browser before any app code and before +// hydration, so module-evaluation, hydration and early-navigation errors are captured. Replaces the +// former `SentryProvider` `useEffect`, which only started Sentry after hydration (ENG-1686). +import * as Sentry from "@sentry/nextjs"; +import { initClientSentryFromRuntimeConfig } from "@/lib/sentry/init-client-sentry"; + +initClientSentryFromRuntimeConfig(); + +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; diff --git a/apps/web/lib/sentry/SentryClientConfigScript.tsx b/apps/web/lib/sentry/SentryClientConfigScript.tsx new file mode 100644 index 000000000000..c8077c116118 --- /dev/null +++ b/apps/web/lib/sentry/SentryClientConfigScript.tsx @@ -0,0 +1,37 @@ +import { IS_PRODUCTION, SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_RELEASE } from "@/lib/constants"; +import { + SENTRY_CLIENT_RUNTIME_CONFIG_KEY, + type TSentryClientRuntimeConfig, +} from "@/lib/sentry/client-runtime-config"; + +/** + * Serialises the server-only Sentry env vars into the document so `instrumentation-client.ts` can + * start the browser SDK with runtime values (see `client-runtime-config.ts` for why this hand-off + * exists). Rendering nothing is the gate: without a DSN or outside production no config -- and no + * DSN -- reaches the browser, matching the previous `SentryProvider` gating. + */ +export const SentryClientConfigScript = () => { + if (!IS_PRODUCTION || !SENTRY_DSN) { + return null; + } + + const config: TSentryClientRuntimeConfig = { + dsn: SENTRY_DSN, + release: SENTRY_RELEASE, + environment: SENTRY_ENVIRONMENT, + }; + + // `` cannot appear in a DSN, but escaping `<` to its unicode form keeps the + // inline script safe regardless of what the env vars hold. String.raw avoids + // double-escaping the backslash, so the replacement reads as it lands. + const serializedConfig = JSON.stringify(config).replaceAll("<", String.raw`\u003c`); + + return ( +