GTM Tracking Service in Angular, Part II
Tags: angular, tagmanager, analytics
Published: 2022-04-25 13:12:01

Previously, I created a [service with simple data model](https://garage.sekrab.com/posts/gtm-tracking-service-in-angular) to adapt the data layer with different mappers. Today, we create a directive, and look into flushing the data layer.

## Directive

Most often than not, the analytics report is to measure clicks around an application. Let's create a directive for general clicks, that submits the basic data model:

```javascript
const dataLayerO = {
    event: eventname || 'gr_click',
    gr_track: {
        source: eventsource
    }
}
```

Those elements are good enough to create a report of event counts, the end result of how it should be used in HTML looks like this:

```html
// pass an object
<a [grGtm]="{ event: 'gr_click', source: 'home' }">Login</a>

// or setup data attributes
<a class="gr_catch" data-event="gr_click" data-source="Home">Projects</a>
```

Either way can be handled in GTM. With the former, we can update `dataLayer` programatically, and in the latter the work load is on GTM, but there are more information available per click. Let's create a directive that does both.

First let's make the **Enum values** available in templates. But instead of repeating on every template, we can create a** base component** for GTM purposes, and extend our component classes from it (this is a bit subjective, you are allowed to dislike it).

```typescript
import { EnumGtmEvent, EnumGtmSource } from './gtm';

export class GtmComponent {
  // these will be used throughout the templates
  enumGtmEvent = EnumGtmEvent;
  enumGtmSource = EnumGtmSource;
}
```

Then in the template and component.

```typescript

// in components that use the directive, extend from GtmComponent
@Component({
  template: `
  <a routerLink="/login" [grGtm]="{ source: enumGtmSource.Home }">Login</a>`
})
export class AppComponent extends GtmComponent {
  // if you have a constructor, you need to call super() inside of it
}
```

Now the directive:

```typescript
@Directive({
  selector: '[grGtm]'
})
export class GtmDirective implements AfterViewInit  {
  // we will decide later
  @Input() grGtm: any;

  constructor(private el: ElementRef){
  }

  ngAfterViewInit(): void {
    // these will help GTM experts in creating Click triggers
    this.el.nativeElement.setAttribute('data-source', this.grGtm.source || EnumGtmSource.Anywhere);
    this.el.nativeElement.setAttribute('data-event', this.grGtm.event || EnumGtmEvent.Click);
    // later we will decide what other attributes we can add
  }

  // also create a click handler
  @HostListener('click', ['$event.target'])
  onClick(target: HTMLElement): void {
    // on event click, gather information and register gtm event
    GtmTracking.RegisterEvent(
      {
        event: this.grGtm.event || EnumGtmEvent.Click,
        source: this.grGtm.source || EnumGtmSource.Anywhere,
      }
    );
  }
}
```

This is the richest way to create an event for GTM experts to create a **Click trigger** with `data-event` (for example), or a **Custom Event trigger**. I will not dig any deeper, but there are pros and cons for either way. Just a couple of enhancements to cover all scenarios, then you can choose one or both ways in your project.

### Enhancement: group events

We can group all of these directive clicks under one event, and add a new property to distinguish them. This allows the experts to **create one tag**, for all directive clicks, **without flooding GA4 with custom events**. The new property is `group`. In GTM Service:

```typescript
// few examples
export enum EnumGtmGroup {
  Login = 'login', // watch all login button clicks
  Upload = 'upload', // wach all upload button clicks
  Reveal = 'reveal' // watch all reveal button clicks
  Navigation = 'navigtion', // watch all navigation clicks
  General = 'general' // the default
}

export enum EnumGtmEvent {
  // ... add a general directive click event
  GroupClick = 'garage_group_click',
}
export class GtmTracking {
  // ...

  // add a mapper for group clicks
  public static MapGroup(group: EnumGtmGroup) {
    return {
      group
    }
  }
}
```

And in the directive:

```typescript
ngAfterViewInit(): void {
  // the event is always garage_group_click
  this.el.nativeElement.setAttribute('data-event', EnumGtmEvent.GroupClick);

  this.el.nativeElement.setAttribute('data-source', this.grGtm.source || EnumGtmSource.Anywhere);
  this.el.nativeElement.setAttribute('data-group', this.grGtm.group || EnumGtmGroup.General);
}

@HostListener('click', ['$event.target'])
onClick(target: HTMLElement): void {
  GtmTracking.RegisterEvent(
    {
      // this is now always group click
      event: EnumGtmEvent.GroupClick,
      source: this.grGtm.source || EnumGtmSource.Anywhere,
    },
    // map group
    GtmTracking.MapGroup(
      this.grGtm.group || EnumGtmGroup.General
    )
  );
}
```

In GTM, now we can create a new **variable** for `gr_track.group`. Then a **Custom Event trigger** for all events of type `garage_group_click`, and a **Group tag**, that passes source and group values. But we have no access to the text that distinguishes the click events. (Click text is only available with Click triggers.)

### Enhancement: add label

In the directive, we have access to the triggering element, so we can pass the label as well.

In GTM service

```typescript
// update mapper to accept label
public static MapGroup(group: EnumGtmGroup, label?: string) {
  return {
    group, label
  }
}
```

In directive click handler, and input model:

```typescript
// the model of the input now clearer:
@Input() grGtm: { source: EnumGtmSource; group: EnumGtmGroup };

@HostListener('click', ['$event.target'])
onClick(target: HTMLElement): void {
  GtmTracking.RegisterEvent(
    {
      event: EnumGtmEvent.GroupClick,
      source: this.grGtm.source || EnumGtmSource.Anywhere,
    },
    // pass group and label
    GtmTracking.MapGroup(
      this.grGtm.group || EnumGtmGroup.General,
      this.el.nativeElement.innerText
    )
  );
}
```

And the templates now look like this

```typescript
<a [grGtm]="{source: enumGtmSource.Homepage, group: enumGtmGroup.Login}">Login</a>

<a [grGrm]="{source: enumGtmSource.NavigationDesktop, group: enumGtmGroup.Navigation}">Projects</a>
```

And here is how the GTM tag looks like:

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

Add label as a custom dimention to GA4, and this, quite much starts to look like Universal Analytics.

> PS: Localizing? The label will be different for every language, if you, in anyway depend on it to create reports, take notice, and adapt.

## Data layer flushing

As more events are pushed to data layer, the variables do not automatically reset, they are available as long as nothing resets them. Consider this:

```typescript
setOne() {
    // reigster event and push datalayer
    GtmTracking.RegisterEvent({
        event: EnumGtmEvent.Filter,
        source: EnumGtmSource.ProjectsList,
    }, {
        filter: 'one'
    });
}
setTwo() {
    GtmTracking.RegisterEvent({
        event: EnumGtmEvent.Filter,
        source: EnumGtmSource.EmployeesList,
    });
}
```

The first function sets the data layer with filter "one", and the second call has no filter set. Here is how the `dataLayer` available to GA4 looks like after the second call:

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

In most cases, when you build a report on GA4, you **filter out for a specific event**, which usually has its parameters set together - because we are using internal mappers, like `MapProduct`. In other words, when you create a report for `view_item` event, you will not bother about the `group` property, rather the `value` property, which is set on every `view_item` event occurence, even if set to null. Thus, **this isn't a big issue**.

Nevertheless, we need a way to flush down the remote data layer, and we need to know when. The [reset functionality is provided](https://developers.google.com/tag-platform/tag-manager/web/datalayer#reset) by GTM:

```typescript
// in GTM service
public static Reset() {
  dataLayer.push(function () {
    this.reset();
  });
}
```

The other side effect, is the **dataLayer array is growing on the client side**. In most cases that is not an issue. Reseting the `dataLayer` variable is not allowed in GTM, and it breaks the `push` behavior. *(The GTM dataLayer.push is an overridden method.)*

Except... well, don't try this at home, but you can **splice out all elements except the first one**, which contains the `gtm.start` event. **Use this at your own risk**:

```typescript
public static Reset() {
  // not recommended but works
  // remove all elemnts except the first, mutating the original array
  dataLayer.splice(1);

  dataLayer.push(function () {
      this.reset();
  });
}
```

Flushing the data layer can be a manual process, **when in doubt, flush**. We also can auto flush on route changes. In the base `AppModule`, detect `NavigationEnd` events, and flush.

```typescript
export class AppRouteModule {
  constructor(router: Router) {
    router.events
      .pipe(filter((event) => event instanceof NavigationEnd))
      .subscribe({
        next: (event) => {
          // flush dataLayer
          GtmTracking.Reset();
        },
      });
  }
}
```

## Next

We have created a directive, and managed the resetting of our data layer. Next we will add a third party, and track errors.

Thanks for sticking around, did you smell anything bad? Let me know in the comments.

Find the directive on [StackBlitz](https://stackblitz.com/edit/gtm-angular-state-local?file=src/app/directives/gtm.ts).

#### RESOURCES

-   [Analytics mania: Track Single Page App with Google Analytics 4 and Google Tag Manager](https://www.analyticsmania.com/post/single-page-web-app-with-google-tag-manager/)
-   [GTM data layer reset](https://developers.google.com/tag-platform/tag-manager/web/datalayer#reset)