From 2030810be246eb5141dc37734dfdf08381b821b6 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 18 Dec 2025 16:50:45 -0500 Subject: [PATCH 1/2] fix: PostHog cross-subdomain tracking and cookie consent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add persistence: "cookie" and persistence_name to all apps for proper cross-subdomain tracking (matching monorepo pattern) - Default NEXT_PUBLIC_COOKIE_DOMAIN to "localhost" for development - Refactor PostHogUserIdentifier to use Clerk internally (no props) - Move PostHogUserIdentifier to root layout (web) and dashboard layout (admin) - Update marketing cookie banner to Trigger.dev-style (bottom-left, opt-out model) - Add Next.js 16 proxy.ts note to AGENTS.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 1 + apps/admin/next.config.ts | 19 ++++++ apps/admin/package.json | 1 + apps/admin/src/app/providers.tsx | 31 ++++++---- .../PostHogUserIdentifier.tsx | 24 +++++++ .../components/PostHogUserIdentifier/index.ts | 1 + apps/admin/src/env.ts | 9 ++- apps/admin/src/instrumentation-client.ts | 22 +++++++ .../PostHogUserIdentifier.tsx | 23 +++++++ .../components/PostHogUserIdentifier/index.ts | 1 + .../src/renderer/contexts/AppProviders.tsx | 2 + .../src/renderer/contexts/PostHogProvider.tsx | 62 ++++--------------- apps/desktop/src/renderer/lib/posthog.ts | 34 ++++++++++ apps/docs/next.config.ts | 33 ++++++++++ apps/docs/package.json | 6 +- apps/docs/src/app/layout.tsx | 22 ++++--- apps/docs/src/app/providers.tsx | 8 +++ apps/docs/src/env.ts | 33 ++++++++++ apps/docs/src/instrumentation-client.ts | 22 +++++++ apps/marketing/next.config.ts | 19 ++++++ apps/marketing/package.json | 1 + apps/marketing/src/app/layout.tsx | 15 ++--- apps/marketing/src/app/providers.tsx | 22 +++++++ .../CookieConsent/CookieConsent.tsx | 62 +++++++++++++++++++ .../src/components/CookieConsent/index.ts | 1 + apps/marketing/src/env.ts | 9 ++- apps/marketing/src/instrumentation-client.ts | 28 +++++++++ apps/marketing/src/lib/constants.ts | 1 + apps/web/next.config.ts | 19 ++++++ apps/web/package.json | 1 + apps/web/src/app/providers.tsx | 31 ++++++---- .../PostHogUserIdentifier.tsx | 24 +++++++ .../components/PostHogUserIdentifier/index.ts | 1 + apps/web/src/env.ts | 9 ++- apps/web/src/instrumentation-client.ts | 22 +++++++ bun.lock | 9 ++- 36 files changed, 529 insertions(+), 99 deletions(-) create mode 100644 apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx create mode 100644 apps/admin/src/components/PostHogUserIdentifier/index.ts create mode 100644 apps/admin/src/instrumentation-client.ts create mode 100644 apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx create mode 100644 apps/desktop/src/renderer/components/PostHogUserIdentifier/index.ts create mode 100644 apps/desktop/src/renderer/lib/posthog.ts create mode 100644 apps/docs/src/app/providers.tsx create mode 100644 apps/docs/src/env.ts create mode 100644 apps/docs/src/instrumentation-client.ts create mode 100644 apps/marketing/src/app/providers.tsx create mode 100644 apps/marketing/src/components/CookieConsent/CookieConsent.tsx create mode 100644 apps/marketing/src/components/CookieConsent/index.ts create mode 100644 apps/marketing/src/instrumentation-client.ts create mode 100644 apps/marketing/src/lib/constants.ts create mode 100644 apps/web/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx create mode 100644 apps/web/src/components/PostHogUserIdentifier/index.ts create mode 100644 apps/web/src/instrumentation-client.ts diff --git a/AGENTS.md b/AGENTS.md index 6a171f37049..2f72233e765 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,7 @@ Bun + Turbo monorepo with: - **Database**: Drizzle ORM + Neon PostgreSQL - **UI**: React + TailwindCSS v4 + shadcn/ui - **Code Quality**: Biome (formatting + linting at root) +- **Next.js**: Version 16 - NEVER create `middleware.ts`. Next.js 16 renamed middleware to `proxy.ts`. Always use `proxy.ts` for request interception. ## Common Commands diff --git a/apps/admin/next.config.ts b/apps/admin/next.config.ts index db7abcb7fef..767a9ce0daa 100644 --- a/apps/admin/next.config.ts +++ b/apps/admin/next.config.ts @@ -10,6 +10,25 @@ if (process.env.NODE_ENV !== "production") { const config: NextConfig = { reactCompiler: true, typescript: { ignoreBuildErrors: true }, + + async rewrites() { + return [ + { + source: "/ingest/static/:path*", + destination: "https://us-assets.i.posthog.com/static/:path*", + }, + { + source: "/ingest/:path*", + destination: "https://us.i.posthog.com/:path*", + }, + { + source: "/ingest/decide", + destination: "https://us.i.posthog.com/decide", + }, + ]; + }, + + skipTrailingSlashRedirect: true, }; export default config; diff --git a/apps/admin/package.json b/apps/admin/package.json index 99ae15f1a3a..87e3813655f 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -26,6 +26,7 @@ "date-fns": "^4.1.0", "next": "^16.0.10", "next-themes": "^0.4.6", + "posthog-js": "^1.306.1", "react": "^19.2.3", "react-dom": "^19.2.3", "react-icons": "^5.5.0", diff --git a/apps/admin/src/app/providers.tsx b/apps/admin/src/app/providers.tsx index c28c8dc69c0..5b0348e1382 100644 --- a/apps/admin/src/app/providers.tsx +++ b/apps/admin/src/app/providers.tsx @@ -3,22 +3,29 @@ import { THEME_STORAGE_KEY } from "@superset/shared/constants"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ThemeProvider } from "next-themes"; +import posthog from "posthog-js"; +import { PostHogProvider } from "posthog-js/react"; + +import { PostHogUserIdentifier } from "@/components/PostHogUserIdentifier"; import { TRPCReactProvider } from "../trpc/react"; export function Providers({ children }: { children: React.ReactNode }) { return ( - - - {children} - - - + + + + + {children} + + + + ); } diff --git a/apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx b/apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx new file mode 100644 index 00000000000..656dce84cb1 --- /dev/null +++ b/apps/admin/src/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useUser } from "@clerk/nextjs"; +import posthog from "posthog-js"; +import { useEffect } from "react"; + +export function PostHogUserIdentifier() { + const { user, isLoaded } = useUser(); + + useEffect(() => { + if (!isLoaded) return; + + if (user) { + posthog.identify(user.id, { + email: user.primaryEmailAddress?.emailAddress, + name: user.fullName, + }); + } else { + posthog.reset(); + } + }, [user, isLoaded]); + + return null; +} diff --git a/apps/admin/src/components/PostHogUserIdentifier/index.ts b/apps/admin/src/components/PostHogUserIdentifier/index.ts new file mode 100644 index 00000000000..9c592fde4de --- /dev/null +++ b/apps/admin/src/components/PostHogUserIdentifier/index.ts @@ -0,0 +1 @@ +export { PostHogUserIdentifier } from "./PostHogUserIdentifier"; diff --git a/apps/admin/src/env.ts b/apps/admin/src/env.ts index 8c8857439c1..2ef4ac46fdc 100644 --- a/apps/admin/src/env.ts +++ b/apps/admin/src/env.ts @@ -20,7 +20,12 @@ export const env = createEnv({ NEXT_PUBLIC_API_URL: z.string().url(), NEXT_PUBLIC_WEB_URL: z.string().url(), NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(), - NEXT_PUBLIC_COOKIE_DOMAIN: z.string(), + NEXT_PUBLIC_COOKIE_DOMAIN: z.string().default("localhost"), + NEXT_PUBLIC_POSTHOG_KEY: z.string(), + NEXT_PUBLIC_POSTHOG_HOST: z + .string() + .url() + .default("https://us.i.posthog.com"), }, experimental__runtimeEnv: { @@ -30,6 +35,8 @@ export const env = createEnv({ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, NEXT_PUBLIC_COOKIE_DOMAIN: process.env.NEXT_PUBLIC_COOKIE_DOMAIN, + NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, + NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, }, skipValidation: !!process.env.SKIP_ENV_VALIDATION, diff --git a/apps/admin/src/instrumentation-client.ts b/apps/admin/src/instrumentation-client.ts new file mode 100644 index 00000000000..0d82b3aee17 --- /dev/null +++ b/apps/admin/src/instrumentation-client.ts @@ -0,0 +1,22 @@ +import posthog from "posthog-js"; + +import { env } from "@/env"; + +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: env.NODE_ENV === "development", + cross_subdomain_cookie: true, + persistence: "cookie", + persistence_name: env.NEXT_PUBLIC_COOKIE_DOMAIN, + loaded: (posthog) => { + posthog.register({ + app_name: "admin", + domain: window.location.hostname, + }); + }, +}); diff --git a/apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx b/apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx new file mode 100644 index 00000000000..b361223dfc4 --- /dev/null +++ b/apps/desktop/src/renderer/components/PostHogUserIdentifier/PostHogUserIdentifier.tsx @@ -0,0 +1,23 @@ +import { useEffect } from "react"; +import { trpc } from "renderer/lib/trpc"; + +import { posthog } from "../../lib/posthog"; + +export function PostHogUserIdentifier() { + const { data: user, isLoading } = trpc.user.me.useQuery(); + + useEffect(() => { + if (isLoading) return; + + if (user) { + posthog.identify(user.id, { + email: user.email, + name: user.name, + }); + } else { + posthog.reset(); + } + }, [user, isLoading]); + + return null; +} diff --git a/apps/desktop/src/renderer/components/PostHogUserIdentifier/index.ts b/apps/desktop/src/renderer/components/PostHogUserIdentifier/index.ts new file mode 100644 index 00000000000..9c592fde4de --- /dev/null +++ b/apps/desktop/src/renderer/components/PostHogUserIdentifier/index.ts @@ -0,0 +1 @@ +export { PostHogUserIdentifier } from "./PostHogUserIdentifier"; diff --git a/apps/desktop/src/renderer/contexts/AppProviders.tsx b/apps/desktop/src/renderer/contexts/AppProviders.tsx index 0ecd8980969..719536e68d8 100644 --- a/apps/desktop/src/renderer/contexts/AppProviders.tsx +++ b/apps/desktop/src/renderer/contexts/AppProviders.tsx @@ -1,4 +1,5 @@ import type React from "react"; +import { PostHogUserIdentifier } from "renderer/components/PostHogUserIdentifier"; import { MonacoProvider } from "./MonacoProvider"; import { PostHogProvider } from "./PostHogProvider"; import { TRPCProvider } from "./TRPCProvider"; @@ -11,6 +12,7 @@ export function AppProviders({ children }: AppProvidersProps) { return ( + {children} diff --git a/apps/desktop/src/renderer/contexts/PostHogProvider.tsx b/apps/desktop/src/renderer/contexts/PostHogProvider.tsx index 2863b06daf7..ab9e56ef4b5 100644 --- a/apps/desktop/src/renderer/contexts/PostHogProvider.tsx +++ b/apps/desktop/src/renderer/contexts/PostHogProvider.tsx @@ -1,64 +1,24 @@ -// TEMPORARILY DISABLED - PostHog bricked the desktop app -// TODO: Re-enable after fixing the issue - -// import posthog from "posthog-js"; -// import { PostHogProvider as PHProvider } from "posthog-js/react"; import type React from "react"; +import { useEffect, useState } from "react"; -// import { useEffect, useState } from "react"; - -// const POSTHOG_KEY = import.meta.env.NEXT_PUBLIC_POSTHOG_KEY as -// | string -// | undefined; -// const POSTHOG_HOST = -// (import.meta.env.NEXT_PUBLIC_POSTHOG_HOST as string | undefined) || -// "https://us.i.posthog.com"; +import { initPostHog } from "../lib/posthog"; interface PostHogProviderProps { children: React.ReactNode; } export function PostHogProvider({ children }: PostHogProviderProps) { - // TEMPORARILY DISABLED - PostHog bricked the desktop app - // const [isInitialized, setIsInitialized] = useState(false); - - // useEffect(() => { - // if (!POSTHOG_KEY) { - // console.log("[posthog] No PostHog key configured, skipping init"); - // setIsInitialized(true); - // return; - // } - - // posthog.init(POSTHOG_KEY, { - // api_host: POSTHOG_HOST, - // // Electron apps don't have traditional page views - // capture_pageview: false, - // // Disable session recording for now - // disable_session_recording: true, - // // Persist across sessions - // persistence: "localStorage", - // // Load feature flags on init - // bootstrap: { - // featureFlags: {}, - // }, - // }); - - // setIsInitialized(true); - // console.log("[posthog] Initialized"); - // }, []); - - // // Don't render children until PostHog is initialized (or skipped) - // if (!isInitialized) { - // return null; - // } + const [isInitialized, setIsInitialized] = useState(false); - // // If no PostHog key, just render children without the provider - // if (!POSTHOG_KEY) { - // return <>{children}; - // } + useEffect(() => { + initPostHog(); + setIsInitialized(true); + }, []); - // return {children}; + // Don't render children until PostHog is initialized + if (!isInitialized) { + return null; + } - // Just render children without PostHog return <>{children}; } diff --git a/apps/desktop/src/renderer/lib/posthog.ts b/apps/desktop/src/renderer/lib/posthog.ts new file mode 100644 index 00000000000..a1b604786fe --- /dev/null +++ b/apps/desktop/src/renderer/lib/posthog.ts @@ -0,0 +1,34 @@ +import posthog from "posthog-js/dist/module.full.no-external"; + +const POSTHOG_KEY = import.meta.env.NEXT_PUBLIC_POSTHOG_KEY as + | string + | undefined; + +export function initPostHog() { + if (!POSTHOG_KEY) { + console.log("[posthog] No key configured, skipping"); + return; + } + + posthog.init(POSTHOG_KEY, { + api_host: "https://us.i.posthog.com", + ui_host: "https://us.posthog.com", + defaults: "2025-11-30", + capture_pageview: false, + capture_pageleave: false, + capture_exceptions: true, + person_profiles: "identified_only", + persistence: "localStorage", + debug: import.meta.env.DEV, + loaded: (ph) => { + ph.register({ + app_name: "desktop", + platform: window.navigator.platform, + }); + }, + }); + + console.log("[posthog] Initialized"); +} + +export { posthog }; diff --git a/apps/docs/next.config.ts b/apps/docs/next.config.ts index dfe5e3d6b7e..08bb3e65b68 100644 --- a/apps/docs/next.config.ts +++ b/apps/docs/next.config.ts @@ -1,12 +1,45 @@ +import { join } from "node:path"; +import { config as dotenvConfig } from "dotenv"; import type { NextConfig } from "next"; import nextra from "nextra"; +// Load .env from monorepo root during development +if (process.env.NODE_ENV !== "production") { + dotenvConfig({ path: join(process.cwd(), "../../.env"), override: true }); +} + const withNextra = nextra({ defaultShowCopyCode: true, }); const nextConfig: NextConfig = { reactStrictMode: true, + + /** Turbopack MDX resolution for nextra */ + turbopack: { + resolveAlias: { + "next-mdx-import-source-file": "./mdx-components.tsx", + }, + }, + + async rewrites() { + return [ + { + source: "/ingest/static/:path*", + destination: "https://us-assets.i.posthog.com/static/:path*", + }, + { + source: "/ingest/:path*", + destination: "https://us.i.posthog.com/:path*", + }, + { + source: "/ingest/decide", + destination: "https://us.i.posthog.com/decide", + }, + ]; + }, + + skipTrailingSlashRedirect: true, }; export default withNextra(nextConfig); diff --git a/apps/docs/package.json b/apps/docs/package.json index b546c221168..26a494de7f7 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -13,15 +13,19 @@ "@react-three/drei": "^10.7.6", "@react-three/fiber": "^9.4.0", "@superset/ui": "workspace:*", + "@t3-oss/env-nextjs": "^0.13.8", + "dotenv": "^17.2.3", "framer-motion": "^12.23.24", "geist": "^1.5.1", "next": "^16.0.10", "nextra": "^4.6.1", "nextra-theme-blog": "^4.6.1", "nextra-theme-docs": "^4.6.1", + "posthog-js": "^1.306.1", "react": "^19.2.3", "react-dom": "^19.2.3", - "three": "^0.181.2" + "three": "^0.181.2", + "zod": "^4.1.13" }, "devDependencies": { "@superset/typescript": "workspace:*", diff --git a/apps/docs/src/app/layout.tsx b/apps/docs/src/app/layout.tsx index 856992ee7cc..474ec300453 100644 --- a/apps/docs/src/app/layout.tsx +++ b/apps/docs/src/app/layout.tsx @@ -4,6 +4,8 @@ import { Footer, Layout, Navbar } from "nextra-theme-docs"; import type * as React from "react"; import "nextra-theme-docs/style.css"; +import { Providers } from "./providers"; + export const metadata = { title: "Superset Docs", description: "Superset Documentation", @@ -31,15 +33,17 @@ export default async function DocsLayout({ return ( - - {children} - + + + {children} + + ); diff --git a/apps/docs/src/app/providers.tsx b/apps/docs/src/app/providers.tsx new file mode 100644 index 00000000000..2a69d18923b --- /dev/null +++ b/apps/docs/src/app/providers.tsx @@ -0,0 +1,8 @@ +"use client"; + +import posthog from "posthog-js"; +import { PostHogProvider } from "posthog-js/react"; + +export function Providers({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/apps/docs/src/env.ts b/apps/docs/src/env.ts new file mode 100644 index 00000000000..7ebe35bef7b --- /dev/null +++ b/apps/docs/src/env.ts @@ -0,0 +1,33 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { vercel } from "@t3-oss/env-nextjs/presets-zod"; +import { z } from "zod"; + +export const env = createEnv({ + extends: [vercel()], + shared: { + NODE_ENV: z + .enum(["development", "production", "test"]) + .default("development"), + }, + + server: {}, + + client: { + NEXT_PUBLIC_COOKIE_DOMAIN: z.string().default("localhost"), + NEXT_PUBLIC_POSTHOG_KEY: z.string(), + NEXT_PUBLIC_POSTHOG_HOST: z + .string() + .url() + .default("https://us.i.posthog.com"), + }, + + experimental__runtimeEnv: { + NODE_ENV: process.env.NODE_ENV, + NEXT_PUBLIC_COOKIE_DOMAIN: process.env.NEXT_PUBLIC_COOKIE_DOMAIN, + NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, + NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, + }, + + skipValidation: !!process.env.SKIP_ENV_VALIDATION, + emptyStringAsUndefined: true, +}); diff --git a/apps/docs/src/instrumentation-client.ts b/apps/docs/src/instrumentation-client.ts new file mode 100644 index 00000000000..c8a26f49640 --- /dev/null +++ b/apps/docs/src/instrumentation-client.ts @@ -0,0 +1,22 @@ +import posthog from "posthog-js"; + +import { env } from "@/env"; + +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: env.NODE_ENV === "development", + cross_subdomain_cookie: true, + persistence: "cookie", + persistence_name: env.NEXT_PUBLIC_COOKIE_DOMAIN, + loaded: (posthog) => { + posthog.register({ + app_name: "docs", + domain: window.location.hostname, + }); + }, +}); diff --git a/apps/marketing/next.config.ts b/apps/marketing/next.config.ts index c9b9e812a6c..034b7731376 100644 --- a/apps/marketing/next.config.ts +++ b/apps/marketing/next.config.ts @@ -11,6 +11,25 @@ const config: NextConfig = { reactStrictMode: true, reactCompiler: true, typescript: { ignoreBuildErrors: true }, + + async rewrites() { + return [ + { + source: "/ingest/static/:path*", + destination: "https://us-assets.i.posthog.com/static/:path*", + }, + { + source: "/ingest/:path*", + destination: "https://us.i.posthog.com/:path*", + }, + { + source: "/ingest/decide", + destination: "https://us.i.posthog.com/decide", + }, + ]; + }, + + skipTrailingSlashRedirect: true, }; export default config; diff --git a/apps/marketing/package.json b/apps/marketing/package.json index 6c0656c0c35..f3371da3f55 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -22,6 +22,7 @@ "lucide-react": "^0.560.0", "next": "^16.0.10", "next-themes": "^0.4.6", + "posthog-js": "^1.306.1", "react": "^19.2.3", "react-dom": "^19.2.3", "react-fast-marquee": "^1.6.5", diff --git a/apps/marketing/src/app/layout.tsx b/apps/marketing/src/app/layout.tsx index e6c95fbc026..a17546bab03 100644 --- a/apps/marketing/src/app/layout.tsx +++ b/apps/marketing/src/app/layout.tsx @@ -1,16 +1,16 @@ import { ClerkProvider } from "@clerk/nextjs"; -import { THEME_STORAGE_KEY } from "@superset/shared/constants"; import type { Metadata } from "next"; import { IBM_Plex_Mono, Inter } from "next/font/google"; import Script from "next/script"; -import { ThemeProvider } from "next-themes"; +import { CookieConsent } from "@/components/CookieConsent"; import { env } from "@/env"; import { CTAButtons } from "./components/CTAButtons"; import { Footer } from "./components/Footer"; import { Header } from "./components/Header"; import "./globals.css"; +import { Providers } from "./providers"; const ibmPlexMono = IBM_Plex_Mono({ weight: ["300", "400", "500"], @@ -49,17 +49,12 @@ export default function RootLayout({ /> - +
} /> {children}