This plan provides step-by-step instructions for implementing Google Analytics 4 (GA4) tracking on the California DOGE website. Focus on tracking user interactions with government data: search terms, page navigation, filter usage, and data exploration patterns.
✅ UPDATE: Now includes three-layer analytics strategy with Web Vitals integration for comprehensive performance monitoring.
- Search queries: What users search for in government data
- Page views: Which department pages and data sections get attention
- Filter selections: How users filter spending, workforce, and budget data
- Text input: Search terms, form inputs, data queries
- Workforce card clicks: Which employee/position cards users explore
- External links: Clicks to official government sources (PropuBLICA, fiscal.ca.gov, etc.)
- Navigation patterns: How users move through department data
- Page load performance on data-heavy pages
- Search result relevance and success rates
- User flow through complex government data structures
- Mobile vs desktop usage patterns for government transparency tools
- ✅ NEW: Core Web Vitals monitoring (LCP, INP, CLS, FCP, TTFB)
# Add to .env.local
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXX
NEXT_PUBLIC_GA_ENABLE_DEVELOPMENT=true # For testing✅ Status: Environment confirmed, Vercel Pro plan active, dependencies installed.
✅ Updated with marketing-focused configuration:
'use client';
import Script from 'next/script';
import { useEffect } from 'react';
declare global {
interface Window {
gtag: (command: string, targetId: string | Date, config?: any) => void;
dataLayer: any[];
}
}
interface GoogleAnalyticsProps {
measurementId: string;
enableDevelopment?: boolean;
}
export default function GoogleAnalytics({
measurementId,
enableDevelopment = false
}: GoogleAnalyticsProps) {
const isProduction = process.env.NODE_ENV === 'production';
const shouldLoadGA = isProduction || enableDevelopment;
useEffect(() => {
if (!shouldLoadGA) return;
window.dataLayer = window.dataLayer || [];
function gtag(...args: any[]) {
window.dataLayer.push(args);
}
window.gtag = gtag;
gtag('js', new Date());
gtag('config', measurementId, {
// ✅ UPDATED: Marketing-focused settings for social media and newsletter campaigns
anonymize_ip: false, // Allow IP tracking for better demographics
allow_google_signals: true, // Enable for audience insights and remarketing
allow_ad_personalization_signals: true, // Enable for marketing insights
restricted_data_processing: false, // Allow full data processing for marketing
// Enhanced marketing tracking
enhanced_conversions: true, // Better conversion tracking
automatic_screen_view: true, // Track single-page app navigation
send_page_view: true,
debug_mode: !isProduction,
// Custom dimensions for government transparency data
custom_map: {
'custom_parameter_1': 'department',
'custom_parameter_2': 'data_type',
'custom_parameter_3': 'search_query',
'custom_parameter_4': 'fiscal_year',
'custom_parameter_5': 'user_engagement_level'
}
});
}, [measurementId, shouldLoadGA, isProduction]);
if (!shouldLoadGA) return null;
return (
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${measurementId}`}
strategy="afterInteractive"
/>
);
}✅ Updated with three-layer analytics strategy:
import { track as vercelTrack } from '@vercel/analytics';
import {
trackEvent as gaTrackEvent,
trackDepartmentView as gaTrackDepartmentView,
trackSearch as gaTrackSearch,
trackFilterApplication as gaTrackFilterApplication,
trackWorkforceCardClick as gaTrackWorkforceCardClick,
trackExternalLinkClick as gaTrackExternalLinkClick,
trackNewsletterSignup as gaTrackNewsletterSignup,
trackSocialShare as gaTrackSocialShare,
trackPageView as gaTrackPageView
} from '@/components/GoogleAnalytics';
/**
* Three-Layer Analytics Interface for California DOGE
*
* Layer 1: Google Analytics (GA4) - Marketing, user behavior, demographics
* Layer 2: Vercel Analytics - Technical performance, simple event tracking
* Layer 3: Web Vitals - Core Web Vitals monitoring and optimization
*/
export const analytics = {
// Track search queries
search: (query: string, resultCount: number, searchType?: string) => {
// GA4: Comprehensive search analytics with engagement metrics
gaTrackSearch(query, resultCount, searchType);
// Vercel: Simple search performance tracking
const vercelProps: Record<string, string | number> = {
query: query.substring(0, 50), // Truncate for Vercel limits
results: resultCount
};
if (searchType) vercelProps.type = searchType;
vercelTrack('Search', vercelProps);
},
// Track page views with context
pageView: (pagePath: string, pageTitle?: string, department?: string) => {
// GA4: Detailed page tracking with custom dimensions
gaTrackPageView(pagePath, pageTitle);
// Vercel: Automatic page view tracking (no manual call needed)
},
// Track filter usage
filterUsed: (filterType: string, filterValue: string, pageContext?: string) => {
// GA4: Detailed filter analytics with department context
gaTrackFilterApplication(filterType, filterValue, pageContext);
// Vercel: Simple filter tracking for UX optimization
vercelTrack('Filter Applied', {
filter: filterType,
value: filterValue.substring(0, 50) // Truncate for Vercel limits
});
},
// Track workforce card clicks
workforceCardClick: (employeeTitle: string, department: string, salaryRange?: string) => {
// GA4: Detailed workforce engagement tracking
gaTrackWorkforceCardClick(department, parseInt(salaryRange?.replace(/\D/g, '') || '0'));
// Vercel: Simple workforce interaction tracking
vercelTrack('Workforce Card Click', { title: employeeTitle, dept: department, salary: salaryRange });
},
// Track external link clicks
externalLink: (url: string, linkText?: string) => {
// GA4: Comprehensive outbound link tracking
gaTrackExternalLinkClick(url, linkText || 'unknown');
// Vercel: Simple external link tracking
vercelTrack('External Link', { url, text: linkText });
},
// Track department navigation
departmentView: (departmentName: string, departmentSlug?: string, orgCode?: string) => {
// GA4: Detailed department analytics with custom parameters
gaTrackDepartmentView(departmentName, departmentSlug);
// Vercel: Simple department tracking for performance correlation
const vercelProps: Record<string, string> = { department: departmentName };
if (departmentSlug) vercelProps.slug = departmentSlug;
if (orgCode) vercelProps.code = orgCode;
vercelTrack('Department View', vercelProps);
},
// ✅ NEW: Newsletter signup tracking
newsletterSignup: (source: string) => {
gaTrackNewsletterSignup(source);
vercelTrack('Newsletter Signup', { source });
},
// ✅ NEW: Social media sharing tracking
socialShare: (platform: string, contentType: string, contentId?: string) => {
gaTrackSocialShare(platform, contentType, contentId);
const vercelProps: Record<string, string> = { platform, content: contentType };
if (contentId) vercelProps.id = contentId;
vercelTrack('Social Share', vercelProps);
},
// Generic event tracking
event: (eventName: string, parameters?: Record<string, any>) => {
gaTrackEvent(eventName, parameters);
vercelTrack(eventName, parameters);
},
// ✅ NEW: Performance tracking (Web Vitals integration)
performance: (metric: string, value: number, pagePath?: string) => {
// Vercel: Performance-focused tracking
vercelTrack('Performance', {
metric,
value: Math.round(value),
page: pagePath?.substring(0, 50) || window.location.pathname.substring(0, 50)
});
// GA4: Core Web Vitals tracking with government data context
if (['LCP', 'INP', 'CLS', 'FCP', 'TTFB'].includes(metric)) {
gaTrackEvent('web_vitals', {
metric_name: metric,
metric_value: value,
page_path: pagePath || window.location.pathname
});
}
}
};
// ✅ NEW: Utility for tracking Core Web Vitals (called by WebVitalsTracker)
export const trackWebVitals = (metric: any) => {
analytics.performance(metric.name, metric.value, window.location.pathname);
};✅ Updated with three-layer analytics setup:
import GoogleAnalytics from '@/components/GoogleAnalytics';
import { Analytics } from '@vercel/analytics/react';
import WebVitalsTracker from '@/components/WebVitalsTracker';
// Add this inside the <body> tag, before closing </body>
{/* Three-Layer Analytics Setup */}
{/* Layer 1: Vercel Analytics - Performance and Core Web Vitals */}
<Analytics />
{/* Layer 2: Google Analytics - Marketing and User Behavior */}
<GoogleAnalytics
measurementId={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || 'G-XXXXXXX'}
enableDevelopment={process.env.NEXT_PUBLIC_GA_ENABLE_DEVELOPMENT === 'true'}
/>
{/* Layer 3: Web Vitals - Performance Monitoring and Optimization */}
<WebVitalsTracker />✅ NEW: Core Web Vitals monitoring for government data optimization:
'use client';
import { useEffect } from 'react';
import { trackWebVitals } from '@/lib/analytics';
/**
* Web Vitals Tracker for California DOGE
*
* Monitors Core Web Vitals to optimize government transparency site performance:
* - LCP: How fast department/budget pages load
* - INP: How responsive search and filters feel (replaces FID)
* - CLS: How stable data visualizations are
* - FCP: How quickly users see initial content
* - TTFB: Server response time for large datasets
*/
export default function WebVitalsTracker() {
useEffect(() => {
// Only run in browser environment
if (typeof window === 'undefined') return;
// Dynamically import web-vitals to avoid SSR issues
import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
// Track Core Web Vitals for government data optimization
onLCP(trackWebVitals);
onINP(trackWebVitals);
onCLS(trackWebVitals);
onFCP(trackWebVitals);
onTTFB(trackWebVitals);
}).catch((error) => {
console.warn('Failed to load web-vitals library:', error);
});
}, []);
return null;
}✅ Status: Analytics tracking implemented across key components with real user interactions.
Files Updated: src/components/DepartmentSearch.tsx, src/components/EnhancedSearch.tsx, src/app/search/page.tsx
// DepartmentSearch.tsx - Department search tracking
const handleDepartmentClick = (department: DepartmentSearchResult) => {
analytics.departmentView(department.name, department.slug);
onClose();
};
// Search query tracking with results count
useEffect(() => {
if (searchTerm.trim()) {
analytics.search(searchTerm, filtered.length, 'department_search');
}
}, [searchTerm, departments]);
// EnhancedSearch.tsx - Enhanced search with multiple types
const performSearch = async (query: string, types: string[]) => {
// ... search logic ...
if (data) {
const totalResults = (data.departments?.length || 0) +
(data.vendors?.length || 0) +
(data.programs?.length || 0) +
(data.funds?.length || 0) +
(data.keywords?.length || 0);
analytics.search(query.trim(), totalResults, 'enhanced_search');
}
};
// Search page form submissions
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (query.trim()) {
analytics.search(query.trim(), 0, 'search_page_form');
// ... form submission logic ...
}
};Files Updated: src/app/workforce/page.tsx, src/app/search/page.tsx, src/components/EnhancedSearch.tsx
// Workforce page filters
onClick={() => {
analytics.filterApplied('view_mode', 'parent-only', 'workforce_page');
setViewMode('parent-only');
}}
onChange={(e) => {
const year = e.target.value;
if (year.match(/^\d{4}$/)) {
analytics.filterApplied('fiscal_year', year, 'workforce_page');
setSelectedFiscalYear(year as AnnualYear);
}
}}
// Search page filters
const toggleType = (type: string) => {
analytics.filterApplied('search_type', type, 'search_page');
// ... filter logic ...
};
const handleExcludeCommonChange = (value: string) => {
analytics.filterApplied('exclude_common', value, 'search_page');
// ... filter logic ...
};
// Enhanced search type filters
const toggleType = (type: string) => {
analytics.filterApplied('search_type', type, 'enhanced_search');
// ... filter logic ...
};Files Updated: src/app/workforce/page.tsx
// Department card click tracking
const handleCardClick = () => {
// Track workforce card click with employee count
analytics.workforceCardClick(department.name, _workforceData);
onClick();
};
// Department selection tracking
const handleSelectDepartment = (department: DepartmentData) => {
// Track department view
analytics.departmentView(department.name, department._slug);
setSelectedDepartmentName(department.name);
};Files Updated: src/app/workforce/page.tsx, src/app/search/page.tsx, src/app/spend/page.tsx
// Government sources tracking
<a
href="https://publicpay.ca.gov/Reports/State/State.aspx"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
onClick={() => analytics.externalLinkClick('https://publicpay.ca.gov/Reports/State/State.aspx', 'workforce_sources')}
>
California State Controller's Office - Government Compensation
</a>
// Vendor lookup tracking (spend page)
<a
href={`https://projects.propublica.org/nonprofits/search?q=${encodeURIComponent(record.vendor)}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-300 hover:underline mr-2"
onClick={() => analytics.externalLinkClick(`https://projects.propublica.org/nonprofits/search?q=${encodeURIComponent(record.vendor)}`, 'vendor_lookup_propublica')}
>
ProPublica
</a>Files Updated: src/app/search/page.tsx, src/app/workforce/page.tsx
// Search page tracking
useEffect(() => {
analytics.pageView('/search', 'Search California Government Data');
}, []);
// Workforce page tracking
useEffect(() => {
analytics.pageView('/workforce', 'California State Government Workforce');
}, [transactionId, searchParams]);Files Updated: src/components/EnhancedSearch.tsx
// Search error tracking
} catch (error) {
console.error('Error performing search:', error);
setIsLoading(false);
setSearchResults(null);
// Track search errors
analytics.error('search_error', error instanceof Error ? error.message : 'Unknown search error');
}Files Updated: src/components/EnhancedSearch.tsx
// Enhanced search result clicks
const handleResultClick = (item: SearchItem | KeywordItem) => {
if (item.type === 'keyword') {
gaTrackEvent('keyword_click', { keyword: item.term, sources_count: item.sources.length });
} else {
switch (item.type) {
case 'department':
analytics.departmentView(item.term, item.id);
break;
case 'vendor':
analytics.vendorView(item.term);
break;
case 'program':
analytics.programView(item.id, item.term);
break;
case 'fund':
gaTrackEvent('fund_view', { fund_name: item.term, fund_id: item.id });
break;
}
}
// ... navigation logic ...
};Total Components Updated: 6 files
src/components/DepartmentSearch.tsx- Department search trackingsrc/components/EnhancedSearch.tsx- Enhanced search with error handlingsrc/app/search/page.tsx- Search page with filters and external linkssrc/app/workforce/page.tsx- Workforce cards, filters, and external linkssrc/app/spend/page.tsx- Vendor lookup and external links
Analytics Events Implemented:
- ✅ Search queries with result counts and search types
- ✅ Filter applications (view mode, fiscal year, search types, limits)
- ✅ Workforce card clicks with employee counts
- ✅ Department navigation and views
- ✅ External link clicks with context tracking
- ✅ Page view tracking with titles
- ✅ Error tracking for search failures
- ✅ Result click tracking by type (department, vendor, program, fund, keyword)
Ready for Testing: All tracking calls are implemented and ready for GA4 configuration and validation.
Custom events are NOT created in the GA4 interface. Your custom events are already implemented in your code (Steps 1-6). GA4 will automatically start collecting them once deployed. The GA4 interface is used for:
- Creating custom dimensions/metrics to access event parameters in reports
- Modifying existing events that are already being collected
- Creating new events based on existing collected events
- Marking events as key events (conversions)
Your custom events are already coded and ready to go. First, verify they're working:
- Deploy your implementation with analytics enabled
- Set
NEXT_PUBLIC_GA_ENABLE_DEVELOPMENT=truefor testing
Navigate to: GA4 Property → Configure → DebugView
- Perform actions on your site (search, filter, click workforce cards)
- Verify these events appear in DebugView:
searchwith parameters:search_term,result_count,search_typefilter_appliedwith parameters:filter_type,filter_value,page_contextworkforce_card_clickwith parameters:department_name,employee_countdepartment_viewwith parameters:department_name,department_slugexternal_link_clickwith parameters:link_url,link_contextweb_vitalswith parameters:metric_name,metric_value,page_path
- Events will start appearing in standard reports after processing
- DebugView shows real-time data immediately
Navigate to: GA4 Property → Configure → Custom definitions → Custom dimensions → Create custom dimension
To access event parameters in your reports, create these custom dimensions:
-
Department Name
- Dimension name:
Department - Scope: Event
- Event parameter:
department_name - Description: "California government department"
- Dimension name:
-
Search Type
- Dimension name:
Search Type - Scope: Event
- Event parameter:
search_type - Description: "Type of search performed"
- Dimension name:
-
Filter Type
- Dimension name:
Filter Type - Scope: Event
- Event parameter:
filter_type - Description: "Type of filter applied"
- Dimension name:
-
Page Context
- Dimension name:
Page Context - Scope: Event
- Event parameter:
page_context - Description: "Page where interaction occurred"
- Dimension name:
-
Link Context
- Dimension name:
Link Context - Scope: Event
- Event parameter:
link_context - Description: "Context of external link clicks"
- Dimension name:
-
Performance Metric
- Dimension name:
Performance Metric - Scope: Event
- Event parameter:
metric_name - Description: "Core Web Vitals metric name"
- Dimension name:
-
Error Type
- Dimension name:
Error Type - Scope: Event
- Event parameter:
error_type - Description: "Type of error encountered"
- Dimension name:
Navigate to: GA4 Property → Configure → Custom definitions → Custom metrics → Create custom metric
For numerical event parameters:
-
Result Count
- Metric name:
Search Result Count - Scope: Event
- Event parameter:
result_count - Unit of measurement: Standard
- Metric name:
-
Employee Count
- Metric name:
Employee Count - Scope: Event
- Event parameter:
employee_count - Unit of measurement: Standard
- Metric name:
-
Performance Value
- Metric name:
Performance Value - Scope: Event
- Event parameter:
metric_value - Unit of measurement: Milliseconds
- Metric name:
Navigate to: GA4 Property → Configure → Events
Wait for your events to appear in the Events list (24-48 hours), then:
- Find
searchevent → Toggle "Mark as key event" ✅ - Find
department_viewevent → Toggle "Mark as key event" ✅ - Find
workforce_card_clickevent → Toggle "Mark as key event" ✅ - Find
vendor_viewevent → Toggle "Mark as key event" ✅ - Find
program_viewevent → Toggle "Mark as key event" ✅
These will now count as conversions in your reports.
Navigate to: GA4 Property → Data streams → Web → website → Enhanced measurement
Enable these automatic measurements that complement your custom tracking:
- ✅ Page views - Automatic page tracking
- ✅ Scrolls - 90% page scroll depth
- ✅ Outbound clicks - Clicks to external domains
- ✅ Site search - URL-based search tracking
- ✅ File downloads - PDF, doc, zip downloads
- ✅ Form interactions - Form starts and submissions
Navigate to: GA4 Property → Data streams → Web → website → Modify events
Only if needed, you can modify existing events. For example:
- Modify existing events: Create new event from
page_view - Matching conditions:
page_locationcontainsthank-youorconfirmation - New event name:
contact_form_submission
- Modify existing events: Create new event from
search - Matching conditions:
result_countgreater than10 - New event name:
successful_search
Navigate to: GA4 Property → Configure → Data settings → Data retention
- Event data retention: 14 months (maximum free tier)
- Reset user data on new activity: ON (recommended)
- Custom events implemented in components
- Analytics interface created
- Web Vitals tracking added
- Three-layer analytics setup complete
- Deploy code and verify events in DebugView
- Create 7 custom dimensions for event parameters
- Create 3 custom metrics for numerical parameters
- Mark 5 events as key events (conversions)
- Enable Enhanced Measurement features
- Set data retention to 14 months
- Verify events appear in standard reports
- Check custom dimensions populate with data
- Confirm key events show in conversions report
- Create custom explorations using new dimensions
- Your events are already implemented - GA4 will automatically collect them once deployed
- Custom dimensions are essential - Without them, you can't access event parameters in reports
- Key events = conversions - Mark important events to measure success
- Enhanced Measurement is free - Provides additional automatic tracking
- Data takes 24-48 hours - Be patient for events to appear in standard reports
This corrected approach aligns with how GA4 actually works: events are coded (✅ done), then configured in the interface for reporting and analysis.
- Set
NEXT_PUBLIC_GA_ENABLE_DEVELOPMENT=true - Open browser dev tools → Network tab
- Perform actions (search, filter, click cards)
- Verify GA events are being sent
- Check GA4 DebugView for real-time events
- ✅ NEW: Monitor Web Vitals in browser console
- Deploy with analytics enabled
- Test key user flows
- Verify events appear in GA4 within 24-48 hours
- Check custom dimensions are populated
- ✅ NEW: Monitor Core Web Vitals in Vercel Analytics dashboard
- Basic page view tracking
- Search query tracking
- Filter usage tracking
- ✅ NEW: Web Vitals monitoring
- Workforce card click tracking
- Department navigation tracking
- External link tracking
- ✅ NEW: Newsletter signup tracking
- ✅ NEW: Social media sharing tracking
- Time on page tracking
- Scroll depth tracking
- Form abandonment tracking
- ✅ NEW: Performance-behavior correlation analysis
- User behavior, demographics, marketing attribution
- Newsletter signups, social shares, conversion tracking
- Custom dimensions for government data analysis
- Page performance, Core Web Vitals, technical metrics
- Simple event tracking, UX optimization data
- LCP, INP, CLS, FCP, TTFB monitoring
- Performance correlation with user behavior
- Government data optimization insights
- Vercel Pro: $20/month (already using)
- Google Analytics: Free
- Web Vitals: Free (open-source)
- Total Additional Cost: $0
This plan provides concrete steps for implementing comprehensive analytics tracking focused on actual user interactions with California government data that exist in your codebase, now enhanced with performance monitoring and marketing optimization.