Posthog Implementation
PostHog Analytics & A/B Testing Implementation Guide
Overview
This guide covers implementing PostHog for:
- Page view tracking with CMS context (page IDs, content IDs, locale)
- Event tracking (scroll milestones, time on page, CTA clicks)
- A/B testing with feature flags
1. Installation & Initialization
Install PostHog
npm install posthog-js
Initialize in instrumentation-client.ts
import posthog from 'posthog-js' declare global { interface Window { posthog?: typeof posthog } } const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY const postHogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST if (postHogKey && postHogHost) { posthog.init(postHogKey, { api_host: postHogHost, defaults: '2025-05-24' }) // Expose on window for provider access window.posthog = posthog }
Environment Variables
NEXT_PUBLIC_POSTHOG_KEY=phc_xxx NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
2. Analytics Provider Pattern
Create a provider abstraction to decouple your app from PostHog directly:
// posthog-provider.ts function getPostHog() { if (typeof window === 'undefined') return null const posthog = window.posthog return posthog?.__loaded ? posthog : null } // Queue events that arrive before PostHog is ready const eventQueue: QueuedEvent[] = [] export const PostHogProvider: AnalyticsProvider = { page(name: string, properties?: PageViewProperties) { const posthog = getPostHog() if (!posthog) { eventQueue.push({ type: 'page', name, properties }) waitForPostHogAndFlush() return } posthog.capture('$pageview', { $current_url: window.location.href, $pathname: properties?.path, $title: properties?.title, locale: properties?.locale, // CMS context pageID: properties?.pageID, contentIDs: properties?.contentIDs, }) }, track(event: string, properties?: EventProperties) { const posthog = getPostHog() if (!posthog) { eventQueue.push({ type: 'track', event, properties }) waitForPostHogAndFlush() return } posthog.capture(event, properties) }, isReady(): boolean { return getPostHog() !== null } }
3. Tracking CMS Content IDs
Add Data Attributes to Components
Components should include data-agility-component={contentID}:
<Container data-agility-component={contentID}> {/* component content */} </Container>
Extract Content IDs from DOM
// agility-context.ts export function getAgilityContext(): AgilityContext { if (typeof document === 'undefined') return {} const contentIDs: number[] = [] // Find page ID const pageElement = document.querySelector('[data-agility-page]') const pageID = pageElement?.getAttribute('data-agility-page') // Find all component content IDs document.querySelectorAll('[data-agility-component]').forEach((el) => { const id = parseInt(el.getAttribute('data-agility-component') || '', 10) if (!isNaN(id) && !contentIDs.includes(id)) { contentIDs.push(id) } }) return { pageID: pageID ? parseInt(pageID, 10) : undefined, contentIDs } }
4. A/B Testing with Feature Flags
Client Component for A/B Testing
'use client' import { useFeatureFlagVariantKey, usePostHog } from 'posthog-js/react' export function ABTestComponent({ variants }: { variants: Variant[] }) { const posthog = usePostHog() const variantKey = useFeatureFlagVariantKey('your-experiment-name') const [isLoading, setIsLoading] = useState(true) useEffect(() => { if (variantKey !== undefined) { setIsLoading(false) // Track experiment exposure posthog?.capture('$experiment_exposure', { experimentName: 'your-experiment-name', variantKey, contentID: contentID }) } }, [variantKey]) if (isLoading) { return <SkeletonLoader /> // Prevent flicker } const activeVariant = variants.find(v => v.key === variantKey) || variants[0] return <RenderVariant variant={activeVariant} /> }
5. HogQL Queries for Analytics
Query Pageviews by Content ID
SELECT count() as impressions FROM events WHERE event = '$pageview' AND has(JSONExtractArrayRaw(properties, 'contentIDs'), toString(27)) AND JSONExtractString(properties, 'locale') = 'en-us' AND timestamp > now() - INTERVAL 30 DAY
Query Pages Containing a Content ID
SELECT JSONExtractInt(properties, 'pageID') as pageID, count() as views FROM events WHERE event = '$pageview' AND has(JSONExtractArrayRaw(properties, 'contentIDs'), toString(27)) AND JSONExtractInt(properties, 'pageID') > 0 GROUP BY pageID ORDER BY views DESC
Query Scroll Depth for Content
SELECT avg(JSONExtractInt(properties, 'depth')) as avgDepth FROM events WHERE event = 'scroll_milestone' AND has(JSONExtractArrayRaw(properties, 'contentIDs'), toString(27)) AND timestamp > now() - INTERVAL 30 DAY
Query Time on Page Distribution
SELECT JSONExtractInt(properties, 'seconds') as seconds, count() as count FROM events WHERE event = 'time_milestone' AND has(JSONExtractArrayRaw(properties, 'contentIDs'), toString(27)) AND timestamp > now() - INTERVAL 30 DAY GROUP BY seconds ORDER BY seconds
Query CTA Clicks by Content ID
SELECT count() as clicks FROM events WHERE event = 'outbound_link_clicked' AND JSONExtractInt(properties, 'contentID') = 27 AND timestamp > now() - INTERVAL 30 DAY
6. Key Implementation Notes
Event Queuing
PostHog may not be ready when your app starts. Queue events and flush when ready to avoid losing early pageviews.
Bot Detection
PostHog filters bots by default. Automated testing (Playwright, Puppeteer, Chrome DevTools Protocol) may trigger bot detection due to navigator.webdriver = true. Events won't be sent in these environments.
Batching
PostHog batches events and sends them periodically (~30 seconds) or on page unload. Events won't appear instantly in the network tab during development.
Content ID Tracking
Use the contentIDs array to track which CMS components appear on each page. This enables per-component analytics like:
- Which content gets the most impressions
- Scroll depth by content
- Time spent viewing specific content
A/B Test Flicker Prevention
Use skeleton loaders while waiting for feature flag evaluation to prevent content flicker. The variant should only render after useFeatureFlagVariantKey returns a defined value.
Container Component Pattern
Ensure wrapper components pass through data attributes:
export function Container({ className, children, ...props // This spreads data-agility-component and other attributes }: { className?: string children: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>) { return ( <div className={clsx(className, 'px-6 lg:px-8')} {...props}> <div className="mx-auto max-w-2xl lg:max-w-7xl">{children}</div> </div> ) }