Tiago Fortunato
ProjectsOdysObservability

PostHog Analytics

PostHog: cookie-gated init, manual pageview, identify in dashboard

PostHog Analytics

This page covers the implementation of PostHog analytics in the client, specifically focusing on cookie-consent gated initialization, manual pageview tracking, and user identification within the authenticated dashboard.

PostHog is initialized only after the user has accepted cookies, ensuring compliance with privacy requirements. The PostHogProvider component in src/components/posthog-provider.tsx controls this behavior by checking localStorage for a previously stored consent value under the key odys_cookie_consent.

// src/components/posthog-provider.tsx
const CONSENT_KEY = "odys_cookie_consent"

if (localStorage.getItem(CONSENT_KEY) === "accepted") {
  initPostHog(key)
}

If consent was given in a prior session, PostHog initializes immediately. For new consent, the provider listens for the odys:cookie-accepted event dispatched by the CookieBanner component (src/components/cookie-banner.tsx) when the user clicks "Aceitar" (Accept). Upon receiving this event, initPostHog() is called with the environment key from NEXT_PUBLIC_POSTHOG_KEY.

The PostHog client is configured with:

  • capture_pageview: false — disables automatic pageview tracking
  • capture_pageleave: true — enables tracking when users leave the page
  • persistence: "localStorage+cookie" — stores user identity and session data

Initialization is idempotent via the initialized flag to prevent multiple calls to posthog.init().

Manual Pageview Tracking

Pageviews are tracked manually using the PageViewTracker component, which is conditionally rendered inside PostHogProvider. Automatic pageview capture is disabled in the PostHog configuration.

// src/components/posthog-provider.tsx
function PageViewTracker() {
  const pathname = usePathname()
  const searchParams = useSearchParams()
  const ph = usePostHog()

  useEffect(() => {
    ph.capture("$pageview", { $current_url: window.location.href })
  }, [pathname, searchParams, ph])

  return null
}

This component uses usePathname and useSearchParams from next/navigation to detect route changes in the App Router. The useEffect hook triggers a $pageview event on every change to the pathname or query parameters, ensuring accurate tracking without duplication — a known issue with automatic tracking in Next.js App Router.

Because useSearchParams requires Suspense, the tracker is wrapped in a Suspense boundary with fallback={null}.

User Identification

Authenticated users are identified in PostHog using the PostHogIdentify component (src/components/posthog-identify.tsx). This component should be rendered within the authenticated dashboard layout, where a userId is available.

// src/components/posthog-identify.tsx
export function PostHogIdentify({ userId }: { userId: string }) {
  useEffect(() => {
    posthog.identify(userId)
  }, [userId])
  return null
}

The useEffect hook calls posthog.identify(userId) when the component mounts or if the userId changes. This ensures all subsequent events are associated with the identified user in PostHog.

Known Gaps

  • Unused feature flag integration: The PostHog provider is initialized with feature flag support, but no posthog.isFeatureEnabled(...) calls exist in the codebase. Feature flags are currently not utilized.

Why this shape

The integration design emphasizes privacy compliance and tracking accuracy:

  • Cookie gating ensures tracking only begins after explicit consent.
  • Manual pageview capture avoids double-firing issues inherent in App Router’s automatic instrumentation.
  • Separate identification component isolates user identity logic, making it explicit and reusable within authenticated contexts.

On this page