tldr;
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.
Getting Started
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.
Google Analytics Service
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();
}
}
Tracking Page Views
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.
Tracking Other Events
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.
Conclusion
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.
Top comments (0)