Google Analytics Integration
[!NOTE] This documentation was created with assistance from Claude Sonnet 4.5
This document explains how Google Analytics 4 (GA4) is implemented in our React Router application, covering technical details, execution flow, and architectural decisions.
What is Google Analytics?
Google Analytics 4 (GA4) is a web analytics service that tracks and reports website traffic, user behavior, and marketing campaign performance. It allows us to:
- Track page views and user navigation
- Monitor UTM campaign parameters (Meta, Google, TikTok ads)
- Analyze user engagement and conversion rates
- Create custom reports and dashboards
Implementation Architecture
We use a component-based approach rather than a Context Provider pattern:
// app/root.jsx
<body>
{children}
<GoogleAnalytics />
<ScrollRestoration />
<Scripts />
</body>
Why component-based?
- Analytics is only used in one place (root layout)
- No need to share analytics state across components
- Simpler to understand and maintain
How It Works
Initialization Flow
1. First Page Load (SSR + Hydration)
Key Points:
- GA script is NOT executed during SSR (server-side rendering)
- GA only runs client-side after React hydration
windowobject checks prevent SSR errors- Script injection happens in
useEffect(client-only)
2. Client-Side Navigation (SPA)
Key Points:
- No page reload occurs (SPA behavior)
useLocation()hook detects route changes- Only page view event is sent (config already done)
- UTM parameters are automatically included
Code Execution Details
useEffect #1: Initialization (Runs Once)
useEffect(() => {
// Skip if no measurement ID is configured
if (!measurementId) return;
// Skip if gtag is already loaded
if (window.gtag) return;
// Create and inject the gtag.js script
const script = document.createElement("script");
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`;
document.head.appendChild(script);
// Initialize gtag function
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag() {
window.dataLayer.push(arguments);
};
window.gtag("js", new Date());
// Configure GA4 with measurement ID
window.gtag("config", measurementId, {
send_page_view: false, // We'll manually send page views
});
}, [measurementId]);
When does this run?
- ✅ Runs once on initial client-side mount
- ✅ Runs only in the browser (not during SSR)
- ❌ Does not run on subsequent route changes
- ❌ Does not run on the server
Why send_page_view: false?
- We manually send page view events to include UTM parameters
- Gives us full control over what data is sent
- Follows GA4 best practices for SPAs
useEffect #2: Page View Tracking (Runs on Every Route Change)
useEffect(() => {
// Skip if gtag is not loaded or no measurement ID
if (!window.gtag || !measurementId) return;
// Send page view with UTM parameters
window.gtag("event", "page_view", {
page_path: location.pathname + location.search,
page_location: window.location.href,
page_title: document.title,
});
}, [location, measurementId]);
When does this run?
- ✅ Runs on initial page load (after hydration)
- ✅ Runs on every route change (client-side navigation)
- ✅ Includes UTM parameters from URL query string
- ❌ Does not run during SSR
Dependencies:
location- Changes on every route navigationmeasurementId- Static, but included for completeness
Why GA Only Runs Client-Side
// This check prevents SSR errors
if (!window.gtag || !measurementId) return;
Reasons:
- No
windowobject on server - Node.js doesn't have browser APIs - GA script requires DOM - Can't inject scripts during SSR
- User tracking requires browser - Cookies, IP, user agent are client-side
- Performance - Avoid unnecessary server-side overhead
React Router SSR Flow
Server Request → SSR (no GA) → Send HTML → Browser Hydration → GA Initializes
Shared Measurement ID
We use the same GA4 Measurement ID for:
careers.jodapp.com(this project)gig-partners.jodapp.com(other project)
Why Same G-ID for Multiple Subdomains?
Benefits:
-
Unified Analytics Dashboard
- View all traffic in one GA4 property
- Compare performance across subdomains
- Consolidated reporting
-
Cross-Domain Tracking
- Track user journeys across subdomains
- Understand how users navigate between products
- Better attribution modeling
-
Simplified Management
- Single GA4 property to configure
- One set of conversion goals
Event Flow Diagram
Configuration
Environment Variables
# .env
VITE_GA_MEASUREMENT_ID="G-XXXXXXXXXX"
Where is it used?
- Build time: Vite injects into client bundle
- Runtime: Accessed via
import.meta.env.VITE_GA_MEASUREMENT_ID - Deployment: Set in GitHub Actions, Dockerfile, Kamal config
Deployment Configuration
The measurement ID must be configured in:
-
GitHub Actions (
.github/workflows/ci.yml)- QA and Production deployment steps
-
Dockerfile (
Dockerfile)- Build arguments for both stages
-
Kamal (
deploy/deploy.yml)- Runtime environment variables
- Builder arguments
UTM Parameter Tracking
Automatic Extraction
UTM parameters are automatically captured from the URL:
https://careers.jodapp.com/jobs/ra-cashiering-hTJNvFVG?utm_source=facebook&utm_medium=social&utm_campaign=urgent-hiring
Tracked Parameters:
utm_source→ Campaign source (facebook, google, tiktok)utm_medium→ Campaign medium (cpc, email, social)utm_campaign→ Campaign name (urgent-hiring)utm_term→ Paid search keywordsutm_content→ Ad variation identifier
How It Works
// Automatically included in page_view event
window.gtag("event", "page_view", {
page_path: location.pathname + location.search, // Includes ?utm_...
page_location: window.location.href, // Full URL with UTM
page_title: document.title,
});
GA4 automatically parses UTM parameters from the URL and attributes them to the session.
Future Enhancements
When to Add Context Provider
Consider refactoring to Context if you need:
- Track custom events from multiple components
- Expose
trackEvent()function app-wide - Conditional analytics logic in components
Example Context Pattern
// If needed in the future
const { trackEvent } = useAnalytics();
trackEvent("button_click", {
button_name: "apply_now",
job_slug: "ra-cashiering-hTJNvFVG",
});