From 5258d01b4dc8918eedd34f181ba691895934c904 Mon Sep 17 00:00:00 2001 From: woohyeokk-choi Date: Sat, 11 Apr 2026 18:28:46 +0900 Subject: [PATCH 1/3] fix: eliminate FOUC on cookie accept and custom font load Remove the isInitialized state from PostHogProvider so the wrapper type stays stable (always PostHogReactProvider in prod) and React never unmounts/remounts children when analytics consent is given. Verified safe against posthog-js v1.367.0 source: all capture/identify calls are no-ops before init() via the __loaded guard. Add font-display: swap to all @font-face rules so custom fonts (Redaction, SF Pro) swap in without blocking render. Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/providers/posthog.tsx | 25 ++++++++++--------------- apps/web/src/styles.css | 7 +++++++ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/web/src/providers/posthog.tsx b/apps/web/src/providers/posthog.tsx index a79e96c933..30e8792fab 100644 --- a/apps/web/src/providers/posthog.tsx +++ b/apps/web/src/providers/posthog.tsx @@ -1,6 +1,6 @@ import { PostHogProvider as PostHogReactProvider } from "@posthog/react"; import posthog from "posthog-js"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { env } from "../env"; @@ -14,32 +14,27 @@ export function PostHogProvider({ enabled: boolean; }) { const didInitRef = useRef(false); - const [isInitialized, setIsInitialized] = useState(false); useEffect(() => { if ( typeof window === "undefined" || !enabled || !env.VITE_POSTHOG_API_KEY || - isDev + isDev || + didInitRef.current ) { - setIsInitialized(false); return; } - if (!didInitRef.current) { - posthog.init(env.VITE_POSTHOG_API_KEY, { - api_host: env.VITE_POSTHOG_HOST, - autocapture: true, - capture_pageview: true, - }); - didInitRef.current = true; - } - - setIsInitialized(true); + posthog.init(env.VITE_POSTHOG_API_KEY, { + api_host: env.VITE_POSTHOG_HOST, + autocapture: true, + capture_pageview: true, + }); + didInitRef.current = true; }, [enabled]); - if (!enabled || !env.VITE_POSTHOG_API_KEY || isDev || !isInitialized) { + if (!env.VITE_POSTHOG_API_KEY || isDev) { return <>{children}; } diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 4e7bd286f5..1ccaa05720 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -3,6 +3,7 @@ src: url("/fonts/Redaction-Regular.otf") format("opentype"); font-weight: normal; font-style: normal; + font-display: swap; } @font-face { @@ -10,6 +11,7 @@ src: url("/fonts/Redaction35-Regular.otf") format("opentype"); font-weight: 350; font-style: normal; + font-display: swap; } @font-face { @@ -17,6 +19,7 @@ src: url("/fonts/Redaction70-Regular.otf") format("opentype"); font-weight: 700; font-style: normal; + font-display: swap; } @font-face { @@ -24,6 +27,7 @@ src: url("/fonts/SF-Pro-Text-Regular.otf") format("opentype"); font-weight: 400; font-style: normal; + font-display: swap; } @font-face { @@ -31,6 +35,7 @@ src: url("/fonts/SF-Pro-Text-Medium.otf") format("opentype"); font-weight: 500; font-style: normal; + font-display: swap; } @font-face { @@ -38,6 +43,7 @@ src: url("/fonts/SF-Pro-Text-Semibold.otf") format("opentype"); font-weight: 600; font-style: normal; + font-display: swap; } @font-face { @@ -45,6 +51,7 @@ src: url("/fonts/SF-Pro-Text-Bold.otf") format("opentype"); font-weight: 700; font-style: normal; + font-display: swap; } @import "tailwindcss"; From 879178f35ed8b4003e1aff58f35596a62ed9a428 Mon Sep 17 00:00:00 2001 From: woohyeokk-choi Date: Mon, 13 Apr 2026 10:48:32 +0900 Subject: [PATCH 2/3] fix: restore mount-time analytics via analyticsReady context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PostHogReadyContext to PostHogProvider so consumers know when posthog.init() has actually run. useAnalytics now guards track/identify/ reset with analyticsReady and includes it in useCallback deps. When analyticsReady flips false→true, track/identify refs change and any useEffect depending on them (hero_section_viewed, auth identify, billing identify) re-fires against the initialized client — no events lost, no replay of pre-consent calls, GDPR compliant. Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/hooks/use-posthog.ts | 20 +++++++++++++------- apps/web/src/providers/posthog.tsx | 20 +++++++++++++++++--- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/web/src/hooks/use-posthog.ts b/apps/web/src/hooks/use-posthog.ts index be6f485d9a..8b9948393d 100644 --- a/apps/web/src/hooks/use-posthog.ts +++ b/apps/web/src/hooks/use-posthog.ts @@ -1,45 +1,51 @@ import { usePostHog } from "@posthog/react"; import { useCallback } from "react"; +import { usePostHogReady } from "@/providers/posthog"; + export { usePostHog }; /** - * Hook for type-safe PostHog event tracking + * Hook for type-safe PostHog event tracking. + * All callbacks are stable references that update when PostHog initializes, + * so mount-time useEffects depending on them will re-run after init. */ export function useAnalytics() { const posthog = usePostHog(); + const analyticsReady = usePostHogReady(); const track = useCallback( (eventName: string, properties?: Record) => { - if (!posthog) { + if (!analyticsReady || !posthog) { return; } posthog.capture(eventName, properties); }, - [posthog], + [posthog, analyticsReady], ); const identify = useCallback( (userId: string, properties?: Record) => { - if (!posthog) { + if (!analyticsReady || !posthog) { return; } posthog.identify(userId, properties); }, - [posthog], + [posthog, analyticsReady], ); const reset = useCallback(() => { - if (!posthog) { + if (!analyticsReady || !posthog) { return; } posthog.reset(); - }, [posthog]); + }, [posthog, analyticsReady]); return { track, identify, reset, posthog, + analyticsReady, }; } diff --git a/apps/web/src/providers/posthog.tsx b/apps/web/src/providers/posthog.tsx index 30e8792fab..d7b5dd0c73 100644 --- a/apps/web/src/providers/posthog.tsx +++ b/apps/web/src/providers/posthog.tsx @@ -1,11 +1,17 @@ import { PostHogProvider as PostHogReactProvider } from "@posthog/react"; import posthog from "posthog-js"; -import { useEffect, useRef } from "react"; +import { createContext, useContext, useEffect, useRef, useState } from "react"; import { env } from "../env"; const isDev = import.meta.env.DEV; +const PostHogReadyContext = createContext(false); + +export function usePostHogReady() { + return useContext(PostHogReadyContext); +} + export function PostHogProvider({ children, enabled, @@ -14,6 +20,7 @@ export function PostHogProvider({ enabled: boolean; }) { const didInitRef = useRef(false); + const [isInitialized, setIsInitialized] = useState(false); useEffect(() => { if ( @@ -32,13 +39,20 @@ export function PostHogProvider({ capture_pageview: true, }); didInitRef.current = true; + setIsInitialized(true); }, [enabled]); if (!env.VITE_POSTHOG_API_KEY || isDev) { - return <>{children}; + return ( + + {children} + + ); } return ( - {children} + + {children} + ); } From 83474af18d4856d7a7b52e1dd3cee9d38dfe437e Mon Sep 17 00:00:00 2001 From: woohyeokk-choi Date: Mon, 13 Apr 2026 11:13:13 +0900 Subject: [PATCH 3/3] fix: reset analyticsReady on consent withdrawal and allow re-enable Restore setIsInitialized(false) in the disabled path so cross-tab consent withdrawal immediately stops event capture. Move didInitRef back into a nested guard so setIsInitialized(true) remains reachable on re-enable without re-calling posthog.init(). Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/providers/posthog.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/web/src/providers/posthog.tsx b/apps/web/src/providers/posthog.tsx index d7b5dd0c73..5128b477ba 100644 --- a/apps/web/src/providers/posthog.tsx +++ b/apps/web/src/providers/posthog.tsx @@ -27,18 +27,21 @@ export function PostHogProvider({ typeof window === "undefined" || !enabled || !env.VITE_POSTHOG_API_KEY || - isDev || - didInitRef.current + isDev ) { + setIsInitialized(false); return; } - posthog.init(env.VITE_POSTHOG_API_KEY, { - api_host: env.VITE_POSTHOG_HOST, - autocapture: true, - capture_pageview: true, - }); - didInitRef.current = true; + if (!didInitRef.current) { + posthog.init(env.VITE_POSTHOG_API_KEY, { + api_host: env.VITE_POSTHOG_HOST, + autocapture: true, + capture_pageview: true, + }); + didInitRef.current = true; + } + setIsInitialized(true); }, [enabled]);