How to implement Tealium Tracking using an Angular Library

How to implement Tealium Tracking using an Angular Library

Learn how to build an Angular Library that handles all your Tealium-related tasks and requests. This will cover some core concepts of the Angular framework: libraries, services, dependency injection and custom directives.

The article is structured as follows:

Tealium
Data Layer
Angular Library
Initialize the Angular Library
Tracking Service with injected configuration
Usage of the library
Form Tracking Directive
Conclusion

Tealium

Tealium is a Tag Management System (TMS) that controls the deployment of all tracking tags through a web interface.

This not only allows the easy integration, deletion or adaption of tracking tags without code changes but also the possibility to manage user consent through a consent manager.

Tealium lets you group your tracking tags into categories and loads the respective category only if the user accepted it in the consent manager prompt.

For your code, this means that you no longer send different information to different tracking providers, but all the information is only sent to Tealium by adding them to the so called Data Layer.

Data Layer

The Data Layer is nothing more than a JavaScript object that contains all the relevant information and should be present on every page of your website where you want to track information. This information can then be used for loading rule conditions and as content parameters for tracking requests.

Tealium recommends a flat data object that has a depth of one layer in order to quickly access the values. Additionally, it is recommended to only use strings and arrays of strings because you are not doing any math with the Data Layer values, you just pass them through.

It makes sense to establish a global naming convention and divide the website information into categories that are mirrored in their names. For example, there could be properties containing information about the categories website, page, user, event, form, product, request or marketing campaigns.

In Tealium the datalayer object is called “utag_data” and could be initialized like this (properties and property names are examples):

utag_data = {
  site_country: "DE",
  site_env_type: "prod",
  site_language: "de",
  page_name: "/ueber-uns/standorte/muenchen.html",
  page_type: "location",
  page_level_1: "Über uns",
  page_level_2: "Standorte",
  page_level_3: "München",
  user_login_status: "logged out",
}

Tealium also automatically enriches the Data Layer with some values: It stores all cookie values, all meta information, all query parameters, DOM information and general Tealium account settings.

Angular Library

In your Angular Application you will mainly do three things to integrate and work with Tealium:

  1. Load the Tealium Script and initialize the Data Layer Object on every page (this automatically triggers a Tealium PageView Event)
  2. Trigger the Consent Manager Prompt to let the user accept or decline cookie categories
  3. Enrich the Data Layer with Event Tracking calls on different user actions, e.g.:
    • Button/Link Click
    • Form Input Fields (Values, Errors,…)
    • Requests (Successful Conversions,…)

An Angular Library that you can use through all your Angular Applications and Angular Elements can help you achieve these tasks and of course, you can adapt this library and for example add more tracking cases that are perfectly tailored to your needs.

A library cannot run on its own, it must be imported and used in an application.

Initialize the Angular Library

Inside your workspace run:

ng generate library tealium-tracking

Using this command the workspace configuration file, angular.json, is updated with a project of type library.

Additionally, it creates the folder projects/tealium-tracking, which contains an NgModule with a component and a service. As our tealium-tracking library is just a functional library you can delete the component – we won’t need it.

To make library code reusable you must define a public API for it. This „user layer“ defines what is available to consumers of your library and is maintained in the public-api.ts file in your library folder. Anything exported from this file is made public when your library is imported into an application. We will adapt this file later in this article and export everything that should be available from outside.

You have two possibilities to use your library inside your applications

  1. publish your library as npm package with
    ng build tealium-tracking
    cd dist/tealium-tracking
    npm publish

    Then install the npm package in your application and import the TealiumTrackingModule in any NgModule from your node_modules.
    If you want to develop or adapt the library you can use npm link to test it in your application without reinstalling it on every change.
  2. Import it directly from the workspace
    Just run
    ng build tealium-tracking
    and import the TealiumTrackingModule in any NgModule.
    Incremental builds can run as a background process in your development environment, just add the --watch flag to the build command to automatically rebuild the library on every change:
    ng build my-lib --watch

Tracking Service with injected configuration

The initialization and handling of tracking calls should be done by a service. A service is typically a class with a specific, not view-related purpose, what is given here. By defining such methods in an injectable service class, you make those available to any component.

If you want to use your tealium-tracking library in different modules, apps or use cases with different configurations, you have to provide the configuration to your TealiumTrackingModule from outside. Then the module would look something like this:

tealium-tracking.module.ts

import { ModuleWithProviders, NgModule } from '@angular/core';
import { TealiumTrackingService} from './services/tealium-tracking.service';
import { TealiumConfigToken } from './services/tealium-config.service';
import { TealiumConfig } from './models/tealium-config.model';

export const defaultConfig = (): TealiumConfig => {
  return {
    account: 'accName',
    environment: 'prod',
    profile: 'main',
    dataLayer: {
      page_name: window.location.pathname,
      page_level_0: 'Home',
      site_country: 'DE',
      site_language: 'de',
    },
  };
};

@NgModule({
  providers: [
    TealiumTrackingService,
    {
      provide: TealiumConfigToken,
      useValue: defaultConfig,
    },
  ],
})
export class TealiumTrackingModule {
  static forRoot(config: Function): ModuleWithProviders<TealiumTrackingModule> {
    return {
      ngModule: TealiumTrackingModule,
      providers: [
        TealiumTrackingService,
        {
          provide: TealiumConfigToken,
          useValue: config,
        },
      ],
    };
  }
}

And you can import it in any NgModule and provide your custom configuration like this:

imports: [
   TealiumTrackingModule.forRoot(myCustomConfigObject)
]

The TealiumConfigToken is an InjectionToken used to import the configuration object provided from outside. It looks like this:

tealium-config.service.ts:

import { InjectionToken } from '@angular/core';

export const TealiumConfigToken = new InjectionToken<Function>('TealiumConfig');

If no configuration is injected, the module will take your declared defaultConfig. It makes sense to declare an interface for the config object. You can use it in your application where you import the library and make sure that your configuration has the correct structure.
In my example the configuration has the following structure, but you can use different properties and in particular a completely different Data Layer:

models/tealium-config.model.ts

import {DataLayer} from './data-layer.model';

export interface TealiumConfig {
  account: string;
  profile: string;
  environment: string;
  dataLayer: DataLayer;
}

models/data-layer.model.ts

export interface DataLayer {
  site_country: string;
  site_language: string;
  page_name: string;
  page_level_0?: string;
  page_level_1?: string;
  page_level_2?: string;
}

Now we can implement the actual service. The configuration is injected in the constructor.
After that we need to implement the methods for the base functionality of the service.

It will initialize by creating the Data Layer object and then request the Tealium script source. On load, the service will trigger the Consent Manager Prompt that you can configure in Tealium. Attention: the utag.gdpr object, which is necessary for the Consent Manager Prompt, will only be available if it is activated in the Tealium Web Interface under Client-side Tools > Consent Management:

Additionally, we have publicly accessible methods to track a view event (triggered at page changes) and a link event (triggered at user events on a page).

The full service looks like this:

services/tealium-tracking.service.ts

import { Inject, Injectable } from '@angular/core';
import { TealiumConfigToken} from './tealium-config.service';
import { TealiumConfig } from '../models/tealium-config.model';
import { TealiumEvent } from '../models/tealium-event.model';

@Injectable()
export class TealiumTrackingService {
  private readonly config: TealiumConfig;
  private isInitialized: boolean;

  constructor(@Inject(TealiumConfigToken) private configFunc: Function) {
    this.config = this.configFunc();
    this.initialize();
  }

  public initialize(): void {
    if (!this.isInitialized) {
      const hasUtagBeenLoaded = new Promise((resolve, reject) => {
        try {
          this.setDataLayer();
          this.renderScript(resolve);
        } catch (e) {
          reject(e);
          console.error('Error in TealiumTrackingService: ' + e);
        }
      });
      hasUtagBeenLoaded.then(this.triggerConsentPrompt);
    }
    this.isInitialized = true;
  }

  private setDataLayer(): void {
      (<any>window).utag_data = this.config.dataLayer;
  }

  private renderScript(callback: Function): void {
    let script;
    script = document.createElement('script');
    script.language = 'javascript';
    script.type = 'text/javascript';
    script.async = 1;
    script.charset = 'utf-8';
    script.src = `https://tags.tiqcdn.com/utag/${this.config.account}/${this.config.profile}/${this.config.environment}/utag.js`;
    script.addEventListener('load', callback);
    script.addEventListener('error', () => {
      console.error('Could not load utag. Please check your configuration!');
    });
    document.getElementsByTagName('head')[0].appendChild(script);
  }

  private triggerConsentPrompt(): void {
    if (!this.isConsentGiven()) {
      (<any>window).utag?.gdpr ?
        (<any>window).utag?.gdpr.showConsentPreferences() :
        console.error('utag.gdpr not defined! Check your settings in Tealium.');
    }
  }

  private isConsentGiven(): boolean {
    return (<any>window).utag?.gdpr?.getCookieValues?.()?.consent === 'true';
  }

  public view(data: { page_name: string }, callback?: Function, tagIds?: number[]): void {
    this.track('view', data, callback, tagIds);
  }

  public link(data: TealiumEvent, callback?: Function, tagIds?: number[]): void {
    this.track('link', data, callback, tagIds);
  }

  private track(tealium_event: string, data: any, callback?: Function, tagIds?: number[]): void {
    try {
      (<any>window).utag.track(tealium_event, data, callback, tagIds);
    } catch (e) {
      console.error('utag object does not exist on this page');
    }
  }

The interface TealiumEvent used as parameter in the link method is also an example structure – you can decide for your website/application what an event call would look like and what information you want to add to the Data Layer:

models/tealium-event.model.ts

export enum TEALIUM_EVENT_ACTIONS {
  BUTTON_CLICK = 'button_click',
  LINK_CLICK = 'link_click',
  ICON_CLICK = 'icon_click'
}
export interface TealiumEvent {
  event_action: TEALIUM_EVENT_ACTIONS;
  event_title: string;
  event_linktype: 'internal' | 'external';
  event_source: string;
  event_target: string;
}

Usage of the library

To make your service and models available from the application you have to export them in the public-api.ts under tealium-tracking/src/:

public-api.ts

export * from './services/tealium-tracking.service';
export * from './models/tealium-config.model';
export * from './models/data-layer.model';
export * from './tealium-tracking.module';

After building (and optional publishing) the library, you can import the module in any NgModule of your application:

import { TealiumTrackingModule } from 'tealium-tracking';

@NgModule({
  imports: [
	TealiumTrackingModule.forRoot({
  account: 'myAccount',
  profile: 'profile-de',
  environment: 'prod',
  dataLayer: {
    site_country: 'de',
    site_language: 'DE',
    page_name: 'Startseite'
  },
  }),
  …
 ]
})

Now you can call the public methods of the TealiumTrackingService:

  • initialize: you can call this manually if you don’t want to use any other tracking in the module, otherwise the initialize method is called automatically at the injection of the service
  • link: call this for event trackings, like button clicks, conversions and so on
  • view: call this at page changes. When the URL changes Tealium sends this event automatically (if you don’t disable it) but in an SPA you may want to trigger some page view events manually with this method

Here is a usage example for each of these methods:

import { TealiumTrackingService, TEALIUM_EVENT_ACTIONS } from 'tealium-tracking';

export class MortgageRequestComponent {
	constructor(private tealiumTrackingService: TealiumTrackingService) {}

	private initializeTrackingService(): void {
		this.tealiumTrackingService.initialize();
	}

	private trackThanksPage(): void {
		this.tealiumTrackingService.view({
  			page_name: window.location.pathname + 'thanks/',
        	});
	}

	private trackButtonClick(): void {
		this.tealiumTrackingService.link({
  			form_step: TEALIUM_EVENT_ACTIONS.BUTTON_CLICK,
  			event_title: 'Go to homepage',
  			event_linktype: 'internal',
  			event_source: '/about',
  			event_target: '/home'
		});
	}
}

Form Tracking Directive

You can also equip your library with some more special functionalities. We have a use case where we want to track every field value in a form, when it’s blurred. This can be done with a directive.

With the @Directive decorator you can attach custom behavior to DOM elements – in our case to the form element.

By using @ContentChildren you can select every input in the respective ElementRef and add a blur EventListener to it. This is how a simple form field tracking directive could look like:

directives/tealium-track-form/tealium-track-form.directive.ts

import {AfterContentInit, ContentChildren, Directive, ElementRef, OnDestroy, OnInit, QueryList,} from '@angular/core';
import { ControlContainer, FormGroup } from '@angular/forms';
import { TealiumTrackingService } from '../../services/tealium-tracking.service';

@Directive({
  selector: '[libTealiumTrackForm]',
})
export class TealiumTrackFormDirective implements OnInit, AfterContentInit, OnDestroy {
  @ContentChildren('input', { read: ElementRef, descendants: true }) private controls: QueryList<ElementRef>;

  private formGroup: FormGroup;
  private prevFormValues = {};

  constructor(
    private elementRef: ElementRef,
    private trackingService: TealiumTrackingService,
    private controlContainer: ControlContainer
  ) {}

  ngOnInit(): void {
    this.formGroup = <FormGroup>this.controlContainer.control;
  }

  ngAfterContentInit(): void {
    this.controls.forEach((control: ElementRef) => {
      const controlElement: HTMLElement = control.nativeElement;
      controlElement?.addEventListener('blur', this.triggerFormTracking.bind(this));
    });
  }

  ngOnDestroy(): void {
    this.removeEventListeners(this.controls);
  }

  private removeEventListeners(elements): void {
    elements.forEach((control: ElementRef) => {
      const controlElement: HTMLElement = control.nativeElement;
      controlElement?.removeEventListener('blur', this.triggerFormTracking.bind(this));
    });
  }

  private triggerFormTracking(event: any): void {
    const controlId = event.target.id;
    if (controlId) {
        const formControl = this.formGroup.get(controlId);
        const controlValue = formControl.value.toString();
        if (this.prevFormValues[controlId] !== controlValue) {
          this.trackingService.link({
            form_field: controlId,
            form_field_value: controlValue,
          });
          this.prevFormValues[controlId] = controlValue;
        }
    }
  }
}

When calling our TealiumTrackingService link method with this data object you will have to adapt the parameter type to also accept form field values:

models/tealium-form-event.model.ts

export interface TealiumFormEvent {
  form_field: string;
  form_field_value: string;
}

tealium-tracking.service.ts

public link(data: TealiumEvent | TealiumFormEvent, callback?: Function, tagIds?: number[]): void {
  this.track('link', data, callback, tagIds);
}

And of course you have to declare and export the directive in your TealiumTrackingModule:

tealium-tracking.module.ts

@NgModule({
  declarations: [TealiumTrackFormDirective],
  exports: [TealiumTrackFormDirective],
…

Then you can use it like this and every value of the form will be added to the Data Layer on blur.

<form [formGroup]="testForm" libTealiumTrackForm>
  <input #input id="myInput" type="text" formControlName="myInput" />
  <input #input id="email" type="email" formControlName="email" />
</form>

Conclusion

I hope this article provides you with a basic structure of a Tealium Tracking library and an understanding of how to employ the Angular concepts of a library, a service and a directive. Maybe it also inspired you to add more features or adapt it to fit your requirements.

If you have any issues or questions please feel free to add a comment below!

Schreibe einen Kommentar