From b126a56c406c77c2816d506953e9a60fa3c6e3d7 Mon Sep 17 00:00:00 2001 From: Michal Kopanski Date: Sun, 12 Apr 2026 17:55:54 -0400 Subject: [PATCH] fix(marketing): prevent env validation from blocking React hydration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NEXT_PUBLIC_POSTHOG_HOST had no default, causing createEnv to throw during module init when the var was absent. Since instrumentation-client runs before Next.js calls hydrate(), this silently prevented all of React from mounting — animations stayed at opacity 0, hooks never ran. - Add .default("https://us.posthog.com") to NEXT_PUBLIC_POSTHOG_HOST - Wrap posthog.init and Sentry.init in try/catch as a secondary safeguard - Add apps/marketing/.env.example documenting all required vars --- apps/marketing/.env.example | 21 ++++++ apps/marketing/src/env.ts | 2 +- apps/marketing/src/instrumentation-client.ts | 76 +++++++++++--------- 3 files changed, 64 insertions(+), 35 deletions(-) create mode 100644 apps/marketing/.env.example diff --git a/apps/marketing/.env.example b/apps/marketing/.env.example new file mode 100644 index 00000000000..5da9f185759 --- /dev/null +++ b/apps/marketing/.env.example @@ -0,0 +1,21 @@ +# Server +BETTER_AUTH_SECRET= +RESEND_API_KEY= +KV_REST_API_URL= +KV_REST_API_TOKEN= +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= +STRIPE_PRO_MONTHLY_PRICE_ID= +STRIPE_PRO_YEARLY_PRICE_ID= +SLACK_BILLING_WEBHOOK_URL= +SENTRY_AUTH_TOKEN= +ANTHROPIC_API_KEY= + +# Client +NEXT_PUBLIC_API_URL= +NEXT_PUBLIC_WEB_URL= +NEXT_PUBLIC_POSTHOG_KEY= +# Defaults to https://us.posthog.com if not set +NEXT_PUBLIC_POSTHOG_HOST=https://us.posthog.com +NEXT_PUBLIC_SENTRY_DSN_MARKETING= +NEXT_PUBLIC_SENTRY_ENVIRONMENT= diff --git a/apps/marketing/src/env.ts b/apps/marketing/src/env.ts index c962325f8cc..5f6455f3011 100644 --- a/apps/marketing/src/env.ts +++ b/apps/marketing/src/env.ts @@ -26,7 +26,7 @@ export const env = createEnv({ NEXT_PUBLIC_API_URL: z.string().url(), NEXT_PUBLIC_WEB_URL: z.string().url(), NEXT_PUBLIC_POSTHOG_KEY: z.string(), - NEXT_PUBLIC_POSTHOG_HOST: z.string().url(), + NEXT_PUBLIC_POSTHOG_HOST: z.string().url().default("https://us.posthog.com"), NEXT_PUBLIC_SENTRY_DSN_MARKETING: z.string().optional(), NEXT_PUBLIC_SENTRY_ENVIRONMENT: z .enum(["development", "preview", "production"]) diff --git a/apps/marketing/src/instrumentation-client.ts b/apps/marketing/src/instrumentation-client.ts index ebc25f752b2..1f7572928f1 100644 --- a/apps/marketing/src/instrumentation-client.ts +++ b/apps/marketing/src/instrumentation-client.ts @@ -5,41 +5,49 @@ import posthog from "posthog-js"; import { env } from "@/env"; import { ANALYTICS_CONSENT_KEY } from "@/lib/constants"; -posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, { - api_host: "/ingest", - ui_host: "https://us.posthog.com", - defaults: "2025-11-30", - capture_pageview: "history_change", - capture_pageleave: true, - capture_exceptions: true, - debug: false, - cross_subdomain_cookie: true, - persistence: "cookie", - persistence_name: POSTHOG_COOKIE_NAME, - disable_session_recording: true, - loaded: (posthog) => { - posthog.register({ - app_name: "marketing", - domain: window.location.hostname, - }); +try { + posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: "/ingest", + ui_host: "https://us.posthog.com", + defaults: "2025-11-30", + capture_pageview: "history_change", + capture_pageleave: true, + capture_exceptions: true, + debug: false, + cross_subdomain_cookie: true, + persistence: "cookie", + persistence_name: POSTHOG_COOKIE_NAME, + disable_session_recording: true, + loaded: (posthog) => { + posthog.register({ + app_name: "marketing", + domain: window.location.hostname, + }); - const consent = localStorage.getItem(ANALYTICS_CONSENT_KEY); - if (consent === "declined") { - posthog.opt_out_capturing(); - } - }, -}); + const consent = localStorage.getItem(ANALYTICS_CONSENT_KEY); + if (consent === "declined") { + posthog.opt_out_capturing(); + } + }, + }); +} catch (e) { + console.warn("PostHog failed to initialize", e); +} -Sentry.init({ - dsn: env.NEXT_PUBLIC_SENTRY_DSN_MARKETING, - environment: env.NEXT_PUBLIC_SENTRY_ENVIRONMENT, - enabled: env.NEXT_PUBLIC_SENTRY_ENVIRONMENT === "production", - tracesSampleRate: - env.NEXT_PUBLIC_SENTRY_ENVIRONMENT === "production" ? 0.1 : 1.0, - replaysSessionSampleRate: 0, - replaysOnErrorSampleRate: 0, - sendDefaultPii: true, - debug: false, -}); +try { + Sentry.init({ + dsn: env.NEXT_PUBLIC_SENTRY_DSN_MARKETING, + environment: env.NEXT_PUBLIC_SENTRY_ENVIRONMENT, + enabled: env.NEXT_PUBLIC_SENTRY_ENVIRONMENT === "production", + tracesSampleRate: + env.NEXT_PUBLIC_SENTRY_ENVIRONMENT === "production" ? 0.1 : 1.0, + replaysSessionSampleRate: 0, + replaysOnErrorSampleRate: 0, + sendDefaultPii: true, + debug: false, + }); +} catch (e) { + console.warn("Sentry failed to initialize", e); +} export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;