Previously, I created a service with simple data model 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:
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:
// 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).
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.
// 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:
@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:
// 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:
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
// update mapper to accept label
public static MapGroup(group: EnumGtmGroup, label?: string) {
return {
group, label
}
}
In directive click handler, and input model:
// 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
<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:
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:
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:
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 by GTM:
// 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:
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.
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.
Top comments (0)