mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat(blog): add PostHog analytics events for blog pages
- Add analytics.ts with blog-specific tracking events: - trackBlogIndexView: Track blog index views with post count - trackBlogPostView: Track individual post views with metadata - trackBlogPostScrollDepth: Track reading progress (25%, 50%, 75%, 100%) - trackBlogPostTimeSpent: Track time spent on posts - trackBlogPostShare: Track social share clicks - trackBlogPostCTAClick: Track CTA engagement - Add BlogIndexAnalytics and BlogPostAnalytics client components - Integrate analytics into /blog and /blog/[slug] pages Attribution is handled by PostHog save_referrer and save_campaign_params. MKT-74
This commit is contained in:
parent
6f782056ad
commit
bd1b3ad995
5 changed files with 221 additions and 0 deletions
|
|
@ -12,6 +12,7 @@ import {
|
|||
getArticleStructuredData,
|
||||
getBlogPostBreadcrumbStructuredData,
|
||||
} from "@/lib/blog"
|
||||
import { BlogPostAnalytics } from "@/components/blog/blog-analytics"
|
||||
|
||||
// Force dynamic rendering to evaluate publish gating at request-time
|
||||
export const dynamic = "force-dynamic"
|
||||
|
|
@ -87,6 +88,17 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
|
|||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }} />
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} />
|
||||
|
||||
{/* PostHog Analytics */}
|
||||
<BlogPostAnalytics
|
||||
post={{
|
||||
slug: post.slug,
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
tags: post.tags,
|
||||
publish_date: post.publish_date,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
|
||||
<article className="mx-auto max-w-3xl">
|
||||
{/* Back link */}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
getBlogCollectionStructuredData,
|
||||
getBlogBreadcrumbStructuredData,
|
||||
} from "@/lib/blog"
|
||||
import { BlogIndexAnalytics } from "@/components/blog/blog-analytics"
|
||||
|
||||
// Force dynamic rendering to evaluate publish gating at request-time
|
||||
export const dynamic = "force-dynamic"
|
||||
|
|
@ -65,6 +66,9 @@ export default function BlogIndex() {
|
|||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(collectionSchema) }} />
|
||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} />
|
||||
|
||||
{/* PostHog Analytics */}
|
||||
<BlogIndexAnalytics postCount={posts.length} />
|
||||
|
||||
<div className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
{/* Page Header */}
|
||||
|
|
|
|||
99
apps/web-roo-code/src/components/blog/blog-analytics.tsx
Normal file
99
apps/web-roo-code/src/components/blog/blog-analytics.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
import type { BlogPost } from "@/lib/blog"
|
||||
import { trackBlogIndexView, trackBlogPostView, trackBlogPostScrollDepth, trackBlogPostTimeSpent } from "@/lib/blog"
|
||||
|
||||
interface BlogIndexAnalyticsProps {
|
||||
postCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Client component that tracks blog index page view
|
||||
* Place this inside the blog index page
|
||||
*/
|
||||
export function BlogIndexAnalytics({ postCount }: BlogIndexAnalyticsProps) {
|
||||
const tracked = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!tracked.current) {
|
||||
trackBlogIndexView(postCount)
|
||||
tracked.current = true
|
||||
}
|
||||
}, [postCount])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
interface BlogPostAnalyticsProps {
|
||||
post: {
|
||||
slug: string
|
||||
title: string
|
||||
description: string
|
||||
tags: string[]
|
||||
publish_date: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Client component that tracks blog post view, scroll depth, and time spent
|
||||
* Place this inside the blog post page
|
||||
*/
|
||||
export function BlogPostAnalytics({ post }: BlogPostAnalyticsProps) {
|
||||
const trackedView = useRef(false)
|
||||
const trackedDepths = useRef<Set<25 | 50 | 75 | 100>>(new Set())
|
||||
const startTime = useRef<number>(Date.now())
|
||||
|
||||
useEffect(() => {
|
||||
// Capture start time for cleanup function
|
||||
const effectStartTime = startTime.current
|
||||
|
||||
// Track page view on mount
|
||||
if (!trackedView.current) {
|
||||
trackBlogPostView(post as BlogPost)
|
||||
trackedView.current = true
|
||||
}
|
||||
|
||||
// Track scroll depth
|
||||
const handleScroll = () => {
|
||||
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight
|
||||
if (scrollHeight <= 0) return
|
||||
|
||||
const scrollPercent = (window.scrollY / scrollHeight) * 100
|
||||
|
||||
const depths: (25 | 50 | 75 | 100)[] = [25, 50, 75, 100]
|
||||
for (const depth of depths) {
|
||||
if (scrollPercent >= depth && !trackedDepths.current.has(depth)) {
|
||||
trackedDepths.current.add(depth)
|
||||
trackBlogPostScrollDepth(post as BlogPost, depth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track time spent on page when leaving
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "hidden") {
|
||||
const timeSpent = Date.now() - effectStartTime
|
||||
trackBlogPostTimeSpent(post as BlogPost, timeSpent)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange)
|
||||
|
||||
// Check initial scroll position
|
||||
handleScroll()
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll)
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange)
|
||||
|
||||
// Track time spent when component unmounts
|
||||
const timeSpent = Date.now() - effectStartTime
|
||||
trackBlogPostTimeSpent(post as BlogPost, timeSpent)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [post.slug])
|
||||
|
||||
return null
|
||||
}
|
||||
96
apps/web-roo-code/src/lib/blog/analytics.ts
Normal file
96
apps/web-roo-code/src/lib/blog/analytics.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* Blog Analytics Events
|
||||
*
|
||||
* PostHog tracking events for the blog section.
|
||||
* These events help understand blog engagement and attribution.
|
||||
*/
|
||||
|
||||
import posthog from "posthog-js"
|
||||
import type { BlogPost } from "./types"
|
||||
|
||||
/**
|
||||
* Track blog index page view
|
||||
* Called when user views /blog
|
||||
*/
|
||||
export function trackBlogIndexView(postCount: number): void {
|
||||
if (typeof window === "undefined" || !posthog.__loaded) return
|
||||
|
||||
posthog.capture("blog_index_viewed", {
|
||||
post_count: postCount,
|
||||
page_type: "blog_index",
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Track blog post view
|
||||
* Called when user views /blog/[slug]
|
||||
*/
|
||||
export function trackBlogPostView(post: BlogPost): void {
|
||||
if (typeof window === "undefined" || !posthog.__loaded) return
|
||||
|
||||
posthog.capture("blog_post_viewed", {
|
||||
post_slug: post.slug,
|
||||
post_title: post.title,
|
||||
post_tags: post.tags,
|
||||
publish_date: post.publish_date,
|
||||
page_type: "blog_post",
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Track blog post scroll depth
|
||||
* Called at various scroll thresholds (25%, 50%, 75%, 100%)
|
||||
*/
|
||||
export function trackBlogPostScrollDepth(post: BlogPost, depth: 25 | 50 | 75 | 100): void {
|
||||
if (typeof window === "undefined" || !posthog.__loaded) return
|
||||
|
||||
posthog.capture("blog_post_scroll_depth", {
|
||||
post_slug: post.slug,
|
||||
post_title: post.title,
|
||||
scroll_depth: depth,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Track blog post share
|
||||
* Called when user clicks a share button
|
||||
*/
|
||||
export function trackBlogPostShare(post: BlogPost, platform: string): void {
|
||||
if (typeof window === "undefined" || !posthog.__loaded) return
|
||||
|
||||
posthog.capture("blog_post_shared", {
|
||||
post_slug: post.slug,
|
||||
post_title: post.title,
|
||||
share_platform: platform,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Track blog post CTA click
|
||||
* Called when user clicks a CTA within a blog post
|
||||
*/
|
||||
export function trackBlogPostCTAClick(post: BlogPost, ctaType: string, ctaTarget: string): void {
|
||||
if (typeof window === "undefined" || !posthog.__loaded) return
|
||||
|
||||
posthog.capture("blog_post_cta_click", {
|
||||
post_slug: post.slug,
|
||||
post_title: post.title,
|
||||
cta_type: ctaType,
|
||||
cta_target: ctaTarget,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Track time spent on blog post
|
||||
* Called when user leaves the page (or at intervals)
|
||||
*/
|
||||
export function trackBlogPostTimeSpent(post: BlogPost, timeMs: number): void {
|
||||
if (typeof window === "undefined" || !posthog.__loaded) return
|
||||
|
||||
posthog.capture("blog_post_time_spent", {
|
||||
post_slug: post.slug,
|
||||
post_title: post.title,
|
||||
time_spent_ms: timeMs,
|
||||
time_spent_seconds: Math.round(timeMs / 1000),
|
||||
})
|
||||
}
|
||||
|
|
@ -44,3 +44,13 @@ export {
|
|||
getBlogPostBreadcrumbStructuredData,
|
||||
getBlogPostUrl,
|
||||
} from "./structured-data"
|
||||
|
||||
// Analytics (PostHog events)
|
||||
export {
|
||||
trackBlogIndexView,
|
||||
trackBlogPostView,
|
||||
trackBlogPostScrollDepth,
|
||||
trackBlogPostShare,
|
||||
trackBlogPostCTAClick,
|
||||
trackBlogPostTimeSpent,
|
||||
} from "./analytics"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue