From the blog

Embedding Google Analytics in Angular Applications with RxJS and NgRX

Angular Logo and Rightpoint Logo

It always seems like the simplest problems involve more complicated solutions. When integrating Google Analytics with a modern Angular application, make no mistake: it requires more than just dropping in the Google Analytics boilerplate tag. Google Analytics integration requires four success criteria to be met.

  1. Google Analytics keys must be driven by environment variables.
  2. Google Analytics must be able to capture custom events, such as tracking file downloads.
  3. Each route change in Angular must log a page view to Google Analytics.
  4. Integrate the solution with existing NgRX structure (an added bonus).

After scouring Google (and even going to the second page of results in some cases), a homegrown approach proved to be the best solution. Let’s break this down into four steps.

1. Using environment variables for Google Analytics

The Google Analytics documentation is pretty great. It’s clear, the UI is intuitive, but their examples are pretty basic and leave the mind to exercise its creative muscles. At first, the tracking codes look ready for plug-and-play implementation:

<!-- Global site tag (gtag.js) - Google Analytics -->
  window.dataLayer = window.dataLayer || [];
  function gtag() {
  gtag("js", new Date());

  gtag("config", "UA-XXXXXX");

This seems like a straightforward solution to drop these two script tags in the main index.html file, but what if Google Analytics Global Site Tags differ between environments? We can access them from environment variables, but index.html doesn’t have a straightforward access point to these environment variables. Two solutions immediately come to mind:

1. Switch out index.html files at compile time inside angular.json that has different versions for local, development, and production build configurations, or

2. Dynamically generate script tags on application startup.

Since the first one still involves checking GA codes into source control, let’s try the latter approach.

The logical place for ensuring tags are registered only once on application startup is the component used during Angular’s bootstrapping process (out of the box, this is AppComponent). Inside the constructor of app.component.ts, dynamically generate both script tags:

constructor() {
  if (environment.gaTrackingId) {
    // register google tag manager
    const gTagManagerScript = document.createElement('script');
    gTagManagerScript.async = true;
    gTagManagerScript.src = `${environment.gaTrackingId}`;

    // register google analytics
    const gaScript = document.createElement('script');
    gaScript.innerHTML = `
      window.dataLayer = window.dataLayer || [];
      function gtag() { dataLayer.push(arguments); }
      gtag('js', new Date());
      gtag('config', '${environment.gaTrackingId}');

At this point, verify that both Google Analytics script tags have been inserted into the head tag in the HTML. Additionally, ensure the ga object exists globally by simply running in the browser console.

2. Create a custom service to send data to Google Analytics

Google Analytics is now available thanks to the dynamic script registration in the AppComponent constructor; however, this doesn’t actually track page views automatically. If this weren’t a Single Page Application (SPA), Google Analytics would register page views in the second script listed above. In this case, Google Analytics requires SPAs to register page views in a different way outside of the above script tag.

First, begin by creating a service called GoogleAnalyticsService. Since the requirements of this solution need to log both custom events and page views, create stubs for two functions: logCustomEvent and logPageView. Let’s take a look at the anatomy of logPageView (below):

declare const ga: {
  (...args: any[]): () => void;

@Injectable({ providedIn: "root" })
export class GoogleAnalyticsService {
logCustomEvent() {
throw new Error('Method not implemented.');
logPageView(url: string) { ga("set", "page", url); ga("send", "pageview"); } }

Note the declare const ga on the first line of the service file. Since we can reasonably assume the logPageView function will be called only after the page has loaded and Google Analytics has registered ga on window, this declares (but doesn’t assign) the variable ga to be used locally without any type errors. It includes a basic typed function to accept any number of string parameters. The function ensures ga exists on window before tracking any page views using a combination of both the Google Analytics readyCallback and Object.prototype.hasOwnProperty.

The logPageView function is pulled straight from the Google Analytics documentation. The only nuance in this implementation is the first line, which is required to let Google Analytics know it needs to update its internal tracker to a given URL. After that, a pageview can be sent to Google Analytics.

Next, let’s add a function to log custom events to Google Analytics and add the readyCallback to ensure the ga object exists:

type Tracker = {
  send: (
    hitType: string,
    category: string,
    action: string,
    label?: string
  ) => void;

declare const ga: {
  (...args: any[]): () => void;
  getAll: () => Tracker[];

const has = Object.prototype.hasOwnProperty;

@Injectable({ providedIn: "root" })
export class GoogleAnalyticsService {
    eventCategory: string,
    eventAction: string,
    eventLabel?: string
  ) {
    ga(() => {
      if (, "ga")) {
        const tracker = ga.getAll();
        if (tracker?.length > 0) {
          tracker[0]?.send("event", eventCategory, eventAction, eventLabel);

  logPageView(url: string) {
    ga(() => {
      if (, "ga")) {
        ga("set", "page", url);
        ga("send", "pageview");

This is a bit more involved, so let’s break down the changes starting with the logCustomEvent function. It uses the same guard to ensure ga exists on window. It would be intuitive to think ga('send', 'event', ...args) would be sufficient. However, professional Googling brought up an article noting custom events need to be logged through a specific tracker in the global ga object. Additionally, note the getAll type added to the ga declaration that allows for strict typing to return an array of Tracker types. Again, this might be overkill but it ensures solid IntelliSense and the normal benefits of type-checking.

3. Log each page view to Google Analytics

Now that the GoogleAnalyticsService allows for both custom events and page view tracking to be sent to Google Analytics, it’s time to wire up page views to Google Analytics. Each time the application detects a NavigationEnd event from the router, a page view should be logged to Google Analytics. This code can be added to the existing AppComponent:

export class AppComponent implements OnInit, OnDestroy {
 private destroy$ = new Subject<void>();
  private router: Router,
  private service: GoogleAnalyticsService){
   // ...GA code from steps 1 and 2

 ngOnInit() {
       filter(() =>, 'ga')),
       switchMap(() => {
           filter((e) => e instanceof NavigationEnd),
           tap((e: NavigationEnd) => {

 ngOnDestroy() {

Note the extra lines of code with timer observable. Since there is no guarantee when the Google Analytics code has loaded (due to constraints such as network latency), use the timer observable combined with filter, take and switchMap to execute the definition check for ga globally, throwing it away once the filter has evaluates a truthy result.

This works as expected, but don’t forget about our bonus criteria: hook up to NgRX.

4. Integrate the solution with existing NgRX structure

Using the existing router state wired up in the root state combined with both the NgRX Router Store and Todd Motto’s Custom Route Serializer, the below example listens for each update made to the router state after navigation has completed:
    tap(() => {
        new Date().toISOString()
    catchError((err) => {
      return err;

Leave a Reply

Your email address will not be published.