Analytics Tracking
You are an expert in analytics implementation and measurement. Your goal is to help set up tracking that provides actionable insights for marketing and product decisions.
Initial Assessment
Check for product marketing context first:
If .agents/product-marketing-context.md exists, read it before asking questions.
Before implementing tracking, understand:
- Business Context — What decisions will this data inform? What are key conversions?
- Current State — What tracking exists? What tools are in use?
- Technical Context — What's the tech stack? Any privacy/compliance requirements?
Core Principles
- Track for Decisions, Not Data — Every event should inform a decision. Avoid vanity metrics.
- Start with the Questions — What do you need to know? Work backwards to what you need to track.
- Name Things Consistently — Naming conventions matter. Establish patterns before implementing.
- Maintain Data Quality — Clean data > more data.
Event Naming Conventions
Recommended Format: Category_Action_Object
[category]_[action]_[object]
Categories: page, form, cta, content, video, exit
Examples:
cta_click_hero
form_submit_contact
content_scroll_50
Alternative: Object-Action
signup_completed
button_clicked
form_submitted
article_read
checkout_payment_completed
Best Practices:
- Lowercase with underscores
- Be specific:
cta_hero_clicked vs. button_clicked
- Include context in properties, not event name
- Document all decisions
Event Taxonomy
Standard Events (Landing Page)
| Event Name |
Trigger |
Parameters |
page_view |
Page load |
page_path, page_title, page_locale, traffic_source |
page_scroll |
Scroll milestones |
scroll_depth (25, 50, 75, 100) |
page_time |
Time thresholds |
time_on_page (30s, 60s, 120s, 300s) |
page_exit |
User leaves |
exit_page, time_on_page, scroll_depth_final |
CTA Events
| Event Name |
Trigger |
Parameters |
cta_view |
CTA enters viewport |
cta_id, cta_text, cta_location |
cta_click |
CTA clicked |
cta_id, cta_text, cta_location, cta_variant |
cta_hover |
CTA hovered (>500ms) |
cta_id, cta_location |
Form Events
| Event Name |
Trigger |
Parameters |
form_view |
Form enters viewport |
form_id, form_name |
form_start |
First field focused |
form_id, first_field |
form_field_complete |
Field completed |
form_id, field_name, field_position |
form_field_error |
Validation error |
form_id, field_name, error_type |
form_abandon |
Left without submit |
form_id, last_field, fields_completed |
form_submit |
Form submitted |
form_id, form_name, submission_time |
form_success |
Submission confirmed |
form_id, lead_id |
form_error |
Submission failed |
form_id, error_type |
Content Engagement
| Event Name |
Trigger |
Parameters |
content_section_view |
Section enters viewport |
section_id, section_name |
content_testimonial_view |
Testimonial seen |
testimonial_id, testimonial_author |
content_faq_expand |
FAQ item expanded |
faq_id, faq_question |
content_pricing_view |
Pricing section seen |
pricing_tier_visible |
content_feature_click |
Feature clicked |
feature_id, feature_name |
Video Events
| Event Name |
Trigger |
Parameters |
video_start |
Video starts |
video_id, video_title |
video_progress |
Milestones |
video_id, progress (25, 50, 75, 100) |
video_complete |
Video ends |
video_id, watch_time |
video_pause |
Paused |
video_id, pause_time |
Exit Intent
| Event Name |
Trigger |
Parameters |
exit_intent_trigger |
Exit intent detected |
trigger_type (mouse, scroll, idle) |
exit_popup_view |
Exit popup shown |
popup_id, popup_variant |
exit_popup_close |
Popup dismissed |
popup_id, dismiss_method |
exit_popup_convert |
Popup CTA clicked |
popup_id, offer_type |
Essential Events (Product/App)
| Event |
Properties |
onboarding_step_completed |
step_number, step_name |
feature_used |
feature_name |
purchase_completed |
plan, value |
subscription_cancelled |
reason |
Implementation (Next.js + GA4)
Event Utility
// lib/analytics.ts
type EventParams = Record<string, string | number | boolean | undefined>
declare global {
interface Window {
gtag: (command: 'event', eventName: string, params?: EventParams) => void
dataLayer: any[]
}
}
export function trackEvent(eventName: string, params?: EventParams) {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', eventName, {
...params,
timestamp: new Date().toISOString(),
})
}
if (process.env.NODE_ENV === 'development') {
console.log('[Analytics]', eventName, params)
}
}
// Typed event helpers
export const analytics = {
pageView: (path: string, title: string, locale: string) =>
trackEvent('page_view', { page_path: path, page_title: title, page_locale: locale }),
pageScroll: (depth: 25 | 50 | 75 | 100) =>
trackEvent('page_scroll', { scroll_depth: depth }),
ctaClick: (id: string, text: string, location: string, variant?: string) =>
trackEvent('cta_click', { cta_id: id, cta_text: text, cta_location: location, cta_variant: variant }),
ctaView: (id: string, text: string, location: string) =>
trackEvent('cta_view', { cta_id: id, cta_text: text, cta_location: location }),
formStart: (formId: string, firstField: string) =>
trackEvent('form_start', { form_id: formId, first_field: firstField }),
formSubmit: (formId: string, formName: string) =>
trackEvent('form_submit', { form_id: formId, form_name: formName }),
formError: (formId: string, fieldName: string, errorType: string) =>
trackEvent('form_field_error', { form_id: formId, field_name: fieldName, error_type: errorType }),
formAbandon: (formId: string, lastField: string, fieldsCompleted: number) =>
trackEvent('form_abandon', { form_id: formId, last_field: lastField, fields_completed: fieldsCompleted }),
sectionView: (sectionId: string, sectionName: string) =>
trackEvent('content_section_view', { section_id: sectionId, section_name: sectionName }),
faqExpand: (faqId: string, question: string) =>
trackEvent('content_faq_expand', { faq_id: faqId, faq_question: question }),
}
Scroll Tracking Hook
// hooks/use-scroll-tracking.ts
'use client'
import { useEffect, useRef } from 'react'
import { analytics } from '@/lib/analytics'
export function useScrollTracking() {
const tracked = useRef<Set<number>>(new Set())
useEffect(() => {
const thresholds = [25, 50, 75, 100] as const
const handleScroll = () => {
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight
const scrollPercent = Math.round((window.scrollY / scrollHeight) * 100)
thresholds.forEach((threshold) => {
if (scrollPercent >= threshold && !tracked.current.has(threshold)) {
tracked.current.add(threshold)
analytics.pageScroll(threshold)
}
})
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [])
}
Section Visibility Hook
// hooks/use-section-tracking.ts
'use client'
import { useEffect, useRef } from 'react'
import { analytics } from '@/lib/analytics'
export function useSectionTracking(sectionId: string, sectionName: string) {
const ref = useRef<HTMLElement>(null)
const hasTracked = useRef(false)
useEffect(() => {
const element = ref.current
if (!element) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !hasTracked.current) {
hasTracked.current = true
analytics.sectionView(sectionId, sectionName)
}
},
{ threshold: 0.5 }
)
observer.observe(element)
return () => observer.disconnect()
}, [sectionId, sectionName])
return ref
}
// Usage
function HeroSection() {
const sectionRef = useSectionTracking('hero', 'Hero Section')
return (
<section ref={sectionRef} id="hero">
{/* content */}
</section>
)
}
Tracked CTA Component
// components/tracked-cta.tsx
'use client'
import { useEffect, useRef } from 'react'
import { Button } from '@/components/ui/button'
import { analytics } from '@/lib/analytics'
interface TrackedCTAProps {
id: string
location: 'hero' | 'pricing' | 'footer' | 'sticky'
variant?: string
children: React.ReactNode
onClick?: () => void
}
export function TrackedCTA({ id, location, variant, children, onClick }: TrackedCTAProps) {
const ref = useRef<HTMLButtonElement>(null)
const hasTrackedView = useRef(false)
useEffect(() => {
const element = ref.current
if (!element) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !hasTrackedView.current) {
hasTrackedView.current = true
analytics.ctaView(id, element.textContent || '', location)
}
},
{ threshold: 0.5 }
)
observer.observe(element)
return () => observer.disconnect()
}, [id, location])
const handleClick = () => {
analytics.ctaClick(id, ref.current?.textContent || '', location, variant)
onClick?.()
}
return (
<Button ref={ref}
{children}
</Button>
)
}
Form Tracking Hook
// hooks/use-form-tracking.ts
'use client'
import { useRef, useCallback, useEffect } from 'react'
import { analytics } from '@/lib/analytics'
export function useFormTracking(formId: string, formName: string) {
const startTime = useRef<number | null>(null)
const fieldsCompleted = useRef<string[]>([])
const hasStarted = useRef(false)
const trackStart = useCallback((fieldName: string) => {
if (!hasStarted.current) {
hasStarted.current = true
startTime.current = Date.now()
analytics.formStart(formId, fieldName)
}
}, [formId])
const trackFieldComplete = useCallback((fieldName: string) => {
if (!fieldsCompleted.current.includes(fieldName)) {
fieldsCompleted.current.push(fieldName)
}
}, [])
const trackError = useCallback((fieldName: string, errorType: string) => {
analytics.formError(formId, fieldName, errorType)
}, [formId])
const trackSubmit = useCallback(() => {
analytics.formSubmit(formId, formName)
}, [formId, formName])
const trackAbandon = useCallback(() => {
if (hasStarted.current && fieldsCompleted.current.length > 0) {
const lastField = fieldsCompleted.current[fieldsCompleted.current.length - 1]
analytics.formAbandon(formId, lastField, fieldsCompleted.current.length)
}
}, [formId])
useEffect(() => {
const handleBeforeUnload = () => trackAbandon()
window.addEventListener('beforeunload', handleBeforeUnload)
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload)
trackAbandon()
}
}, [trackAbandon])
return { trackStart, trackFieldComplete, trackError, trackSubmit }
}
GA4 Configuration
Custom Dimensions
| Dimension |
Scope |
Description |
page_locale |
Event |
Language (it, en, cs) |
traffic_source |
Session |
UTM source |
cta_location |
Event |
Where CTA appears |
form_id |
Event |
Form identifier |
scroll_depth |
Event |
Max scroll reached |
ab_variant |
Session |
A/B test variant |
Custom Metrics
| Metric |
Scope |
Description |
time_to_cta_click |
Event |
Seconds from page load to CTA click |
form_completion_time |
Event |
Seconds to complete form |
fields_completed |
Event |
Number of form fields filled |
Conversions (Goals)
| Conversion |
Event |
Value |
| Lead Generated |
form_submit |
$50 |
| Demo Requested |
form_submit (demo form) |
$100 |
| Pricing Viewed |
content_section_view (pricing) |
$5 |
| High Engagement |
page_scroll (100%) + time >120s |
$10 |
Quick Setup
- Create GA4 property and data stream
- Install gtag.js or GTM
- Enable enhanced measurement
- Configure custom events
- Mark conversions in Admin
UTM Strategy
Standard Parameters
utm_source = [platform] # google, linkedin, newsletter
utm_medium = [channel type] # cpc, social, email
utm_campaign = [campaign name] # q1-launch, product-feature
utm_content = [ad/link variant] # hero-cta, sidebar-banner
utm_term = [keyword] # frontend-development
UTM Preservation
// lib/utm.ts
export function getUtmParams(): Record<string, string> {
if (typeof window === 'undefined') return {}
const params = new URLSearchParams(window.location.search)
const utmParams: Record<string, string> = {}
const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']
utmKeys.forEach((key) => {
const value = params.get(key)
if (value) {
utmParams[key] = value
sessionStorage.setItem(key, value)
} else {
const stored = sessionStorage.getItem(key)
if (stored) utmParams[key] = stored
}
})
return utmParams
}
export function getHiddenUtmFields() {
const utms = getUtmParams()
return Object.entries(utms).map(([key, value]) => (
<input key={key} type="hidden" name={key} value={value} />
))
}
Dashboard KPIs
Primary Metrics
| KPI |
Formula |
Target |
| Conversion Rate |
(form_submit / page_view) × 100 |
>3% |
| CTA Click Rate |
(cta_click / cta_view) × 100 |
>5% |
| Form Completion Rate |
(form_submit / form_start) × 100 |
>60% |
| Bounce Rate |
Single page sessions / Total sessions |
<50% |
| Avg. Time on Page |
Total time / Sessions |
>90s |
Engagement Metrics
| KPI |
Formula |
Target |
| Scroll Depth (Avg) |
Avg of final scroll_depth |
>75% |
| Content Section Views |
Sections viewed per session |
>5 |
| FAQ Engagement |
faq_expand / page_view |
>20% |
| Video Play Rate |
video_start / page_view |
>15% |
Funnel Metrics
Traffic → Page View → CTA Click → Form Start → Form Submit → Qualified Lead
Drop-off Analysis:
- Page View → CTA Click: [%] (Message clarity issue)
- CTA Click → Form Start: [%] (CTA mismatch issue)
- Form Start → Form Submit: [%] (Form friction issue)
A/B Testing Events
trackEvent('ab_test_exposure', {
test_id: 'hero-headline-q1',
variant: 'B',
test_name: 'Hero Headline Test',
})
trackEvent('ab_test_conversion', {
test_id: 'hero-headline-q1',
variant: 'B',
conversion_type: 'form_submit',
})
Privacy and Compliance
- Cookie consent required in EU/UK/CA
- No PII in analytics properties
- Data retention settings in GA4
- Use consent mode (wait for consent before tracking)
- IP anonymization enabled
- Only collect what you need
Debugging
Debug Mode
export function enableAnalyticsDebug() {
if (typeof window !== 'undefined') {
(window as any).analyticsDebug = true
}
}
Common Issues
| Issue |
Cause |
Fix |
| Events not firing |
CSR hydration |
Use useEffect |
| Duplicate events |
Re-renders |
Use refs to track |
| Missing params |
Async state |
Ensure data ready |
| Wrong attribution |
UTM lost |
Session storage preservation |
GA4 Debug View
- Install GA Debugger Chrome extension
- Enable debug mode:
gtag('config', 'GA_ID', { debug_mode: true })
- View real-time in GA4 → Configure → DebugView
Tool Integrations
| Tool |
Best For |
Guide |
| GA4 |
Web analytics, Google ecosystem |
MCP available |
| Mixpanel |
Product analytics, event tracking |
— |
| PostHog |
Open-source analytics, session replay |
— |
| Segment |
Customer data platform, routing |
— |
| Amplitude |
Product analytics, cohort analysis |
— |
Checklist
Related Skills
- ab-testing: For experiment tracking
- page-cro: For conversion optimization (uses this data)
- revops: For pipeline metrics, CRM tracking, revenue attribution
- measurement: See analytics-tracking (this skill replaces it)
1---2name: analytics-tracking3description: When the user wants to set up, improve, or audit analytics tracking and measurement. Also use when the user mentions "set up tracking," "GA4," "Google Analytics," "conversion tracking," "event tracking," "UTM parameters," "tag manager," "GTM," "analytics implementation," or "tracking plan." Includes full Next.js TypeScript implementation. For A/B test measurement, see ab-testing.4---56# Analytics Tracking78You are an expert in analytics implementation and measurement. Your goal is to help set up tracking that provides actionable insights for marketing and product decisions.910## Initial Assessment1112**Check for product marketing context first:**13If `.agents/product-marketing-context.md` exists, read it before asking questions.1415Before implementing tracking, understand:16171. **Business Context** — What decisions will this data inform? What are key conversions?182. **Current State** — What tracking exists? What tools are in use?193. **Technical Context** — What's the tech stack? Any privacy/compliance requirements?2021---2223## Core Principles24251. **Track for Decisions, Not Data** — Every event should inform a decision. Avoid vanity metrics.262. **Start with the Questions** — What do you need to know? Work backwards to what you need to track.273. **Name Things Consistently** — Naming conventions matter. Establish patterns before implementing.284. **Maintain Data Quality** — Clean data > more data.2930---3132## Event Naming Conventions3334### Recommended Format: Category_Action_Object3536```37[category]_[action]_[object]38```3940**Categories**: `page`, `form`, `cta`, `content`, `video`, `exit`4142**Examples**:43- `cta_click_hero`44- `form_submit_contact`45- `content_scroll_50`4647### Alternative: Object-Action4849```50signup_completed51button_clicked52form_submitted53article_read54checkout_payment_completed55```5657**Best Practices:**58- Lowercase with underscores59- Be specific: `cta_hero_clicked` vs. `button_clicked`60- Include context in properties, not event name61- Document all decisions6263---6465## Event Taxonomy6667### Standard Events (Landing Page)6869| Event Name | Trigger | Parameters |70|------------|---------|------------|71| `page_view` | Page load | `page_path`, `page_title`, `page_locale`, `traffic_source` |72| `page_scroll` | Scroll milestones | `scroll_depth` (25, 50, 75, 100) |73| `page_time` | Time thresholds | `time_on_page` (30s, 60s, 120s, 300s) |74| `page_exit` | User leaves | `exit_page`, `time_on_page`, `scroll_depth_final` |7576### CTA Events7778| Event Name | Trigger | Parameters |79|------------|---------|------------|80| `cta_view` | CTA enters viewport | `cta_id`, `cta_text`, `cta_location` |81| `cta_click` | CTA clicked | `cta_id`, `cta_text`, `cta_location`, `cta_variant` |82| `cta_hover` | CTA hovered (>500ms) | `cta_id`, `cta_location` |8384### Form Events8586| Event Name | Trigger | Parameters |87|------------|---------|------------|88| `form_view` | Form enters viewport | `form_id`, `form_name` |89| `form_start` | First field focused | `form_id`, `first_field` |90| `form_field_complete` | Field completed | `form_id`, `field_name`, `field_position` |91| `form_field_error` | Validation error | `form_id`, `field_name`, `error_type` |92| `form_abandon` | Left without submit | `form_id`, `last_field`, `fields_completed` |93| `form_submit` | Form submitted | `form_id`, `form_name`, `submission_time` |94| `form_success` | Submission confirmed | `form_id`, `lead_id` |95| `form_error` | Submission failed | `form_id`, `error_type` |9697### Content Engagement9899| Event Name | Trigger | Parameters |100|------------|---------|------------|101| `content_section_view` | Section enters viewport | `section_id`, `section_name` |102| `content_testimonial_view` | Testimonial seen | `testimonial_id`, `testimonial_author` |103| `content_faq_expand` | FAQ item expanded | `faq_id`, `faq_question` |104| `content_pricing_view` | Pricing section seen | `pricing_tier_visible` |105| `content_feature_click` | Feature clicked | `feature_id`, `feature_name` |106107### Video Events108109| Event Name | Trigger | Parameters |110|------------|---------|------------|111| `video_start` | Video starts | `video_id`, `video_title` |112| `video_progress` | Milestones | `video_id`, `progress` (25, 50, 75, 100) |113| `video_complete` | Video ends | `video_id`, `watch_time` |114| `video_pause` | Paused | `video_id`, `pause_time` |115116### Exit Intent117118| Event Name | Trigger | Parameters |119|------------|---------|------------|120| `exit_intent_trigger` | Exit intent detected | `trigger_type` (mouse, scroll, idle) |121| `exit_popup_view` | Exit popup shown | `popup_id`, `popup_variant` |122| `exit_popup_close` | Popup dismissed | `popup_id`, `dismiss_method` |123| `exit_popup_convert` | Popup CTA clicked | `popup_id`, `offer_type` |124125### Essential Events (Product/App)126127| Event | Properties |128|-------|------------|129| `onboarding_step_completed` | `step_number`, `step_name` |130| `feature_used` | `feature_name` |131| `purchase_completed` | `plan`, `value` |132| `subscription_cancelled` | `reason` |133134---135136## Implementation (Next.js + GA4)137138### Event Utility139140```typescript141// lib/analytics.ts142type EventParams = Record<string, string | number | boolean | undefined>143144declare global {145 interface Window {146 gtag: (command: 'event', eventName: string, params?: EventParams) => void147 dataLayer: any[]148 }149}150151export function trackEvent(eventName: string, params?: EventParams) {152 if (typeof window !== 'undefined' && window.gtag) {153 window.gtag('event', eventName, {154 ...params,155 timestamp: new Date().toISOString(),156 })157 }158159 if (process.env.NODE_ENV === 'development') {160 console.log('[Analytics]', eventName, params)161 }162}163164// Typed event helpers165export const analytics = {166 pageView: (path: string, title: string, locale: string) =>167 trackEvent('page_view', { page_path: path, page_title: title, page_locale: locale }),168169 pageScroll: (depth: 25 | 50 | 75 | 100) =>170 trackEvent('page_scroll', { scroll_depth: depth }),171172 ctaClick: (id: string, text: string, location: string, variant?: string) =>173 trackEvent('cta_click', { cta_id: id, cta_text: text, cta_location: location, cta_variant: variant }),174175 ctaView: (id: string, text: string, location: string) =>176 trackEvent('cta_view', { cta_id: id, cta_text: text, cta_location: location }),177178 formStart: (formId: string, firstField: string) =>179 trackEvent('form_start', { form_id: formId, first_field: firstField }),180181 formSubmit: (formId: string, formName: string) =>182 trackEvent('form_submit', { form_id: formId, form_name: formName }),183184 formError: (formId: string, fieldName: string, errorType: string) =>185 trackEvent('form_field_error', { form_id: formId, field_name: fieldName, error_type: errorType }),186187 formAbandon: (formId: string, lastField: string, fieldsCompleted: number) =>188 trackEvent('form_abandon', { form_id: formId, last_field: lastField, fields_completed: fieldsCompleted }),189190 sectionView: (sectionId: string, sectionName: string) =>191 trackEvent('content_section_view', { section_id: sectionId, section_name: sectionName }),192193 faqExpand: (faqId: string, question: string) =>194 trackEvent('content_faq_expand', { faq_id: faqId, faq_question: question }),195}196```197198### Scroll Tracking Hook199200```typescript201// hooks/use-scroll-tracking.ts202'use client'203204import { useEffect, useRef } from 'react'205import { analytics } from '@/lib/analytics'206207export function useScrollTracking() {208 const tracked = useRef<Set<number>>(new Set())209210 useEffect(() => {211 const thresholds = [25, 50, 75, 100] as const212213 const handleScroll = () => {214 const scrollHeight = document.documentElement.scrollHeight - window.innerHeight215 const scrollPercent = Math.round((window.scrollY / scrollHeight) * 100)216217 thresholds.forEach((threshold) => {218 if (scrollPercent >= threshold && !tracked.current.has(threshold)) {219 tracked.current.add(threshold)220 analytics.pageScroll(threshold)221 }222 })223 }224225 window.addEventListener('scroll', handleScroll, { passive: true })226 return () => window.removeEventListener('scroll', handleScroll)227 }, [])228}229```230231### Section Visibility Hook232233```typescript234// hooks/use-section-tracking.ts235'use client'236237import { useEffect, useRef } from 'react'238import { analytics } from '@/lib/analytics'239240export function useSectionTracking(sectionId: string, sectionName: string) {241 const ref = useRef<HTMLElement>(null)242 const hasTracked = useRef(false)243244 useEffect(() => {245 const element = ref.current246 if (!element) return247248 const observer = new IntersectionObserver(249 ([entry]) => {250 if (entry.isIntersecting && !hasTracked.current) {251 hasTracked.current = true252 analytics.sectionView(sectionId, sectionName)253 }254 },255 { threshold: 0.5 }256 )257258 observer.observe(element)259 return () => observer.disconnect()260 }, [sectionId, sectionName])261262 return ref263}264265// Usage266function HeroSection() {267 const sectionRef = useSectionTracking('hero', 'Hero Section')268 return (269 <section ref={sectionRef} id="hero">270 {/* content */}271 </section>272 )273}274```275276### Tracked CTA Component277278```typescript279// components/tracked-cta.tsx280'use client'281282import { useEffect, useRef } from 'react'283import { Button } from '@/components/ui/button'284import { analytics } from '@/lib/analytics'285286interface TrackedCTAProps {287 id: string288 location: 'hero' | 'pricing' | 'footer' | 'sticky'289 variant?: string290 children: React.ReactNode291 onClick?: () => void292}293294export function TrackedCTA({ id, location, variant, children, onClick }: TrackedCTAProps) {295 const ref = useRef<HTMLButtonElement>(null)296 const hasTrackedView = useRef(false)297298 useEffect(() => {299 const element = ref.current300 if (!element) return301302 const observer = new IntersectionObserver(303 ([entry]) => {304 if (entry.isIntersecting && !hasTrackedView.current) {305 hasTrackedView.current = true306 analytics.ctaView(id, element.textContent || '', location)307 }308 },309 { threshold: 0.5 }310 )311312 observer.observe(element)313 return () => observer.disconnect()314 }, [id, location])315316 const handleClick = () => {317 analytics.ctaClick(id, ref.current?.textContent || '', location, variant)318 onClick?.()319 }320321 return (322 <Button ref={ref} onClick={handleClick}>323 {children}324 </Button>325 )326}327```328329### Form Tracking Hook330331```typescript332// hooks/use-form-tracking.ts333'use client'334335import { useRef, useCallback, useEffect } from 'react'336import { analytics } from '@/lib/analytics'337338export function useFormTracking(formId: string, formName: string) {339 const startTime = useRef<number | null>(null)340 const fieldsCompleted = useRef<string[]>([])341 const hasStarted = useRef(false)342343 const trackStart = useCallback((fieldName: string) => {344 if (!hasStarted.current) {345 hasStarted.current = true346 startTime.current = Date.now()347 analytics.formStart(formId, fieldName)348 }349 }, [formId])350351 const trackFieldComplete = useCallback((fieldName: string) => {352 if (!fieldsCompleted.current.includes(fieldName)) {353 fieldsCompleted.current.push(fieldName)354 }355 }, [])356357 const trackError = useCallback((fieldName: string, errorType: string) => {358 analytics.formError(formId, fieldName, errorType)359 }, [formId])360361 const trackSubmit = useCallback(() => {362 analytics.formSubmit(formId, formName)363 }, [formId, formName])364365 const trackAbandon = useCallback(() => {366 if (hasStarted.current && fieldsCompleted.current.length > 0) {367 const lastField = fieldsCompleted.current[fieldsCompleted.current.length - 1]368 analytics.formAbandon(formId, lastField, fieldsCompleted.current.length)369 }370 }, [formId])371372 useEffect(() => {373 const handleBeforeUnload = () => trackAbandon()374 window.addEventListener('beforeunload', handleBeforeUnload)375 return () => {376 window.removeEventListener('beforeunload', handleBeforeUnload)377 trackAbandon()378 }379 }, [trackAbandon])380381 return { trackStart, trackFieldComplete, trackError, trackSubmit }382}383```384385---386387## GA4 Configuration388389### Custom Dimensions390391| Dimension | Scope | Description |392|-----------|-------|-------------|393| `page_locale` | Event | Language (it, en, cs) |394| `traffic_source` | Session | UTM source |395| `cta_location` | Event | Where CTA appears |396| `form_id` | Event | Form identifier |397| `scroll_depth` | Event | Max scroll reached |398| `ab_variant` | Session | A/B test variant |399400### Custom Metrics401402| Metric | Scope | Description |403|--------|-------|-------------|404| `time_to_cta_click` | Event | Seconds from page load to CTA click |405| `form_completion_time` | Event | Seconds to complete form |406| `fields_completed` | Event | Number of form fields filled |407408### Conversions (Goals)409410| Conversion | Event | Value |411|------------|-------|-------|412| Lead Generated | `form_submit` | $50 |413| Demo Requested | `form_submit` (demo form) | $100 |414| Pricing Viewed | `content_section_view` (pricing) | $5 |415| High Engagement | `page_scroll` (100%) + time >120s | $10 |416417### Quick Setup4184191. Create GA4 property and data stream4202. Install gtag.js or GTM4213. Enable enhanced measurement4224. Configure custom events4235. Mark conversions in Admin424425---426427## UTM Strategy428429### Standard Parameters430431```432utm_source = [platform] # google, linkedin, newsletter433utm_medium = [channel type] # cpc, social, email434utm_campaign = [campaign name] # q1-launch, product-feature435utm_content = [ad/link variant] # hero-cta, sidebar-banner436utm_term = [keyword] # frontend-development437```438439### UTM Preservation440441```typescript442// lib/utm.ts443export function getUtmParams(): Record<string, string> {444 if (typeof window === 'undefined') return {}445446 const params = new URLSearchParams(window.location.search)447 const utmParams: Record<string, string> = {}448 const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']449450 utmKeys.forEach((key) => {451 const value = params.get(key)452 if (value) {453 utmParams[key] = value454 sessionStorage.setItem(key, value)455 } else {456 const stored = sessionStorage.getItem(key)457 if (stored) utmParams[key] = stored458 }459 })460461 return utmParams462}463464export function getHiddenUtmFields() {465 const utms = getUtmParams()466 return Object.entries(utms).map(([key, value]) => (467 <input key={key} type="hidden" name={key} value={value} />468 ))469}470```471472---473474## Dashboard KPIs475476### Primary Metrics477478| KPI | Formula | Target |479|-----|---------|--------|480| **Conversion Rate** | (form_submit / page_view) × 100 | >3% |481| **CTA Click Rate** | (cta_click / cta_view) × 100 | >5% |482| **Form Completion Rate** | (form_submit / form_start) × 100 | >60% |483| **Bounce Rate** | Single page sessions / Total sessions | <50% |484| **Avg. Time on Page** | Total time / Sessions | >90s |485486### Engagement Metrics487488| KPI | Formula | Target |489|-----|---------|--------|490| **Scroll Depth (Avg)** | Avg of final scroll_depth | >75% |491| **Content Section Views** | Sections viewed per session | >5 |492| **FAQ Engagement** | faq_expand / page_view | >20% |493| **Video Play Rate** | video_start / page_view | >15% |494495### Funnel Metrics496497```498Traffic → Page View → CTA Click → Form Start → Form Submit → Qualified Lead499500Drop-off Analysis:501- Page View → CTA Click: [%] (Message clarity issue)502- CTA Click → Form Start: [%] (CTA mismatch issue)503- Form Start → Form Submit: [%] (Form friction issue)504```505506---507508## A/B Testing Events509510```typescript511trackEvent('ab_test_exposure', {512 test_id: 'hero-headline-q1',513 variant: 'B',514 test_name: 'Hero Headline Test',515})516517trackEvent('ab_test_conversion', {518 test_id: 'hero-headline-q1',519 variant: 'B',520 conversion_type: 'form_submit',521})522```523524---525526## Privacy and Compliance527528- Cookie consent required in EU/UK/CA529- No PII in analytics properties530- Data retention settings in GA4531- Use consent mode (wait for consent before tracking)532- IP anonymization enabled533- Only collect what you need534535---536537## Debugging538539### Debug Mode540541```typescript542export function enableAnalyticsDebug() {543 if (typeof window !== 'undefined') {544 (window as any).analyticsDebug = true545 }546}547```548549### Common Issues550551| Issue | Cause | Fix |552|-------|-------|-----|553| Events not firing | CSR hydration | Use useEffect |554| Duplicate events | Re-renders | Use refs to track |555| Missing params | Async state | Ensure data ready |556| Wrong attribution | UTM lost | Session storage preservation |557558### GA4 Debug View5591. Install GA Debugger Chrome extension5602. Enable debug mode: `gtag('config', 'GA_ID', { debug_mode: true })`5613. View real-time in GA4 → Configure → DebugView562563---564565## Tool Integrations566567| Tool | Best For | Guide |568|------|----------|-------|569| **GA4** | Web analytics, Google ecosystem | MCP available |570| **Mixpanel** | Product analytics, event tracking | — |571| **PostHog** | Open-source analytics, session replay | — |572| **Segment** | Customer data platform, routing | — |573| **Amplitude** | Product analytics, cohort analysis | — |574575---576577## Checklist578579- [ ] Event taxonomy defined580- [ ] Analytics utility created581- [ ] Scroll tracking implemented582- [ ] CTA tracking on all buttons583- [ ] Form tracking complete584- [ ] Section visibility tracking585- [ ] UTM preservation working586- [ ] GA4 custom dimensions configured587- [ ] Conversions defined in GA4588- [ ] Debug mode available589- [ ] Dashboard KPIs documented590- [ ] A/B test events ready591- [ ] Privacy/consent compliance verified592593---594595## Related Skills596597- **ab-testing**: For experiment tracking598- **page-cro**: For conversion optimization (uses this data)599- **revops**: For pipeline metrics, CRM tracking, revenue attribution600- **measurement**: See analytics-tracking (this skill replaces it)