Single-page apps and cross-domain tracking

Google Analytics · Expert · 12 min

These two setups are where most broken GA4 implementations live: doubled page views, self-referrals and split sessions.

Single-page applications change the URL with the History API, so the Google tag's initial page_view fires once and then nothing. Two fixes:

  • Enhanced measurement's Page changes based on browser history events fires page_view on pushState/replaceState/popstate. It is easy but naive: it fires on query-string-only changes, and page_title is read immediately, before your framework updates <title>.
  • Set send_page_view: false in the config and send page_view yourself from the router's navigation hook, after the title and content are ready. Send page_location, page_title and page_referrer explicitly; the referrer must be the previous virtual page, not document.referrer, or every view appears to come from the entry page.

Never do both or every route change is counted twice.

Cross-domain tracking is needed when one journey spans domains (example.com to checkout.example-pay.com). Cookies are per domain, so the second domain would start a new user and a new session whose source is the first domain: a self-referral. Fix it in Admin > Data streams > stream > Configure tag settings > Configure your domains: list every domain. The tag then appends a _gl linker parameter to links and form posts between those domains, and the receiving page reads it to adopt the same client ID and session.

Checklist for cross-domain: all domains in the list, the same measurement ID on all of them, links are real <a>/<form> elements (JS-built navigation needs gtag('get', ...) to read the client ID and append _gl yourself), and the second domain is also in List unwanted referrals so it never counts as a source. Subdomains of one registered domain share cookies and need none of this.

Example

// gtag('config', 'G-ABC123XYZ', { send_page_view: false }) in the snippet, then:
let previousUrl = document.referrer;   // first virtual view keeps the real referrer

function trackVirtualPage() {
  const url = location.href;
  gtag('event', 'page_view', {
    page_location: url,
    page_title: document.title,   // call after the router has set the title
    page_referrer: previousUrl
  });
  previousUrl = url;
}

// Example with a generic router "afterEach" hook (React Router, Vue Router, etc.)
router.afterEach(() => {
  // wait a tick so <title> and content are updated
  setTimeout(trackVirtualPage, 0);
});
trackVirtualPage();  // the initial load

Remember

  • SPA: either the history-change switch or manual page_view with send_page_view:false, never both
  • Send page_referrer as the previous virtual page
  • Cross-domain: list all domains in tag settings; the _gl linker carries the client ID

Watch out for

  • Keeping the enhanced-measurement history switch on while also sending page_view manually.

Check yourself

  1. After enabling cross-domain tracking, sessions from checkout.example-pay.com still show source example.com. What did you forget?
  2. In an SPA with manual page_view events, what should page_referrer be?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developers.google.com/analytics/devguides/collection/ga4/single-page-applications