Angular and Google Analytics

There are many times when we need to insert Google Analytics into our application to track what a user does or where they go in the application. Single page apps like Angular work differently, though, than a static site for example. This post will show you how to create a service that loads Google Analytics and sets up page view tracking for you.

Before starting this post, you should have gone to Google Analytics and set up a project. I’m not going to cover that in this blog post, but there are many tutorials out there to show you what to do. Once you have your project set up, you should be given a code snippet to inject into your website. It will look something like this:

<script src="https://www.googletagmanager.com/gtag/js?id=XX-XXXXXXXX-X"></script>
<script>
	window.dataLayer = window.dataLayer || [];
	function gtag() {
		dataLayer.push(arguments);
	}
	gtag('js', new Date());
	gtag('config', 'XX-XXXXXXXX-X');
</script>

The ‘XX-XXXXXXXX-X’ is the ID of the analytics site that will have been given to you when setting up analytics. Once you have all this information, you’re ready to go on to the next step.

If we were building a static site, one that reloaded the site each time you navigated from page to page, then we’d just take the code from above and put it in the head section of each page. But single page applications don’t work like that. The index.html file of an Angular app is loaded just once, and then all the content of the page is dynamically swapped out when the user clicks on different links. So we have to do our page view tracking a little different.

Let’s start by creating a service that will manage all our Google Analytics functionality:

ng g s google-analytics

In this service, we need to load the scripts that Google Analytics gave us, and that I referenced above in the Getting Started section. We’ll make a couple private functions that do the setup, and a single init function that will be called from our main AppComponent. Before showing that, though, we need to take the second script from above (minus the gtag('config', 'XX-XXXXXXXX-X') part) and put it in a separate .js file. So that file will look like this:

// google-analytics-script.js
window.dataLayer = window.dataLayer || [];
function gtag() {
	dataLayer.push(arguments);
}
gtag('js', new Date());

Add it to the assets array for your app in the angular.json file:

<!-- angular.json -->
{
	...
	"build": {
		"options": {
			"assets": ["path/to/google-analytics-script.js"]
		}
	}
}

Okay, so now that we have that second part of the Google Analytics script in a .js file that we can load, let’s take a look at our service:

// google-analytics.service.ts
declare let gtag: Function;

export class GoogleAnalyticsService {
	private googleAnalyticsId: string;
	private renderer2: Renderer2;
	private scriptsLoaded: boolean = false;

	constructor(
		private rendererFactory2: RendererFactory2,
		@Inject(DOCUMENT) private _document: Document,
		private _config: RuntimeConfigLoaderService,
		private _router: Router,
	) {
		this.renderer2 = this.rendererFactory2.createRenderer(null, null);
		this.googleAnalyticsId = this._config.getConfigObjectKey('googleAnalyticsId');
	}

	init() {
		if (!this.scriptsLoaded) {
			this.insertMainScript();
		}
	}

	private insertMainScript() {
		if (this.googleAnalyticsId) {
			const script: HTMLScriptElement = this.renderer2.createElement('script');
			script.type = 'text/javascript';
			script.onload = this.insertSecondHalfOfScript.bind(this);
			script.src = `https://www.googletagmanager.com/gtag/js?id=${this.googleAnalyticsId}`;
			script.text = '';
			this.renderer2.appendChild(this._document.body, script);
		}
	}

	private insertSecondHalfOfScript() {
		const script: HTMLScriptElement = this.renderer2.createElement('script');
		script.type = 'text/javascript';
		script.src = '/path/to/google-analytics-script.js';
		script.text = '';
		this.renderer2.appendChild(this._document.body, script);
		script.onload = () => {
			this.scriptsLoaded = true;
		};
	}
}

Let’s break this down. First, we need to declare gtag outside the class so that we can call it later on. Next, we inject RendererFactory2, DOCUMENT, Router, and RuntimeConfigLoaderService into this service. You don’t have to use RuntimeConfigLoaderService if you don’t want to, but this way you can easily change the Google Analytics ID without touching the service. In the constructor or the service, we create an instance of Renderer2 which we will use to load the scripts. We also store the Google Analytics ID from the configuration.

// google-analytics.service.ts
constructor(
	private rendererFactory2: RendererFactory2,
	@Inject(DOCUMENT) private _document: Document,
	private _config: RuntimeConfigLoaderService,
	private _router: Router,
) {
	this.renderer2 = this.rendererFactory2.createRenderer(null, null);
	this.googleAnalyticsId = this._config.getConfigObjectKey('googleAnalyticsId');
}

Next up we create two private functions that will actually load the scripts, and then a public init function that can be called from the AppComponent:

// google-analytics.service.ts
init() {
	if (!this.scriptsLoaded) {
		this.insertMainScript();
	}
}

private insertMainScript() {
	if (this.googleAnalyticsId) {
		const script: HTMLScriptElement = this.renderer2.createElement('script');
		script.type = 'text/javascript';
		script.onload = this.insertSecondHalfOfScript.bind(this);
		script.src = `https://www.googletagmanager.com/gtag/js?id=${this.googleAnalyticsId}`;
		script.text = '';
		this.renderer2.appendChild(this._document.body, script);
	}
}

private insertSecondHalfOfScript() {
	const script: HTMLScriptElement = this.renderer2.createElement('script');
	script.type = 'text/javascript';
	script.src = '/path/to/google-analytics-script.js';
	script.text = '';
	this.renderer2.appendChild(this._document.body, script);
	script.onload = () => {
		this.scriptsLoaded = true;
	};
}

In the init function, we only call the insertMainScript function if the scripts have not been loaded. The insertMainScript function only runs if we have a googleAnalyticsId. We create a script element with Renderer2, and set the type and src attributes. We also tell it to call a function, insertSecondHalfOfScript, after this first script is loaded. Then we append the newly created script to the document.

In the second function, we load the file we created above, google-analytics-script.js. Once it has loaded, we run an arrow function and set the scriptsLoaded variable to true.

With these three functions created, your app is now ready to load the required Google Analytics Scripts. In your main AppComponent, inject this new GoogleAnalyticsService and call the init method from ngOnInit:

export class AppComponent implements OnInit {
	constructor(..., private _analytics: GoogleAnalyticsService) {}

	ngOnInit() {
		this._analytics.init();
	}
}

So our service is working and loading Google Analytics for us. But we still need to get it tracking page views. To do this, we need to use the Angular Router and call a Google Analytics function to track the navigation event. We’ll do that like this, adding a function to our GoogleAnalyticsService:

// google-analytics.service.ts
trackSinglePageView(event: NavigationEnd) {
	if (this.googleAnalyticsId && this.scriptsLoaded) {
		gtag('config', this.googleAnalyticsId, { page_path: event.urlAfterRedirects });
	}
}

trackPageViews() {
	return this._router.events.pipe(
		filter(() => this.scriptsLoaded === true),
		filter((evt: RouterEvent) => evt instanceof NavigationEnd),
		tap((event: NavigationEnd) => {
			this.trackSinglePageView(event);
		}),
	);
}

The trackPageViews function is the one that we need to subscribe to to make sure page views are logged to Google Analytics. Lets cover what it’s doing real quick though. First, we’re using the events observable stream from the Angular router. Inside the pipe, we use two filter operators. The first one will make sure that our scripts are loaded before we try and track anything. The second filter operator makes sure that we only continue if the current event is a NavigationEnd event. We only want to report anything to Google Analytics if the router is done routing. Finally, we use the tap operator to call a function which will send the event to Google Analytics. You could just report to Google Analytics in the tap operator, but the upside to this is that you could call trackSinglePageView from anywhere, if needed.

Back in our AppComponent, we just need to subscribe to the observable that’s returned from the trackPageViews function in ngOnInit:

export class AppComponent implements OnInit {
	constructor(..., private _analytics: GoogleAnalyticsService) {}

	ngOnInit() {
		this._analytics.init();
		this._analytics.trackPageViews().subscribe();
	}
}

With that, our app will start reporting each page view to Google Analytics.

If you need to track other events using Google Analytics, just add the following function to the GoogleAnalyticsService:

trackEvent(
	{ eventName, eventCategory, eventAction, eventLabel, eventValue } = {
		eventName: null,
		eventCategory: null,
		eventAction: null,
		eventLabel: null,
		eventValue: null,
	},
) {
	gtag('event', eventName, {
		eventCategory,
		eventLabel,
		eventAction,
		eventValue,
	});
}

This function uses named parameters, but all you need to do is pass an event name, category, action, label, and value to the function. It will then pass that event on to Google Analytics. You can call this function from anywhere in your app, any time a user does something you want to track.

Overall, it was easier to add Google Analytics to my site than I thought. It took a little more time to add it all into a service, making sure that the scripts were loaded before doing anything else, but this way I didn’t have to edit the index.html directly for the app. Also, because the ID is an environment variable I now have one ID for our QA environment and one ID for production. If I was editing the index.html file directly, things would hae been more complicated. This is an especially useful way of doing things in an NX workspace, which is where I implemented this. Now with just a couple lines of code and adding a Google Analytics ID, my apps can have Google Analytics tracking.

You can view the entire service in this gist.

Author: admin

Leave a Reply

Your email address will not be published.