fix: add attribution tracking and correct blog event names

- Fix $pageview to include $referrer, $referring_domain, and UTM params
- Track previous URL for SPA navigation referrer attribution
- Rename events to blog_index_view and blog_post_view (per spec)
- Add publish_time_pt property to blog_post_view
- Add trackBlogSubstackClick function for use with MKT-72
This commit is contained in:
Roo Code 2026-01-27 20:04:24 +00:00
parent bd1b3ad995
commit bfa4c67ebc
3 changed files with 81 additions and 10 deletions

View file

@ -3,26 +3,85 @@
import { usePathname, useSearchParams } from "next/navigation" import { usePathname, useSearchParams } from "next/navigation"
import posthog from "posthog-js" import posthog from "posthog-js"
import { PostHogProvider as OriginalPostHogProvider } from "posthog-js/react" import { PostHogProvider as OriginalPostHogProvider } from "posthog-js/react"
import { useEffect, Suspense } from "react" import { useEffect, useRef, Suspense } from "react"
import { hasConsent } from "@/lib/analytics/consent-manager" import { hasConsent } from "@/lib/analytics/consent-manager"
/**
* Extract referring domain from a URL string
*/
function getReferringDomain(referrer: string): string {
if (!referrer) return ""
try {
const url = new URL(referrer)
return url.hostname
} catch {
return ""
}
}
/**
* Get UTM parameters from current URL search params
*/
function getUtmParams(searchParams: URLSearchParams | null): Record<string, string> {
const utmKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"]
const utmParams: Record<string, string> = {}
if (searchParams) {
for (const key of utmKeys) {
const value = searchParams.get(key)
if (value) {
utmParams[key] = value
}
}
}
return utmParams
}
function PageViewTracker() { function PageViewTracker() {
const pathname = usePathname() const pathname = usePathname()
const searchParams = useSearchParams() const searchParams = useSearchParams()
// Track previous URL for SPA navigation referrer
const previousUrl = useRef<string | null>(null)
// Store original document.referrer for first page load
const initialReferrer = useRef<string | null>(null)
// Track page views // Capture initial referrer on mount
useEffect(() => {
if (typeof window !== "undefined" && initialReferrer.current === null) {
initialReferrer.current = document.referrer || ""
}
}, [])
// Track page views with proper attribution
useEffect(() => { useEffect(() => {
if (pathname && process.env.NEXT_PUBLIC_POSTHOG_KEY) { if (pathname && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
let url = window.location.origin + pathname let url = window.location.origin + pathname
if (searchParams && searchParams.toString()) { if (searchParams && searchParams.toString()) {
url = url + `?${searchParams.toString()}` url = url + `?${searchParams.toString()}`
} }
// Determine the referrer:
// - For first pageview: use document.referrer (external referrer)
// - For SPA navigations: use previous URL within the site
const referrer = previousUrl.current || initialReferrer.current || ""
const referringDomain = getReferringDomain(referrer)
// Collect UTM parameters
const utmParams = getUtmParams(searchParams)
posthog.capture("$pageview", { posthog.capture("$pageview", {
$current_url: url, $current_url: url,
$referrer: referrer,
$referring_domain: referringDomain,
...utmParams,
}) })
// Update previous URL for next navigation
previousUrl.current = url
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname, searchParams.toString()]) }, [pathname, searchParams?.toString()])
return null return null
} }

View file

@ -15,9 +15,8 @@ import type { BlogPost } from "./types"
export function trackBlogIndexView(postCount: number): void { export function trackBlogIndexView(postCount: number): void {
if (typeof window === "undefined" || !posthog.__loaded) return if (typeof window === "undefined" || !posthog.__loaded) return
posthog.capture("blog_index_viewed", { posthog.capture("blog_index_view", {
post_count: postCount, post_count: postCount,
page_type: "blog_index",
}) })
} }
@ -28,12 +27,24 @@ export function trackBlogIndexView(postCount: number): void {
export function trackBlogPostView(post: BlogPost): void { export function trackBlogPostView(post: BlogPost): void {
if (typeof window === "undefined" || !posthog.__loaded) return if (typeof window === "undefined" || !posthog.__loaded) return
posthog.capture("blog_post_viewed", { posthog.capture("blog_post_view", {
post_slug: post.slug, slug: post.slug,
post_title: post.title, title: post.title,
post_tags: post.tags, tags: post.tags,
publish_date: post.publish_date, publish_date: post.publish_date,
page_type: "blog_post", publish_time_pt: post.publish_time_pt,
})
}
/**
* Track Substack subscribe click
* Called when user clicks the Substack subscribe link
*/
export function trackBlogSubstackClick(source: "footer" | "blog_index" | "blog_post"): void {
if (typeof window === "undefined" || !posthog.__loaded) return
posthog.capture("blog_substack_click", {
source,
}) })
} }

View file

@ -53,4 +53,5 @@ export {
trackBlogPostShare, trackBlogPostShare,
trackBlogPostCTAClick, trackBlogPostCTAClick,
trackBlogPostTimeSpent, trackBlogPostTimeSpent,
trackBlogSubstackClick,
} from "./analytics" } from "./analytics"