Integrating AngularJS and Google Tag Manager


If you’ve added Google Tag Manager and Google Analytics to an Angular app, you were in for a surprise. After deploying your code, you may have popped open the Real Time Reports and saw… nothing.

Well, not nothing, but not a whole lot of anything. Where are the page paths? Hell, where are the pageviews? You dove into the Google Analytics docs and came back disappointed. Finally, you hit the search results – someone, somewhere had to know what to do.

Welcome, friend! You’ve come to the right place. Let’s get you straightened out.

But First, The Easy Button

Not interested in what’s going on under the hood? The module you’re looking for is Angulartics (Full disclosure: I’m a contributor on the GA and GTM libraries). It supports:

  • ngRoute and UIRouter
  • Google Tag Manager & Hard-coded Google Analytics
  • Declarative event tracking
  • Advanced hit types like User Timings
  • Exception tracking, scroll tracking, and more advanced features
  • 20+ analytics tools

And there’s a GTM Container file for one-click configuration written by yours truly in the repository.

Get the code here!

What Is Google Tag Manager?

Google Tag Manager (also referred to as GTM) is a tool for delivering Google Analytics tracking code and other tracking snippets on a page. It helps de-clutter your code by moving tracking snippets into a single location, instead of littered throughout the page. It features a WYSIWYG-style interface for creating, testing, and publishing additional tracking snippets on the fly, called Tags. Tags are assigned conditions on when they should be executed, called Triggers. In a nutshell, here’s how it works:

  1. A user interaction (clicking, submitting a form) or programmatic notification is observed by GTM (called an event)
  2. Each Trigger is evaluated against the event to see if the firing conditions set in the interface are met. If they are, the Trigger ‘fires’
  3. All Tags assigned to any Triggers that fire are executed

Most commonly, Google Tag Manager is used to deploy Google Analytics tags, from basic pageview tracking all the way up to complex custom Google Analytics Events. For a more in-depth explanation, my colleague Kaelin wrote a great introduction to Google Tag Manager and the relationship between GTM and Google Analytics.

Our Cookbook

Below, we’ve shared some components that we’ve used in the past. These components are designed to minimize the amount of additional code required to integrate Google Tag Manager and an Angular app. Because GTM sits on top of Google Analytics, the data that you give to it is inert by default; whether anything is fired is controlled in the interface. Because of this, it’s a good idea to be inclusive with the data you share with the dataLayer.

Pageview Tracking

The default Trigger for firing pageviews is called All Pages. The event that this Trigger corresponds to is gtm.js, which fires only when the GTM snippet is first loaded by the browser. This is why your pageviews haven’t been showing up. You need to add an internal listener in your app that notifies GTM when a state change occurs. If you’re using ngRoute, it would look like this:

Once you’ve wired this into your app, Google Tag Manager will be notified any time a route change occurs. In order to translate this into a pageview in Google Analytics, you’ll need to create a Trigger for your ngRouteChange event, like so:

GTM Trigger for Angular Route Change

Then create a Variable to extract your page path from the dataLayer, like so:

GTM Variable for Angular Route

And you’ll need to create a Google Analytics pageview tag, like so:

GTM Tag for Angular Pageview

Don’t forget the Fields to Set configurations!

  • cookieDomain is set to auto
  • page is set to {{DLV – Angular Route}}

If you’re unwilling to wire into your router, you can create a Trigger using the History Change listener built into GTM, watching for pushState events, but we don’t recommend this approach.

Model Change Tracking

Often, you may want to track changes to model values in Google Analytics. A simple way to do this is with a custom Directive that notifies Google Analytics or Google Tag Manager when a model’s value changes. A simple solution for this challenge is to use a custom Directive that binds to the change event.

You can then add notify-gtm on any element with an ng-model that you’d like to observe with Google Tag Manager. In Google Tag Manager, you can use a Data Layer Variable to extract the value of the model or view, or use the element object reference to pull out additional data.

Notifying GTM After A Template Renders

Often, we depend on information in the DOM when capturing data to send to Google Analytics. Normally, we can use the gtm.dom or gtm.load as safe-guards for firing our code, but with an Angular app, it can be hard to ensure the data is ready to go. Unfortunately, the best solution I’ve seen so far is equally ugly; creating a Directive that triggers a function after rendering (hat tip to Guilherme Ferreira).

This provides a balance of flexibility and discretion, but it means you’ll have to remember to add the directive at the bottom of each of your templates. If you’ve got more clever solution, please share it in the comments below.

Tracking Other Interactions

If you’d like to use Google Tag Manager and Google Analytics more extensively in your application, take advantage of Google Tag Manager’s reusable Variables to streamline the process. First, you’ll want to build a reusable service for interacting with GTM.

This can be further evolved to streamline things like event tracking.

With the above example, you could create a single Tag and Trigger for your Event Tracking, and simply invoke GTMService.eventTrack whenever you’d like to fire an event.

The Angulartics source code is a great place to learn more about different ways to integrate the two for maximum ease of use.

Other Front-end Frameworks

Many of these same concepts apply to other frameworks, e.g. Knockout, Backbone, and so on. If you’d be interested in knowing how we would approach integrating Google Tag Manager with another framework, let us know in the comments and we’ll see about writing a post covering that particular library.

To Recap

These components should help get you started. Remember, the data pushed to the dataLayer is inert by default, so lean towards ‘over-sharing’. You can automate pageview tracking, model change observation, and template rendering with our example components, and you can use a simple service to interact with the dataLayer in your controllers and other components. Finally, you can further streamline things like event tracking by creating your own services, and I’d recommend reviewing the Angulartics module if you’d like to learn more.

Have you integrated Google Tag Manager and Angular before? What was your experience? Share it with us in the comments.

Dan Wilkerson is a former LunaMetrician and contributor to our blog.

  • Quite successful description. Congratulations. Useful information.

  • Frank Litjens

    This has been a great help, thanks!
    However, I can’t seem to find the newly tracked stats in Google Analytics.

    • Dan Wilkerson

      Hi Frank,

      There was a mistake in the event screenshot – I used routeChange instead of ngRouteChange (I waffled on the naming convention in the middle of writing the post; my bad!). Try that!


  • Stephen M. Harris

    Thanks Dan, this is so helpful!

    Besides being more lightweight than angulartics, are there other advantages to using the code from this post?

    You also say that “the Angulartics source code is a great place to learn more about different ways to integrate the two” … are you recommending using the methods from this post in combination with angulartics for custom tracking? (For instance, to implement event tracking w/o adding markup attributes?)

    • Dan Wilkerson

      Hi Stephen,

      Our cookbook items are plug-and-play/set-and-forget for companies who want to add GTM to their app but don’t want to develop Analytics into their codebase. This works best in companies with clear separation between devs and marketers (which I personally am saddened by, but understanding of).

      Angulartics is better for a company with tightly integrated marketing/development or developers who are looking to measure application performance for themselves. It requires more of a commitment from the developers in terms of understanding how Google Tag Manager and Google Analytics work/model data.

      It’s more often the former rather than the latter, in my experience so far. I think it’s a damn shame, since devs can learn so much from Google Analytics about product usage and performance.

      I call out the Angulartics source because it has more robust mechanisms for doing what these little blurbs do – it supports, for example, way more robust configuration, error tracking (I’ve got a post on that soon, coincidentally), user timings, etc. If a dev wants just bits and pieces, the source is a great starting guide to learning more about how to interact with GTM/GA.


      • Stephen M. Harris

        Got it, thanks Dan!

        Coming from a front/back-end web dev: you’re right! The past couple years I’ve learned to use GA to measure performance of marketing efforts, but before that it was a tool to assist in planning, maintenance, and improvement of the product itself. Out of the box, GA was informing sys/compatibility requirements for new projects, and diagnosing issues in live ones. But it’s way more than that; I wish I knew then what I know now. I keep discovering new ways GA can be used to suggest UI improvements and troubleshoot all sorts of issues. Definitely a sweet tool in the dev’s toolbelt!

        Also agree that it is unfortunate when dev/marketing are not tightly integrated. Separation = transactional approach to measurement implementation = bad. But it’s better than nothing, and way better than *gasp* letting marketing handle implementation 😀

  • JustinBMichaels

    The trigger event name you have in the screenshot is wrong. It has to be identical to what you set in the dataLayer.push object which is ngRouteChange. We didn’t see our pageviews in our Google Analytics account until we had them identical.

    The article was very helpful.

    • Dan Wilkerson

      Hi Justin,

      That will teach me to waffle on naming conventions mid-post – good catch, and thanks for the heads up.


      • Dan Wilkerson

        In my (poor, weak) defense, it was correctly referenced in the text above the image.. 😀

        Fixed the image! Thanks again, Justin.


  • Horia Valeanu

    Hi Dan and thank you for the article. It has helped me for the initial setup, but I don’t really get the GTMService code…
    In the first place, the push function seems redundant, as you could just use dataLayer.push({…}) directly. And maybe because of my ignorance 🙂 but I don’t understand the “forEach” part…Could you please (also) show a snippet of using GTMService.trackEvent() ? How would you call that in a controller? Thank you again!

    • Dan Wilkerson

      I wrap push so that you could mock it for testing and still inject it using Angular; the forEach is because both of those fields are required for an Event.


  • Jone Sathya

    Thanks for posting this useful content, Good to know about new things here, Let me share something useful,
    | AngularJS Training |

  • Vlada Cuk

    Hi Dan,

    Thank you for the article. We have implemented, same way as your, but shortly after that, we faced a huge drop of bounce rate. Is this something with this tag or could be something else? This happen just after we setup this GTM tracking. Thank you in advance!

    • Dan Wilkerson

      That might mean you fixed something or you broke it. Before, GA was only getting your first pageview for your application, so most sessions likely looked like a bounce. What was/is the bounce rate?


  • Tex Andersen

    It would be great to see example code for the trackEvent call.

    From what I can tell, it needs to be passed an object with the following fields:
    ‘attributes’: {
    ‘category’: ‘mandatory’,
    ‘action’: ‘mandatory,
    ‘label’: ‘optional’,
    ‘value’: ‘optional’,
    ‘nonInteraction’: ‘optional’

    This seems pretty convoluted if it is going to be called in a controller. Is this function for use in a directive?

    • Dan Wilkerson

      Hi Tex,

      I think where you use it would depend on the use case? I’m not an Angular expert by any means, but I could see applications in both controllers and directives. Could you share more about your concerns? I’m interested in your feedback.


  • A

    Do you have any idea to create a variable in GTM from the DOM-element that is between Angular’s double curly braces {{}}? Creating by calling the ID or CSS selector does not work.

    • Dan Wilkerson

      I’m able to create Variables that do what you’re asking without issue – maybe double-check your selectors? If you share the HTML you’re rendering and the Variable you’ve configured, I may be able to offer more advice.


  • Hey Dan,
    I am working on an Angular website these days and getting irritated while tracking Adwords conversion via TagManager.

    1. The website has a destination URL goal which is getting tracked in Analytics but it don’t show up for Paid traffic.
    2. The analytics goal is imported in Adwords and it isn’t getting recorded there.
    3. Adwords conversion code shows much higher number what we actually gets in DataBase.

    I have configured ‘History Change’ trigger in Tag manager with ‘Universal Analytics’, ‘Adwords Conversion Code’ and ‘Remarketing Tag’.
    Is there anything else I need to configure, or I have configured something incorrectly?

    Awaiting your response.

    Best | S.

    • Dan Wilkerson

      Hi Shobhit,

      You’re seeing an issue related to how GTM implements GA and how it can go wrong with SPAs. It’s not covered in my blog post, but here’s an issue on GH that explains:

      In short, your first pageview triggers a session with google / cpc because of the gclid parameter in the URL. The second pageview triggers a brand new session with google / organic because the document.referrer value hasn’t changed ( but the campaign parameters are gone AND GTM instantiates new trackers for every hit, whereas traditional on-page code uses a single tracker for all hits.

      We have an internal fix here that involves highjacking the GA global and “resetting” those values conditionally, which is the same fix you’ll have to take, I’m afraid.


  • Gabriel

    Hi Dan!

    First of all thanks for this great article 🙂

    Why don’t you recommend to use historyChange instead of ngRoute?
    What are the differences?


    • Dan Wilkerson

      Hi Gabriel,

      You can be more deliberate with your router, whereas some state changes you’d like to track may not trigger state change events.

  • Tanja Malikovic

    Good article!
    Any chance you would write one on Angular4 and GTM?

  • Declan Murphy

    I have created this GTM service to log custom events:

    I then log events by calling: GoogleTagManagerService(‘Sign-up’, ‘Button click’, ‘Photo – uploaded’);

    For every event that should be logged, only some of these appear in GA. I have waited 24 hours to make sure.

    It also sometimes works and does not work for the same event. So it’s not a matter of the GTM service not being called. (I even added console debug lines to ensure it was working).

  • Christian Schräder

    “If you’re unwilling to wire into your router, you can create a Trigger using the History Change listener built into GTM, watching for pushState events, but we don’t recommend this approach.”

    We are doing exactly this to create pageviews, what could be the potential pitfalls for this approach?

    • Dan Wilkerson

      If you’re using something like ui-router and have state changes you’d like to track as “pageviews”, but they don’t trigger state change events.


Contact Us.

Follow Us



We'll get back to you
in ONE business day.
Our Locations
THE FOUNDRY [map] LunaMetrics

24 S. 18th Street
Suite 100

Pittsburgh, PA 15203


4115 N. Ravenswood
Suite 101
Chicago, IL 60613


2100 Manchester Rd.
Building C, Suite 1750
Wheaton, IL 60187