Single Page Application (SPA) Tracking Issues | Blue Frog Docs

Single Page Application (SPA) Tracking Issues

Diagnose and fix tracking problems in single-page applications where traditional page view tracking doesn't work

Single Page Application (SPA) Tracking Issues

What This Means

Single Page Applications (SPAs) built with frameworks like React, Vue, Angular, or Next.js load content dynamically without full page refreshes. Traditional analytics implementations designed for multi-page websites don't capture route changes in SPAs, resulting in severely underreported page views, broken user journeys, and incomplete analytics data.

Common SPA Tracking Problems

Page Views Not Tracked:

  • Only initial page load tracked
  • Route changes not captured
  • Virtual page views missing
  • History pushState/replaceState not monitored

Analytics Configuration:

  • Analytics loaded only once
  • Configuration not updated on route change
  • Tags firing on initial load only
  • Missing page titles/paths

Event Tracking:

  • Events attributed to wrong page
  • User journey broken across routes
  • Session context lost
  • Duplicate events on route change

Data Layer Issues:

  • Data layer not cleared between routes
  • Variables not updated
  • Old page context persists
  • State management conflicts

Impact on Your Business

Analytics Accuracy:

  • Massively underreported page views (90%+ missing)
  • Incomplete user journeys (can't track multi-step flows)
  • Wrong attribution (conversions credited to wrong pages)
  • Broken funnels (steps appear skipped)
  • Inaccurate engagement metrics (time on page, bounce rate)

Business Intelligence:

  • Can't identify popular pages/features
  • Funnel analysis broken
  • A/B test results invalid
  • User flow reports meaningless
  • ROI calculations wrong

Marketing Impact:

  • Can't optimize user journeys
  • Wasted ad spend on unmeasured pages
  • Poor remarketing audiences
  • Lost conversion tracking

Business Consequences:

  • Decisions based on incomplete data
  • Missed optimization opportunities
  • Wasted marketing budget
  • Lost revenue from poor UX

How to Diagnose

Method 1: Navigate and Check Analytics

  1. Load your SPA
  2. Open browser console
  3. Navigate to different routes
  4. Check if analytics fires on each route change

Test:

// Listen for gtag calls
const originalGtag = window.gtag;
window.gtag = function(...args) {
  console.log('gtag called:', args);
  return originalGtag.apply(this, args);
};

// Navigate through your app
// Watch console for gtag calls

What to Look For:

  • No gtag calls on route changes
  • Page view only on initial load
  • Events without updated page context

Method 2: Google Analytics DebugView

  1. Enable debug mode
  2. Navigate through your SPA
  3. Watch DebugView for page_view events

What to Look For:

  • Single page_view on initial load
  • No page_view events on route changes
  • Missing page_title updates
  • Incorrect page_location

Method 3: Google Tag Manager Preview

  1. Enable GTM Preview mode
  2. Navigate through SPA routes
  3. Watch for:
    • History change events
    • Data layer pushes
    • Tag fires

What to Look For:

  • No History Change trigger firing
  • Data layer not updating
  • Page view tags not firing on route change

Method 4: Network Tab Analysis

  1. Open DevTools Network tab
  2. Filter by "analytics" or "collect"
  3. Navigate through SPA
  4. Count requests

What to Look For:

  • Only one analytics request on page load
  • No subsequent requests on route changes
  • URLs not updating in requests

Method 5: Check Real-Time Reports

  1. Navigate to GA4 Real-time report
  2. Navigate through your SPA
  3. Watch active page changes

What to Look For:

  • Page doesn't change in real-time report
  • Only initial page shown
  • Users stuck on one page

General Fixes

Fix 1: Track Route Changes in React

React Router v6 example:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();

  useEffect(() => {
    // Track page view on route change
    gtag('config', 'G-XXXXXXXXXX', {
      page_path: location.pathname + location.search,
      page_title: document.title
    });
  }, [location]);

  return (
    // Your app content
  );
}

React Router with custom hook:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function usePageTracking() {
  const location = useLocation();

  useEffect(() => {
    // Send page view
    gtag('config', 'G-XXXXXXXXXX', {
      page_path: location.pathname,
      page_title: document.title
    });

    // Or send as event
    gtag('event', 'page_view', {
      page_path: location.pathname,
      page_title: document.title,
      page_location: window.location.href
    });
  }, [location]);
}

// Use in your app
function App() {
  usePageTracking();

  return <div>{/* Your app */}</div>;
}

Fix 2: Track Route Changes in Vue

Vue Router:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // Your routes
  ]
});

// Track page views on route change
router.afterEach((to, from) => {
  // Update page title
  document.title = to.meta.title || 'Default Title';

  // Send page view to Google Analytics
  gtag('config', 'G-XXXXXXXXXX', {
    page_path: to.fullPath,
    page_title: document.title
  });
});

export default router;

Vue 3 Composition API:

import { watch } from 'vue';
import { useRoute } from 'vue-router';

export default {
  setup() {
    const route = useRoute();

    watch(
      () => route.fullPath,
      (newPath) => {
        gtag('config', 'G-XXXXXXXXXX', {
          page_path: newPath,
          page_title: route.meta.title || document.title
        });
      }
    );
  }
};

Fix 3: Track Route Changes in Angular

Angular Router:

// app.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';

declare let gtag: Function;

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {
    this.router.events
      .pipe(filter(event => event instanceof NavigationEnd))
      .subscribe((event: NavigationEnd) => {
        gtag('config', 'G-XXXXXXXXXX', {
          page_path: event.urlAfterRedirects,
          page_title: this.getTitle()
        });
      });
  }

  getTitle(): string {
    return document.title;
  }
}

Fix 4: Use Google Tag Manager with History Change

Set up GTM for SPAs:

  1. Enable Built-in Variables:

    • History Source
    • History Old URL Fragment
    • History New URL Fragment
    • History Change Source
  2. Create History Change Trigger:

    • Trigger Type: History Change
    • Fire on: All History Changes
  3. Update GA4 Tag:

    • Add History Change trigger
    • Update page_path variable

Data Layer push on route change:

// React Router example
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();

  useEffect(() => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'route_change',
      page_path: location.pathname,
      page_title: document.title,
      page_location: window.location.href
    });
  }, [location]);

  return <div>{/* App */}</div>;
}

Fix 5: Track Next.js Applications

Next.js with App Router:

// app/layout.js
'use client';

import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

export default function RootLayout({ children }) {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    const url = pathname + (searchParams.toString() ? `?${searchParams}` : '');

    gtag('config', process.env.NEXT_PUBLIC_GA_ID, {
      page_path: url,
      page_title: document.title
    });
  }, [pathname, searchParams]);

  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

Next.js with Pages Router:

// pages/_app.js
import { useEffect } from 'react';
import { useRouter } from 'next/router';

function MyApp({ Component, pageProps }) {
  const router = useRouter();

  useEffect(() => {
    const handleRouteChange = (url) => {
      gtag('config', process.env.NEXT_PUBLIC_GA_ID, {
        page_path: url,
        page_title: document.title
      });
    };

    router.events.on('routeChangeComplete', handleRouteChange);

    return () => {
      router.events.off('routeChangeComplete', handleRouteChange);
    };
  }, [router.events]);

  return <Component {...pageProps} />;
}

export default MyApp;

Fix 6: Handle Dynamic Page Titles

Update document title on route change:

// React with React Helmet
import { Helmet } from 'react-helmet-async';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function ProductPage({ product }) {
  const location = useLocation();

  useEffect(() => {
    // Track after title is updated
    setTimeout(() => {
      gtag('config', 'G-XXXXXXXXXX', {
        page_path: location.pathname,
        page_title: document.title
      });
    }, 0);
  }, [location]);

  return (
    <>
      <Helmet>
        <title>{product.name} - My Store</title>
      </Helmet>

      <div>{/* Product content */}</div>
    </>
  );
}

Fix 7: Clear Data Layer Between Routes

Prevent data pollution:

// Clear data layer on route change
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();

  useEffect(() => {
    // Clear previous page data
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'route_change',
      page_path: location.pathname,
      page_title: document.title,

      // Clear previous values
      product_id: undefined,
      product_name: undefined,
      category: undefined,
      // ... clear other page-specific variables
    });
  }, [location]);

  return <div>{/* App */}</div>;
}

Platform-Specific Guides

Detailed implementation instructions for your specific platform:

Platform Troubleshooting Guide
Shopify Shopify SPA Tracking Guide
WordPress WordPress SPA Tracking Guide
Wix Wix SPA Tracking Guide
Squarespace Squarespace SPA Tracking Guide
Webflow Webflow SPA Tracking Guide

Verification

After implementing SPA tracking:

  1. Real-time testing:

    • Open GA4 Real-time report
    • Navigate through your SPA
    • Verify page changes in real-time
    • Check page titles update
  2. Debug mode:

    • Enable GA4 debug mode
    • Navigate through routes
    • Check DebugView for page_view events
    • Verify page_path updates
  3. GTM Preview:

    • Enable GTM Preview
    • Navigate routes
    • Check History Change trigger fires
    • Verify data layer updates
  4. Network tab:

    • Open DevTools Network
    • Navigate routes
    • Count analytics requests
    • Should see one per route change
  5. Historical data:

    • Wait 24-48 hours
    • Check page reports
    • Verify all pages appear
    • Check user flow reports

Common Mistakes

  1. Not tracking route changes - Only initial page view tracked
  2. Tracking too early - Before title updates
  3. Double tracking - Both automatic and manual tracking
  4. Not clearing data layer - Old data persists
  5. Missing page titles - Generic titles for all pages
  6. Ignoring query parameters - Different pages look identical
  7. Not testing - Assuming it works without verification
  8. Framework-specific issues - Not using framework hooks properly
  9. Race conditions - Tracking before page ready
  10. Not handling hash routing - Hash routes not tracked

Additional Resources

// SYS.FOOTER