Tracking Snippet

Google's gtag.js is the most recent implementation of their analytics and tracking API. The default installation 'snippet' works well for traditional web pages, but requires some modification for single page applications

The global site tag (gtag.js) is a JavaScript tagging framework and API that allows you to send event data to Google Analytics, AdWords, and Doubleclick -- Google gtag Documentation

This is the official tracking snippet (as of April 2018), where GA_TRACKING_ID is replaced with your site/product id:

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_TRACKING_ID"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'GA_TRACKING_ID');
</script>

Calling gtag('config', 'UA-XXXXXXX-1') sets the tracking id for all further events from that page but also sends a pageview hit to Google Analytics. For SPA's, this can result in multiple (usually 2) page views being logged against the site when a page is loaded for the first time

IP anonymization

[Updated July 2019] Google Analytics does not make visitor IP information available to a website owner, but gtag.js does send this information to Google Analytics. With the introduction of GDRP, Google have introduced an additional option to anonymize the IP addresses of hits sent to Google Analytics

gtag('config', '<GA_MEASUREMENT_ID>', { 'anonymize_ip': true });

Note: the GA_TRACKING_ID is now called the GA_MEASUREMENT_ID in more recent documentation

Snippet For SPA

We can update the default snipped to prevent the initial gtag('config', 'UA-XXXXXXX-1') call from sending a pageview hit by including { 'send_page_view': false }

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-XXXXXXX-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag() { dataLayer.push(arguments); }
  gtag('js', new Date());
  gtag('config', 'UA-XXXXXXX-1', { 'send_page_view': false });
</script>

When navigation events complete we send a pageview hit along with the URL Path we wish to register the call against. This will prevent duplication of page views being logged by analytics

gtag('config', 'UA-XXXXXXXX-1', { 'page_path': '/pages/page1' });

Angular Specific

It's advised (by whom I'm not sure, but lets just says it's advisable) to have an app wide structure for analytics event handling. Creating an analytics service and some helper functions to parse and trim the URL's would be a sensible approach

Analytics Service

google-gtag.service

import { Injectable } from '@angular/core';
import { SITE_CONFIG } from '../../SITE_CONFIG';

declare var gtag: Function;

@Injectable()
export class GoogleGtagService {

  constructor() { }

  sendPageView( path: string) {
    gtag('config', SITE_CONFIG.GA_TRACKING_ID, { 'page_path': path });
  }

}

URL Helper Function

If you send the entire URL path to analytics, paths with fragments or query params will be registered as unique hits. In most cases you will want to log page views against the primary path. Params and fragments could be sent as events, or as additional variables with the call. The following is an example helper function that returns only the root path of the primary Angular outlet

import { Router, PRIMARY_OUTLET, UrlSegmentGroup, UrlSegment } from '@angular/router';
....
/**
 * Return the root url of the PRIMARY_OUTLET - no queryParams or fragment
 * @param url
 */
getRootUrl(url: string) {
  let retVal = '/';
  let urlSegmentGroup: UrlSegmentGroup = this.router.parseUrl(url).root.children[PRIMARY_OUTLET];
  if (urlSegmentGroup) {
    retVal += urlSegmentGroup.toString();
  }
  return retVal;
}

Note: UrlSegmentGroup.toString() does not include a leading '/'. Add this to the URL if required as I have done above

You probably already have a navigation handler - for restoring page scroll or something else - and whithin this you should call your GoogleGtagService

this.router.events
    .pipe(
        filter(event => event instanceof NavigationEnd)
    )
    .subscribe(event => {
        this.updateGoogleAnalytics(event as NavigationEnd);
        this.navEndHandler(event as NavigationEnd);
    });

private updateGoogleAnalytics(event: NavigationEnd) {
    let fullURL = event.urlAfterRedirects;
    let rootURL = this.urlHelper.getRootUrl(fullURL);

    // check that page has changed
    if (rootURL !== this.currentTrackedURL) {
        this.currentTrackedURL = rootURL;
        this.gtagService.sendPageView(this.currentTrackedURL);
    }
}

If you don't want pageview hits during development and testing, you can wrap the sendPageView call within a if(environment.production) condition, making sure to import the environment object from '/environments/environment'; as opposed to environment.prod'; - an easy mistake to make with auto imports!

Make sure to use the event.urlAfterRedirects property of NavigationEnd - this is often different to the event.url if you use any redirectTo properties in your Routes setup

The this.currentTrackedURL check is important as updating query params or routing to page fragments can trigger navigation events for the same page multiple times

Resources

Brian Bishop

Dublin, Ireland
v7.2.15+sha.e7897e1