Skip to main content

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
  • window object 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 navigation
  • measurementId - Static, but included for completeness

Why GA Only Runs Client-Side

// This check prevents SSR errors
if (!window.gtag || !measurementId) return;

Reasons:

  1. No window object on server - Node.js doesn't have browser APIs
  2. GA script requires DOM - Can't inject scripts during SSR
  3. User tracking requires browser - Cookies, IP, user agent are client-side
  4. 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:

  1. Unified Analytics Dashboard

    • View all traffic in one GA4 property
    • Compare performance across subdomains
    • Consolidated reporting
  2. Cross-Domain Tracking

    • Track user journeys across subdomains
    • Understand how users navigate between products
    • Better attribution modeling
  3. 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:

  1. GitHub Actions (.github/workflows/ci.yml)

    • QA and Production deployment steps
  2. Dockerfile (Dockerfile)

    • Build arguments for both stages
  3. 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 keywords
  • utm_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",
});