Tiago Fortunato
ProjectsOdysObservability

PostHog Observability

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

PostHog Observability

The odys application integrates PostHog for product analytics, providing insights into user behavior and platform usage. This integration is carefully designed with user privacy in mind, ensuring that analytics are only collected after explicit user consent. The system employs a cookie-gated initialization, manual pageview tracking, and explicit user identification to give fine-grained control over data collection.

Overview

The PostHog integration in odys is structured across three primary components: PostHogProvider, PageViewTracker, and PostHogIdentify, all orchestrated by a CookieBanner component that manages user consent.

The core of the integration resides in src/components/posthog-provider.tsx. This component is responsible for initializing the PostHog client-side library, but critically, it only does so if the user has explicitly accepted cookies. This consent is managed by the CookieBanner component, found in src/components/cookie-banner.tsx, which stores the user's preference in localStorage.

Once PostHog is initialized, page views are not automatically captured by the library. Instead, a dedicated PageViewTracker component, nested within the PostHogProvider, takes on the responsibility of manually dispatching $pageview events. This approach allows for precise control over what constitutes a page view within the Next.js application's routing lifecycle.

Finally, to connect anonymous user behavior with authenticated user identities, the PostHogIdentify component in src/components/posthog-identify.tsx is used. This component ensures that once a user is known, their subsequent actions can be attributed to their specific userId, which correlates with the userId column in the professionals table, a text type column that is unique for each professional.

Before checking for cookie consent, the PostHogProvider first verifies the presence of NEXT_PUBLIC_POSTHOG_KEY. If this environment variable is not defined, PostHog initialization is skipped entirely, and the application proceeds without analytics tracking.

The journey of PostHog data collection begins with user consent, managed by the CookieBanner component. When a user first visits the odys application, the CookieBanner checks localStorage for a key named odys_cookie_consent. If this key is not present, the banner becomes visible, prompting the user to either "Aceitar" (Accept) or "Recusar" (Decline) cookies.

Upon accepting, the accept function in src/components/cookie-banner.tsx sets localStorage.setItem(STORAGE_KEY, "accepted") and, crucially, dispatches a custom browser event: window.dispatchEvent(new Event("odys:cookie-accepted")). This event acts as a signal to the PostHogProvider.

The PostHogProvider listens for this odys:cookie-accepted event. When it's fired, or if the odys_cookie_consent key is already set to "accepted" from a previous session, the initPostHog function is invoked. To prevent redundant calls and ensure posthog.init is executed only once per session, the initPostHog function employs a module-level initialized flag. This function, defined in src/components/posthog-provider.tsx, initializes the PostHog client with the NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST. Key configurations here include capture_pageview: false, which explicitly disables PostHog's default automatic pageview tracking, deferring this responsibility to a separate component, and capture_pageleave: true, which enables automatic tracking of when users leave a page. The persistence option is set to "localStorage+cookie", allowing PostHog to store its data in both mechanisms, which is a common practice for analytics.

If the user declines cookies, localStorage.setItem(STORAGE_KEY, "declined") is set, and the initPostHog function is never called, effectively preventing any PostHog data collection for that session.

Manual Page View Tracking

With capture_pageview explicitly disabled during initialization, odys takes a manual approach to tracking page views. This is handled by the PageViewTracker component, which is rendered within the PHProvider in src/components/posthog-provider.tsx.

The PageViewTracker leverages Next.js's client-side routing hooks, usePathname and useSearchParams, to detect changes in the URL. Inside a useEffect hook, whenever the pathname or searchParams change, indicating a navigation event, the ph.capture("$pageview", { $current_url: window.location.href }) method is called. This ensures that every significant page navigation is recorded as a $pageview event, providing a clear trail of user navigation within the application. The PageViewTracker is wrapped in a Suspense boundary, which is a requirement for using useSearchParams in a client component.

User Identification

To enrich the analytics data with user-specific information, the PostHogIdentify component is used. This component, located in src/components/posthog-identify.tsx, takes a userId prop. When this component mounts or when the userId prop changes, a useEffect hook calls posthog.identify(userId).

This action tells PostHog that the current anonymous session should now be associated with the provided userId. This is crucial for understanding the behavior of individual users over time, allowing odys to segment and analyze data based on authenticated user profiles. For instance, a userId could correspond to the userId column in the professionals table, which is a text type and unique, ensuring a consistent identifier across the platform.

Design decisions

The design choices for PostHog integration in odys reflect a strong emphasis on user privacy and control, balanced with the need for valuable product analytics.

The decision to gate PostHog initialization behind explicit cookie consent, using localStorage and a custom odys:cookie-accepted event, is a direct response to privacy regulations and user expectations. This ensures that no analytics data is collected before a user has given their permission, fostering trust. The trade-off is that initial page loads for new users who haven't yet accepted cookies will not be tracked, potentially leading to a slight undercount of very early user interactions.

Disabling capture_pageview and implementing a manual PageViewTracker provides granular control over what constitutes a "page view." While PostHog's automatic tracking is convenient, manual tracking allows odys to define page boundaries precisely, especially in single-page application (SPA) contexts where URL changes might not always correspond to a new "page" from an analytics perspective. The trade-off is increased boilerplate code and the need to ensure PageViewTracker is correctly placed and configured. The use of Suspense around PageViewTracker is a technical necessity for useSearchParams in Next.js client components, adding a layer of complexity but ensuring compatibility.

Separating user identification into its own PostHogIdentify component allows for flexible placement within the application. This component can be rendered conditionally or with a dynamic userId once a user authenticates, ensuring that identification happens at the appropriate time without coupling it tightly to the main PostHogProvider. This modularity improves maintainability and clarity.

Potential improvements

  1. Centralize Cookie Consent Key: The cookie consent key is currently defined as CONSENT_KEY in src/components/posthog-provider.tsx and STORAGE_KEY in src/components/cookie-banner.tsx. While their values are identical ("odys_cookie_consent"), using two different variable names for the same logical key can lead to confusion or potential inconsistencies if one were to be updated without the other. A single, shared constant for this key would improve maintainability.
  2. Explicit Opt-Out for PostHog: Currently, if a user declines cookies, PostHog is simply not initialized. However, if PostHog was previously initialized (e.g., the user accepted, then later declined), there isn't an explicit mechanism to "opt-out" or disable PostHog tracking for the current session. While persistence: "localStorage+cookie" helps, an explicit posthog.opt_out_capturing() call in the decline function of src/components/cookie-banner.tsx could provide a more robust and immediate opt-out experience.
  3. Handle NEXT_PUBLIC_POSTHOG_KEY more robustly: In src/components/posthog-provider.tsx, the initPostHog function is called with key!, asserting that key will not be null. While the useEffect checks if (!key) return, it might be cleaner to pass key as a required prop to initPostHog or ensure its presence earlier, removing the need for the non-null assertion.
  4. Reset Identity on Logout: The PostHogIdentify component calls posthog.identify(userId) when userId changes. However, there's no explicit posthog.reset() call when a user logs out. Without a reset, events might continue to be associated with the previous user's identity until a new userId is provided or the session expires. Implementing a PostHogReset component or a similar mechanism on logout would ensure proper user session separation in analytics.

References

  • src/components/posthog-provider.tsx
  • src/components/posthog-identify.tsx
  • src/components/cookie-banner.tsx

On this page