Skip to content

fix(marketing): prevent env validation from blocking React hydration#3388

Open
michalkopanski wants to merge 1 commit into
superset-sh:mainfrom
michalkopanski:marketing-page-env-fixes
Open

fix(marketing): prevent env validation from blocking React hydration#3388
michalkopanski wants to merge 1 commit into
superset-sh:mainfrom
michalkopanski:marketing-page-env-fixes

Conversation

@michalkopanski
Copy link
Copy Markdown
Contributor

@michalkopanski michalkopanski commented Apr 12, 2026

Summary

  • Add .default("https://us.posthog.com") to NEXT_PUBLIC_POSTHOG_HOST in env.ts — it's a fixed public URL with no reason to vary per environment
  • Wrap posthog.init and Sentry.init in try/catch in instrumentation-client.ts so a bad analytics key can't block hydration
  • Add apps/marketing/.env.example documenting all required vars

Why / Context

NEXT_PUBLIC_POSTHOG_HOST was a required field in createEnv with no default. When the var was missing, createEnv threw during module initialization — before Next.js called hydrate(). This silently prevented all of React from mounting: framer-motion animations stayed at their initial state (logo opacity 0), useEffect/useState hooks never ran (TypewriterText stayed empty), with no obvious error visible on the page.

The skipValidation escape hatch (process.env.SKIP_ENV_VALIDATION) couldn't help here because it's not a NEXT_PUBLIC_* var, so it's never available in the browser — validation always ran client-side regardless.

How It Works

NEXT_PUBLIC_POSTHOG_HOST is the PostHog ingestion endpoint. The public US cluster (https://us.posthog.com) is the correct default for all environments — there's no scenario where this should point elsewhere. Making it optional-with-default means the schema still validates it's a valid URL, but missing env no longer throws.

The try/catch around posthog.init and Sentry.init is a secondary safeguard: if either initializer throws for any other reason (bad key format, network-unavailable-at-init, etc.), it logs a warning instead of propagating and killing hydration.

Manual QA Checklist

  • Marketing page loads with NEXT_PUBLIC_POSTHOG_HOST unset — animations play, TypewriterText renders
  • Marketing page loads with NEXT_PUBLIC_POSTHOG_HOST set — PostHog uses the provided host
  • Removing NEXT_PUBLIC_POSTHOG_KEY logs a console.warn instead of crashing the page

Testing

  • bun run typecheck
  • bun run lint

Notes

apps/marketing/.env.example needed git add -f because .gitignore has a broad .env.* pattern. The root .env.example was already force-tracked the same way.


Summary by cubic

Fixes a marketing-page hydration failure caused by client env validation. Sets a safe default for NEXT_PUBLIC_POSTHOG_HOST and hardens analytics init so missing/bad keys don’t block React.

  • Bug Fixes
    • Set NEXT_PUBLIC_POSTHOG_HOST default to https://us.posthog.com in env.ts to prevent client-side schema throws before hydration.
    • Wrap posthog.init and Sentry.init in try/catch to warn instead of crashing when keys/hosts are misconfigured.
    • Add apps/marketing/.env.example documenting required server and client vars.

Written for commit b126a56. Summary will update on new commits.

Summary by CodeRabbit

  • Chores

    • Added environment configuration example file for the marketing application with integration placeholders.
  • Bug Fixes

    • Added error handling for analytics and error tracking service initialization to prevent startup failures.

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
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

The marketing app gains comprehensive environment configuration with server and client-side secrets, integrations (Better Auth, Resend, Stripe, Slack, Sentry, Anthropic), and analytics setup. Client-side instrumentation initialization for PostHog and Sentry is wrapped in error handlers to prevent runtime failures.

Changes

Cohort / File(s) Summary
Environment Configuration
apps/marketing/.env.example, apps/marketing/src/env.ts
Added environment variables template for secrets and third-party integrations. Updated PostHog host validation to default to https://us.posthog.com when unset.
Instrumentation Error Handling
apps/marketing/src/instrumentation-client.ts
Wrapped PostHog and Sentry initialization in try/catch blocks to emit warnings instead of throwing on initialization failures. Preserved consent-based opt-out behavior.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 With .env files in place and schemas refined,
PostHog and Sentry errors now gently decline,
Default values bloom, try/catch guards stand tall,
The marketing app is resilient through it all! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: preventing environment validation from blocking React hydration, which is the core issue addressed.
Description check ✅ Passed The description is comprehensive and follows the template structure, including clear summary, context, how it works, manual QA checklist, testing steps, and notes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 12, 2026

Greptile Summary

This PR fixes a real hydration-blocking bug on the marketing site: NEXT_PUBLIC_POSTHOG_HOST was a required field in createEnv with no default, so when it was absent the schema threw during module initialization — before React could mount. Adding .default(\"https://us.posthog.com\") correctly eliminates the throw for this variable. The secondary try/catch guards around posthog.init and Sentry.init are a sensible defence-in-depth measure, and the new .env.example is a welcome addition for onboarding.

Key points:

  • The primary fix (NEXT_PUBLIC_POSTHOG_HOST default) correctly solves the described hydration issue.
  • The try/catch blocks guard against SDK-internal failures, but do not protect against a missing NEXT_PUBLIC_POSTHOG_KEY — that variable is still required in env.ts, and the import { env } at module scope runs before any try/catch. QA checklist item Bump react from 18.3.1 to 19.2.0 #3 ("Removing NEXT_PUBLIC_POSTHOG_KEY logs a console.warn") would fail as-is.
  • The .env.example cleanly documents all vars and correctly annotates the new default for the host."

Confidence Score: 4/5

Safe to merge for the primary fix; the secondary resilience claim in the QA checklist is inaccurate but does not introduce a regression.

The root cause fix is correct and targeted. The try/catch guards add value for SDK-internal errors. The only gap is that QA checklist item #3 implies NEXT_PUBLIC_POSTHOG_KEY absence is handled gracefully, but the env import at module scope means it is not — a misleading but non-regressing issue.

apps/marketing/src/instrumentation-client.ts — the relationship between the module-scope env import and the try/catch scope needs attention if full resilience to missing env vars is desired.

Important Files Changed

Filename Overview
apps/marketing/src/env.ts Adds .default("https://us.posthog.com") to NEXT_PUBLIC_POSTHOG_HOST, correctly fixing the root cause of the hydration block. NEXT_PUBLIC_POSTHOG_KEY remains required.
apps/marketing/src/instrumentation-client.ts Wraps posthog.init and Sentry.init in try/catch blocks — good secondary safeguard, but the env import at module scope means try/catch cannot protect against missing-env-var failures from createEnv, contradicting QA checklist item #3.
apps/marketing/.env.example New file documenting all required env vars; NEXT_PUBLIC_POSTHOG_HOST includes a helpful comment and default value. Clean addition.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant NextJS as Next.js Runtime
    participant InstrClient as instrumentation-client.ts
    participant EnvModule as env.ts (createEnv)
    participant PostHog
    participant Sentry

    Browser->>NextJS: Load marketing page
    NextJS->>InstrClient: Import module
    InstrClient->>EnvModule: import { env } [module scope]

    alt NEXT_PUBLIC_POSTHOG_HOST missing (before fix)
        EnvModule-->>InstrClient: throws ZodError
        InstrClient-->>NextJS: Module init fails
        NextJS-->>Browser: Hydration blocked
    else NEXT_PUBLIC_POSTHOG_HOST missing (after fix)
        EnvModule-->>InstrClient: returns env with default host
        InstrClient->>PostHog: posthog.init() inside try/catch
        PostHog-->>InstrClient: initialized
        InstrClient->>Sentry: Sentry.init() inside try/catch
        Sentry-->>InstrClient: initialized
        InstrClient-->>NextJS: Module ready
        NextJS-->>Browser: Hydration succeeds
    end

    Note over InstrClient,EnvModule: NEXT_PUBLIC_POSTHOG_KEY still required —
    Note over InstrClient,EnvModule: missing key still throws at module scope
    Note over InstrClient,EnvModule: before any try/catch runs
Loading

Reviews (1): Last reviewed commit: "fix(marketing): prevent env validation f..." | Re-trigger Greptile

Comment on lines +8 to +35
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 try/catch does not protect against a missing PostHog key env var

The PR's QA checklist claims that removing NEXT_PUBLIC_POSTHOG_KEY should log a console.warn instead of crashing the page — but the code does not support this.

Line 5 (import { env } from "@/env") executes at module scope, before any try/catch. If NEXT_PUBLIC_POSTHOG_KEY is absent, createEnv throws during that top-level import, and the module never reaches the try block below. The try/catch only guards against posthog.init itself throwing (e.g. a bad key format or internal SDK error), not against missing-variable failures from createEnv.

To match the documented QA behavior, NEXT_PUBLIC_POSTHOG_KEY would also need to be made optional in env.ts (similar to how NEXT_PUBLIC_POSTHOG_HOST now has a default), and the posthog.init call should guard on its presence before passing it through.

Without this, QA checklist item #3 will fail when tested, and the same silent hydration block described in the PR could recur if the PostHog key var is ever absent from a deployment.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
apps/marketing/src/instrumentation-client.ts (1)

11-11: Use env.NEXT_PUBLIC_POSTHOG_HOST instead of hardcoding ui_host.

Line 11 duplicates the default from env.ts and bypasses env-based overrides. Wiring this to env.NEXT_PUBLIC_POSTHOG_HOST keeps one source of truth.

♻️ Proposed change
-		ui_host: "https://us.posthog.com",
+		ui_host: env.NEXT_PUBLIC_POSTHOG_HOST,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/marketing/src/instrumentation-client.ts` at line 11, Replace the
hardcoded posthog host in the instrumentation client by reading the environment
variable instead of the literal "https://us.posthog.com": locate where ui_host
is set (property ui_host in apps/marketing/src/instrumentation-client.ts) and
change it to use env.NEXT_PUBLIC_POSTHOG_HOST (or the equivalent exported env
object used across the project) so the value is driven by
environment/configuration rather than a duplicated hardcoded default.
apps/marketing/.env.example (1)

2-21: Optional cleanup: reorder keys to silence dotenv-linter UnorderedKey warnings.

This file is functionally fine, but current ordering triggers repeated lint warnings. If dotenv-linter is part of CI gating/noise reduction, sorting keys (or grouping in the expected order) will keep checks clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/marketing/.env.example` around lines 2 - 21, The .env example triggers
dotenv-linter UnorderedKey warnings because keys are not consistently sorted;
reorder the variables to a deterministic order (e.g., alphabetically) to silence
the linter: sort the top-level server keys including ANTHROPIC_API_KEY,
BETTER_AUTH_SECRET, KV_REST_API_TOKEN, KV_REST_API_URL, RESEND_API_KEY,
SENTRY_AUTH_TOKEN, SLACK_BILLING_WEBHOOK_URL, STRIPE_PRO_MONTHLY_PRICE_ID,
STRIPE_PRO_YEARLY_PRICE_ID, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and keep
the Client block (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_POSTHOG_HOST,
NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_SENTRY_DSN_MARKETING,
NEXT_PUBLIC_SENTRY_ENVIRONMENT, NEXT_PUBLIC_WEB_URL) sorted as well so
dotenv-linter no longer reports UnorderedKey.
apps/marketing/src/env.ts (1)

28-29: NEXT_PUBLIC_POSTHOG_KEY is defined as required but unused; making it optional is reasonable for resilience.

NEXT_PUBLIC_POSTHOG_KEY: z.string() at line 28 is required in the schema. However, the variable is never actually consumed in active code paths—PostHog initialization in Providers.tsx bypasses it entirely, and instrumentation-client.ts (which does reference it) is dead code.

While the env schema is validated when CTAButtons.tsx imports it (which runs at app startup), the unused required key adds unnecessary coupling. Making it optional would align with the resilience goal.

🔧 Suggested schema adjustment
-		NEXT_PUBLIC_POSTHOG_KEY: z.string(),
+		NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/marketing/src/env.ts` around lines 28 - 29, The env schema currently
declares NEXT_PUBLIC_POSTHOG_KEY as required (NEXT_PUBLIC_POSTHOG_KEY:
z.string()) even though it's unused; change that schema entry to be optional or
provide a sensible default so startup validation doesn't require the key. Update
the symbol NEXT_PUBLIC_POSTHOG_KEY in apps/marketing/src/env.ts to
z.string().optional() (or z.string().default("") if preferred) so imports like
CTAButtons.tsx won't fail, and leave PostHog initialization in Providers.tsx and
the dead instrumentation-client.ts code unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/marketing/.env.example`:
- Around line 2-21: The .env example triggers dotenv-linter UnorderedKey
warnings because keys are not consistently sorted; reorder the variables to a
deterministic order (e.g., alphabetically) to silence the linter: sort the
top-level server keys including ANTHROPIC_API_KEY, BETTER_AUTH_SECRET,
KV_REST_API_TOKEN, KV_REST_API_URL, RESEND_API_KEY, SENTRY_AUTH_TOKEN,
SLACK_BILLING_WEBHOOK_URL, STRIPE_PRO_MONTHLY_PRICE_ID,
STRIPE_PRO_YEARLY_PRICE_ID, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and keep
the Client block (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_POSTHOG_HOST,
NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_SENTRY_DSN_MARKETING,
NEXT_PUBLIC_SENTRY_ENVIRONMENT, NEXT_PUBLIC_WEB_URL) sorted as well so
dotenv-linter no longer reports UnorderedKey.

In `@apps/marketing/src/env.ts`:
- Around line 28-29: The env schema currently declares NEXT_PUBLIC_POSTHOG_KEY
as required (NEXT_PUBLIC_POSTHOG_KEY: z.string()) even though it's unused;
change that schema entry to be optional or provide a sensible default so startup
validation doesn't require the key. Update the symbol NEXT_PUBLIC_POSTHOG_KEY in
apps/marketing/src/env.ts to z.string().optional() (or z.string().default("") if
preferred) so imports like CTAButtons.tsx won't fail, and leave PostHog
initialization in Providers.tsx and the dead instrumentation-client.ts code
unchanged.

In `@apps/marketing/src/instrumentation-client.ts`:
- Line 11: Replace the hardcoded posthog host in the instrumentation client by
reading the environment variable instead of the literal
"https://us.posthog.com": locate where ui_host is set (property ui_host in
apps/marketing/src/instrumentation-client.ts) and change it to use
env.NEXT_PUBLIC_POSTHOG_HOST (or the equivalent exported env object used across
the project) so the value is driven by environment/configuration rather than a
duplicated hardcoded default.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b192fa5-9067-47aa-8d70-b0e9c351b10d

📥 Commits

Reviewing files that changed from the base of the PR and between ef67d8a and b126a56.

📒 Files selected for processing (3)
  • apps/marketing/.env.example
  • apps/marketing/src/env.ts
  • apps/marketing/src/instrumentation-client.ts

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/marketing/src/instrumentation-client.ts">

<violation number="1" location="apps/marketing/src/instrumentation-client.ts:8">
P1: `try/catch` does not protect against a missing `NEXT_PUBLIC_POSTHOG_KEY`. The `import { env } from "@/env"` on line 5 runs at module evaluation time — before this `try` block is reached. If `NEXT_PUBLIC_POSTHOG_KEY` is absent, `createEnv` throws during the import, killing hydration exactly like the original bug. To actually guard against this, either make `NEXT_PUBLIC_POSTHOG_KEY` optional with a default/guard in `env.ts`, or lazily access `env` inside the `try` block (e.g., via a dynamic import or deferred access pattern).</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

app_name: "marketing",
domain: window.location.hostname,
});
try {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: try/catch does not protect against a missing NEXT_PUBLIC_POSTHOG_KEY. The import { env } from "@/env" on line 5 runs at module evaluation time — before this try block is reached. If NEXT_PUBLIC_POSTHOG_KEY is absent, createEnv throws during the import, killing hydration exactly like the original bug. To actually guard against this, either make NEXT_PUBLIC_POSTHOG_KEY optional with a default/guard in env.ts, or lazily access env inside the try block (e.g., via a dynamic import or deferred access pattern).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/marketing/src/instrumentation-client.ts, line 8:

<comment>`try/catch` does not protect against a missing `NEXT_PUBLIC_POSTHOG_KEY`. The `import { env } from "@/env"` on line 5 runs at module evaluation time — before this `try` block is reached. If `NEXT_PUBLIC_POSTHOG_KEY` is absent, `createEnv` throws during the import, killing hydration exactly like the original bug. To actually guard against this, either make `NEXT_PUBLIC_POSTHOG_KEY` optional with a default/guard in `env.ts`, or lazily access `env` inside the `try` block (e.g., via a dynamic import or deferred access pattern).</comment>

<file context>
@@ -5,41 +5,49 @@ import posthog from "posthog-js";
-			app_name: "marketing",
-			domain: window.location.hostname,
-		});
+try {
+	posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
+		api_host: "/ingest",
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant