Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/marketing/.env.example
Original file line number Diff line number Diff line change
@@ -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=
2 changes: 1 addition & 1 deletion apps/marketing/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
76 changes: 42 additions & 34 deletions apps/marketing/src/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
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

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


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;