GTM Tracking Service in Angular, Part III
Tags: angular, tagmanage, analytics, tutorial
Published: 2022-04-27 20:32:45

After creating a basic service to capture all GTM required logging, let's add another tracker, sentry.io. The how-to use sentry is beyond the scope of this post, rather, what other methods we need to make available, to allow third party configurations.

> The final project is on [StackBlitz](https://stackblitz.com/edit/gtm-angular-state-local?file=src/app/core/gtm.ts)

## Adding a third party

Heading to [Sentry.io CDN installation](https://docs.sentry.io/platforms/javascript/install/cdn/), to see what setup is needed. The code snippet is this:

```html
<script src="https://browser.sentry-cdn.com/6.19.7/bundle.min.js" crossorigin="anonymous"></script>
<script>
  Sentry.init({
    dsn: "https://productKey@o1104258.ingest.sentry.io/projectID",
    // ect
  });
</script>
```

In GTM, that's a **Custom HTML tag** that fires on **DOM Ready trigger**. The second part of the snippet is a call to `Sentry.init` with optional configurations.

One configuration option is `initialScope`. Which allows us to pass extra information about the visit. In the documentation, the example given is

`user: { id: 42, email: "john.doe@example.com" }`

To capture the error, the line of code is:

`Sentry.captureException(err);`

We need a way to pass those values to GTM, without an event. As for the unhandled error, we need to catch it before we send it to GTM. Working backwards:

```typescript
// call this as soon as information is available
GtmTracking.SetValues(GtmTracking.MapUser({name: 'John Doe', id: '123', email: 'email@address.com'}));

// call this when language is available
GtmTracking.SetValues(GtmTracking.MapProfile({language: 'en'}));

// in error, simply register an event with error as a sub property
GtmTracking.RegisterEvent({event: EnumGtmEvent.Error}, {error: error});
```

That is a continuation of the line of thought we already setup. Remember, the idea of creating mappers is to **isolate our internal models, from what gets reported to GTM**. The GTM Service:

```typescript
export enum EnumGtmEvent {
  // ...
  // ADD new event
  Error = 'garage_error'
}

export class GtmTracking {
  // ...

  public static SetValues(values: any): void {
    // pass values into gr_values for GTM better handling
    dataLayer.push({
      gr_values: {...values}
    });
  }

  // create individual mappers when needed
  public static MapUser(user: IUser) {
    return {
      user: user.name,
      email: user.email
    }
  }

  // or mappers with no model
  public static MapProfile(profile: any) {
    return {
      language: profile.language,
      country: profile.country
    }
  }

  // or if you are quite confident about your props, pass without mapping
  // like {error}
}
```

In GTM, we create a **variable** to extract the information from `gr_values`. And from there, the Sentry Error **tag **can make use of some of them. Unlike event parameters set in GA4 Event tags, we do not have to create a variable for every property if we are using Custom HTML tag. (Note, a bit of more work is needed to make sure the variables are not null.)

![GTM Tag](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s76c5dg3dnfbzc5l35bb.png)

As for exceptions, we need to also create a **trigger **for the `garage_error` custom event, and a **tag **that uses the error property. The **Sentry Error tag** of type **Custom HTML** has this:

```html
<script>
if (window.Sentry && window.Sentry.captureException) {
  // pass error variable
  window.Sentry.captureException({{Garage Track error Variable}}))
}
</script>
```

## Error tracking

Generic JavaScript errors are not thrown by default in Angular, thus **JavaScript Error built-in Trigger in GTM won't work**. Instead, manually report unhandled errors to GTM using a custom ErrorHandler. In App.module

```typescript
@NgModule({
  //... create our own Error Hander to overwrite default ErrorHandler
  providers: [{ provide: ErrorHandler, useClass: OurErrorHandler }]
})
export class AppModule {}
```

The ErrorHandler service:

```typescript
import { ErrorHandler, Injectable } from '@angular/core';
import { EnumGtmEvent, GtmTracking } from './gtm';

@Injectable()
export class OurErrorHandler implements ErrorHandler {
  handleError(error: any) {
    console.error(error);

    // track in GTM
    GtmTracking.RegisterEvent({ event: EnumGtmEvent.Error }, { error: error });

    // don't rethrow, it will call the hanlder again
    // throw(error);
  }
}
```

Out of zone errors, and errors which occur in the GTM container itself (like in other Custom HTML tags), are caught with **trigger of type JavaScript Error**. You can access the **built-in variable: Error Message** to report to Sentry. We rewrite the Sentry Error Tag to handle both triggers.

```html
<script>
if (window.Sentry && window.Sentry.captureException) {
  // construct error, from custom event trigger
  var _error = {{Garage Track error Variable}};
  if (!_error){
    // from JavaScript error
    _error = new Error({{Error Message}} || 'Unknown Error');
  }
  window.Sentry.captureException(_error);
}
</script>
```

### Sequence of events, again

Cute asynchronism, never quits popping its head when least expected. In most cases, we do not need to initialize with specific data layer variables. Thus using **Page Initialization built-in event**, is good enough. If we need to access data layer variables, **Dom Ready event is better**.

Consider the case of an error occurring on the first page load. Take a look at the sequence of events occurring in GTM:

![GTM sequence](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c0h551uoblmygnh0d3s7.png)

The message events, are where the data layer variables are set in Angular. The ideal place for Sentry Init Tag to fire is after data layer variables are sent, but before any `garage_error` is triggered. That is a bit tricky to accomplish, we have two options:

- Initialize Sentry Tag on a **custom event trigger (garage_sentry_init)**, that you call directly after setting values.
- Initialize Sentry Tag on **DOM ready**, but check if it is initialized before you fire an Error tag, and wait.

The latter method, we replace the Sentry Error Tag with the following:

```html
<script>
  var _capture = function(){
     window.Sentry.captureException({{Garage Track error Variable}});
  }
  if (!window.Sentry){
    // 2000 is long enough
     setTimeout(_capture, 2000);
  } else {
    _capture();
  }
</script>

```

Note: You might think Sentry Lazy Loaded sdk does it, but it does not! The sdk does not load on `captureException` call.

Lesson learned: **thou shall know thy sequence of events**!

## Resetting with defaults

Last time we added a method to reset data layer. We called it on `NavigationEnd` of route events. This makes sense, because every page has its own properties. If however we need to access global data layer values, with a tag that notifies a third party just in time, we want to keep track of those default values, and set them after data layer reset. Let's adjust GTM service with a new property:

```typescript
export class GtmTracking {
  private static _values = {};
  public static get Values(): any {
    return this._values;
  }
  public static set Values(value: any) {
    // append to values
    this._values = {...this._values,...value};
  }

  // ...
  // update Reset
  public static Reset() {
    dataLayer.push(function () {
      this.reset();
    });
    // set values of defaults, again
    GtmTracking.SetValues(GtmTracking.Values);
  }

  // ...
}
```

First time setting values now becomes like this:

```typescript
// first keep track of values
GtmTracking.Values = GtmTracking.MapUser({name: 'John Doe', id: '123', email: 'email@address.com'});
GtmTracking.Values = GtmTracking.MapProfile({language: 'en', country: 'jo'});
// then set data layer values
GtmTracking.SetValues(GtmTracking.Values);
```
## Sever platform

The last bit to add is to condition out `dataLayer` push events that run on the server, this is a strictly browser platform activity. We create a private method to do the pushing, and delegate all `dataLayer.push` events:

```typescript
// GTM service updated
export class GtmTracking {
  // ...

  private static Push(data: any) {
    // check if window exists
    if (window && window['dataLayer']) {
        dataLayer.push(data);
    }
  }

  // update all push events
  public static RegisterEvent(track: IGtmTrack, extra?: any): void {
    // ...
    this.Push(data)
  }

  public static SetValues(values: any): void {
    // ...
    this.Push(data);
  }
  public static Reset() {
    this.Push(function () {
      this.reset();
    });
    // ...
    }
}
```
## Takeaway

We managed to map our internal models to data models that can be translated and used by GTM. I remember only last year we had to do much more for GA, and third party. After this exercise, my faith in GTM has been restored. Though bets are, next year this needs an update!

Thank you for staying alert, God knows it was hard for me as well.

### RESOURCES

-   [Sentry Error Tracking in GTM](https://hume.dev/articles/sentry-error-tracking/)
-   [Sentry Documentation](https://docs.sentry.io/platforms/javascript/)
-   [Angular ErrorHandler](https://angular.io/api/core/ErrorHandler)
-   [StackBlitz project](https://stackblitz.com/edit/gtm-angular-state-local?file=src/app/core/gtm.ts)