Set up Analytics on an Angular app via Google Tag Manager

Adriano di Lauro
Quick Code
Published in
9 min readFeb 21, 2019

I lately needed to install Google Tag Manager (GTM), dataLayer and Universal Analytics on an Angular app.

Looking for GTM or Analytics plugins for Angular, I saw that the few options available were putting a lot of effort in triggering click events, and none in setting up a proper set of dataLayer variables to attach to page views.

In the end, it turned out that the set up was quite easy and no plugin was needed at all. I am sharing here my experience.

GTM = Google Tag Manager = Web Application to manage several different “tags” (external scripts that you load in your website).

dataLayer = the main tool to store and use variables in GTM

Universal Analytics = the latest version of Google Analytics, also commonly shortened as UA or GA

In this article I’ll assume that you have a basic proficiency of the concepts above.

What DataLayer variables are (and why you should put effort in setting them up right)

Well, technically, GTM’s dataLayer is nothing else than a big pot where you store information needed to your tags.

You have to set it up before calling GTM’s script:

And then you simply push objects to it when you need to share anything with GTM:

The reason, why you initialise the dataLayer as an empty array, is that you can keep pushing things to it while GTM script is still loading: as soon as GTM responds, it will (1) turn window.dataLayer into an object, (2) execute the stack of things you pushed to it.

The most useful application of dataLayer in analytics is to push custom dimensions to it. With custom dimensions, you target page views with a set of variables that describe the environment in which the page view occurred

For instance, let’s say that the user is viewing a page in a news website, and this page is marked as belonging to a specific news category, let’s say sports.

If, before triggering a page view, you push to the dataLayer a variable that contains that page’s category, you’ll then be able to read that variable in GTM, and pass it as a custom dimension in Universal Analytics. And with such a custom dimension, you can filter any event your users fire in pages with that category.

Can you see the advantage of this?

Each event you trigger gets automatically bound to a set of additional information, that you can use to apply filters on your reports

  1. You push a variable to the dataLayer
  2. It is read and stored as a variable in GTM
  3. It is attached it to Page Views as custom dimension
  4. You can now use to filter reports, instead of appending verbose and error prone information to each single event

Setting up upfront a proper set of dataLayer variables saves you a lot of time (and code) when you trigger events

Why it’s better to set up events in GTM, rather than in the code

Short answer: because you remove from your code complexity that doesn’t belong there.

GTM has a wonderfully organised system of variables, triggers and tags, why should you create a replica of it in your code?

Also,

GTM can be easily tested and modified by non-developers, and it even has its own versioning system (you can literally test, deploy and rollback different versions of GTM code, without ever bothering a frontend developer to do it).

All you need to do is make sure that elements that need to be tracked somehow in GTM (clickable elements, or elements for which you want to measure visibility, etc.) are marked with an appropriate HTML class, with any additional information stored in data:

Once you have done this, your job as frontend developer is done. It’s now responsibility of the GTM manager to set up a trigger that fires upon clicking that class, and a Universal Analytics tag that is activated by that trigger.

Priorities of a GTM + Angular implementation

  • Set up a proper set of custom dimensions, store them in the dataLayer, and ensure they are correctly attached to every page view.
  • Then, mark down trackable HTML elements with a set of classes that can be picked up by GTM triggers.

This is exactly the opposite that all the GTM plugin for Angular I found were doing: they focussed on creating a lot of utils for tracking events, and had a very trivial implementation of page views with no custom dimensions at all.

You might object that there are events that cannot be tracked by the set of trigger types defined in GTM: while this is definitely possible, if you do things right, most events you want to track are traceable to either clicks or scroll tracking (both things being manageable via GTM triggers). And who knows what new tracking utilities the GTM guys are going to come out with.

Your goal should be to keep things simple and defer events tracking, as far as possible, to GTM

Define your set of custom dimensions, and store it safely in the dataLayer

Let’s define a sample list of custom dimensions we want to push consistently to the dataLayer (in real life they can be many more than three):

Our goal is to push these variables to the dataLayer before each page view (this way GTM is able to pick them up and attach them as custom dimensions).

To make their way up until Universal Analytics, these variables need to

  1. Be defined in GTM as a dataLayer variable
  2. Be attached in GTM, as custom dimension, to all Universal Analytics page view tags

This is all done in GTM, it’s a pretty standard thing to do.

Since this point, I’ll refer to such variables as “dataLayer custom dimensions”, to distinguish them from other regular dataLayer variables that are used for different purposes.

I have a last remark before coming to the Angular implementation of all this:

Any time you want to update the custom dimensions you defined in the dataLayer, you need to replace all of them, even the ones that are empty.

If you don’t do this, and leave empty custom dimensions in a dataLayer push, GTM will remember the previous value for those variables.

Angular abstraction of dataLayer custom dimensions

Let’s write down the specific requirements we need to meet:

  1. Make sure that we always push custom dimensions to the dataLayer before triggering page views
  2. Make sure you push the whole set of dimensions every single time, even if they are empty

TypeScript can help us in achieving (2): let’s define a type that will raise a compilation error if we are missing any dimension.

First, we need an enum containing the whitelist:

Then, we use this enum to create a type that forces us to push every dimension every time:

Finally, we create a service which specific responsibility is to push custom dimensions to the data layer, making sure that

  1. Whatever dimensions we need to push, the dimensions that are missing from our set are pushed too (with value = undefined)
  2. There are some dimensions that might be useful to send constantly at every push: for instance a boolean that is true if the visiting user is logged in, that you can use to measure behavioural differences between subscribed and unsubscribed users.

At line 21, a single dimension is added to the payload:

The priority defined here is

  1. Use the dimensions passed to the method, where applicable
  2. If a custom dimension is not defined in the payload received by the method, then use the predefined set of dimensions that have to be sent at every push
  3. If the dimension is absent in both payloads, pass the value “undefined”.

Then, at line 26, the previously built set of dataLayer custom dimensions is pushed to window.dataLayer:

The order in which this service’s methods have to be used is the following:

Angular abstraction of Universal Analytics

Now we have a service that solidly wraps custom dimensions we need to store in the dataLayer. Let’s use it inside another service, that abstracts analytics-related actions:

This service doesn’t do anything special: it uses the just defined dataLayerCustomDimensionsService, as an abstraction of the dataLayer custom dimensions. When calling “pageView”, it stores passed variables in dataLayerCustomDimensionsService, triggers it (which means that the dimensions are actually pushed to the dataLayer), and then fires the page view.

Unsurprisingly, the page view is implemented with yet another push to the data layer: from GTM, you’ll typically set up a trigger that listens to this event, and attach it to a Universal Analytics page view tag (as already mentioned, passing along all the dataLayer custom dimensions).

Notice how I added the possibility to override the page view url sent to GTM:

This is useful for when you want to use page views to track non-navigation-related user actions: there are some interesting cases in which, sending a “special” page view, allows passing more information to Universal Analytics, that wouldn’t be passed with a regular event (for example, you can use this method to implement Enhanced Ecommerce funnels). Beginners in Angular programming can benefit from the best Angular tutorials.

How and where do we need to call the action pageView() of our new analytics service?

The answer is simple:

You can call analyticsService.pageView() at any time, as soon as you can obtain both (1) the url to which the user navigated, and (2) all the information you need to push to the dataLayer as custom dimension

To meet the first condition, it’s enough to listen to “router.events” in your app.component:

(obviously, if you are working on a large project, you’d rather want to do this in a separate module, not to overload your app component with too much code).

What about the second condition? Well, it depends on your app’s business logic, and on what you want to track in Universal Analytics: if the only dimensions you are pushing depend on the url, you can just go for the router.events solution above.

If instead, like me, your dimension become known only after the route data is fetched by a resolver, then you should send page views from inside the ngOnInit callback of the components you attached to the router:

Why, in the AnalyticsService and DataLayerCustomDimensionService above, I defined methods in such a fragmented way? I mean, saving a set of dimensions first, then calling “trigger” on the service to push such dimensions, etc. Couldn’t we just do everything together in one go?

Again, the answer is simple:

During implementation of analytics, you usually end up pulling different kinds of information, at different stages of your app’s response: the more you fragment into smaller steps, the more you are likely to have the flexibility you might need in the future

Actually, in our AnalyticsService, we may even fragment things a bit more and get even more future-proofing:

At a first, naive GTM + Analytics implementation, it’s probably enough to trigger a custom dimensions change and page view always at the same moment.

But, as the application grows, we might like to push dataLayer dimensions in a different moment than the actual page view.

Or, we might even decide to optimise things, and trigger multiple times page view without updating the dataLayer dimensions. This is why it’s useful to separate the two actions in the analyticsService.

VueJS is a lightweight, clean, data-driven, and reactive front-end ViewModel, which can help you get up and running on your app in no time.

Bundle everything together and complete the implementation

What are we missing, in order to see things work?

We need to add the GTM initialisation script, this should be done in “index.html”, as up as possible in your app’s <head> tag: and, as already mentioned, just below the first definition of window.dataLayer.

In theory, if you want to use GTM just for analytics, you might as well initialise it from the Angular code, in your app.component, or in the initialisation of your CoreModule if you have one: because the user cannot interact at all until the app is loaded, so there is nothing to track before. So, if you only use GTM for analytics, I actually suggest doing this, because it’s a cleaner and more controllable solution.

But, if you use GTM as well for tags other than analytics, it’s definitely better to call it from the HTML and have it loaded as soon as possible.

What about events tracking? This seems to be the thing that worries most the people who developed the few GTM-Angular extensions out there: but as I said, really, the best and cleanest solution is just to mark with HTML classes the elements you want to track, and do all the heavy lifting from GTM.

By the way, remember to be a good frontend developer: add click event + GTM class on an anchor (<a> tag). If you track clicks on a non-anchor, or on a div that contains an anchor along with other stuff, you can get unexpected side effects in GTM (click not firing all the times, for instance)

With the advent of Angular, typescript, and all the other fancy javascript frameworks, I found that a lot of backend developers started putting their hands in frontend without knowing what they are doing. HTML markdown has still its importance, even inside a modern single page application: have respect for the HTML, and everything will be cleaner.

--

--