Prepare Google Analytics for ITP 2.1 +

Developments in intelligent tracking prevention (ITP 2.1 & ITP 2.2) from browsers, like Safari and Firefox, have made cookies a less reliable way of storing persistent information. While these updates are aimed at limiting cross-site tracking through cookies, they also impact Google Analytics ability to identify returning visitors.

This is because ITP has impact on ALL cookies placed using javascripts’s document.cookie function. Google Analytics uses a javascript library and this function to place cookies and remember if a user visited your website before.

If you want to know about ITP and what reasons behind the implementation, I suggest following Webkit’s Blog.

I’m not a die-hard, professional developer, so please, always control the implementations in this blog yourself! That being said, let’s get started!

ITP workarounds for Google Analytics

There are actually multiple ways to workaround ITP with Google Analytics and make GA less reliant on client-side cookies. The most common way is to make the browser’s localStorage the leading storage for Google Analytics’ user identification.

Simo Ahava actually wrote a great article in depth about this workaround. Based on his blog, I created a my own variant, which works similarly, but differs in implementation and the way cross-domain tracking is handled. Both work, but here’s my approach using Google Tag Manager.

Instruct GA to use localStorage

Google Analytics uses the clientId variable to identify users and connect tracked interactions to a user. By default, this variable is stored in the _ga cookie. So we’re going to instruct GA to use a user identifier stored in the browser’s localStorage.

For this we need the following:

  • A customTask function variable to store the clientId in the localStorage
  • A GTM variable to get clientId from the localStorage
  • A 1st party cookie GTM variable that grabs the _ga cookie value

This is the script we use in the custom javascript clientId GTM variable. I called this variable: js.task.itp

function(){
  var ga_cookie = {{cookie._ga}};
  //localStorage is leading
  if(window.localStorage && window.localStorage.getItem("ga_cid")!==null){
    var ga_cid = window.localStorage.getItem("ga_cid");
	return ga_cid;
  }
  //without localStorage, revert back to using the cookie's value
  else if(ga_cookie!==undefined){
    return ga_cookie.substr(6,ga_cookie.length);
  }
}

This is how the customTask custom javascript GTM variable should be constructed. I named this variable: js.clientId.itp

function(){
    return function(model){
      	//setup necessary variables
        var ga_cid = model.get('clientId');
        var ga_cid_name = "ga_cid";
        expire_date.setDate(expire_date.getDate() + 365);
        //check if localStorage is available
        if(window.localStorage){
            //control if the clientId is already set in the localStorage
            var ga_cid_loc = window.localStorage.getItem(ga_cid_name);
            if(ga_cid_loc===null){
                //if ga url param is passed, make it the leading client id
                window.localStorage.setItem(ga_cid_name, ga_cid);  
            }
            //clientId is already set in localStorage.. do nothing.
        }
        else{
            //do nothing, revert to default behaviour and use cookies
        }
    }
}

These GTM variables need to be passed in the Google Analytics templates as two “Fields to set”. The customTask function variable should be passed in the customTask field, the clientId variable in the clientId field.

These scripts instruct GA to always check the localStorage first for the clientId. If that storage is not available, then the GA tracker resorts back to default behavior and uses the _ga cookie.

Test localStorage implementation

To test if everything works, just delete all cookies & storage and visit your website. If the GA tag is triggered, the clientId should be stored in the localStorage variable with the name of your choosing. The _ga cookie should contain the same numeric id after the first 6 characters.

Try deleting the _ga cookie and after that triggering a GA tag with the scripts set in the correct fields. The GA tag will control the localStorage, detect the clientId and use that for user identification. The GA tag will also restore the _ga cookie with the same clientId as was set in the localStorage. This process works the other way around as well if you delete the clientId in the localStorage.

This implementation will work completely fine if your website doesn’t make use of different subdomains or domains. If you want to localStorage setup across multiple subdomains or domains, we need to make some adjustments…

Cross-domain tracking with link decorations

localStorage has one important disadvantage compared to cookie storage. The information in the browser’s localStorage is only available for requests coming from the exact hostname in which the variables are set. So a localStorage variable set on www.markaay.com, will not be available on blog.markaay.com.

To overcome this limitation, we can pass the clientId through to other domains by appending url parameter to the links going from www.markaay.com to blog.markaay.com and vice versa. ITP 2.2 refers to this practise as using cross-site “link decorations”.

We have to change our code a little and have to instruct our customTask to check our availability url parameter first. To keep our naming conventions clean, we also use ga_cid as the parameter key for the clientId to pass. If the parameter is there, its value will be the leading clientId input for the GA tags. The customTask will also make the localStorage and _ga clientId copy the url parameter’s value to persists it for further tracking of interactions.

Configuring cross-domain tracking

The customTask custom javascript variable needs to be adjusted a little. First, we need to create two additional GTM variables:

  • A url query based GTM variable that grabs the ga_cid parameters value.
  • A constant variable containing the comma-separated domains which you would like to include for cross-domain tracking. E.g. “www.markaay.com,www.markaay.nl”

I named these variables in GTM url.query.ga_cid and constant.crossDomains respectively. So this is how they appear in the script below.

function(){
    return function(model){
      	//setup necessary variables
        var ga_cid = model.get('clientId');
        var expire_date = new Date();
        var ga_cid_name = "ga_cid";
        expire_date.setDate(expire_date.getDate() + 365);
        //check if localStorage is available
        if(window.localStorage){
            //control if the clientId is already set in the localStorage
            var ga_cid_loc = window.localStorage.getItem(ga_cid_name);
            if(ga_cid_loc===null){
                //if ga url param is passed, make it the leading client id
                var ga_param = {{url.query.ga_cid}}; 
                if(ga_param!==undefined){
                  window.localStorage.setItem(ga_cid_name, ga_param);
                }
                else{
                  window.localStorage.setItem(ga_cid_name, ga_cid);  
                }
            }
      		//clientId is already set in localStorage.. do nothing.
        }
        else{
            //do nothing, revert to default behaviour and use cookies
        }
      	
      	//Cross-domain tracking extension. Snippet below appends url_param to the domains to which the client id must be passed
  	//Only necessary on pageview events, therefore filtered on hitType
        if(model.get('hitType')==="pageview"){
          //Retrieve an array of all links on in the DOM of the page
          var all_links = document.getElementsByTagName("a");
          //Set up variables in order to control whether a link is eligible for cross-domain tracking
          var hn = window.location.hostname;
          var query_name = ga_cid_name;
          var crossdomains = {{constant.crossDomains}}.replace(".","\.").replace(",","|");
          var crossregex = new RegExp(crossdomains);
          var client_pass = {{js.clientId.itp}};
          //loop over all links
          for(i=0;i<all_links.length;i++){
              //check if the link in question matches a domain in our constant.crossDomains variable
              if(all_links[i].hostname !== hn && all_links[i].hostname.match(crossregex) && client_pass!==undefined){
                  //Check if the cross-domain link in question already has appended url parameters
                  //If the link has parameters, we have to append the url param with a linking character (? or &)
                  if(all_links[i].search === ""){
                      all_links[i].setAttribute("href", all_links[i].href + "?" + query_name + "=" + client_pass.toString());
                  }
                  else{
                      all_links[i].setAttribute("href", all_links[i].href + "&" + query_name + "=" + client_pass.toString());
                  }
              }
          }
       }
    }
}

The top part of the script handles the priority logic, the bottom part handles the appending of the clientId to the links in the DOM which you want to include for cross-domain tracking.

Next up, the javascript variable for the clientId needs to be adjusted as well.

function(){
  var ga_param = {{url.query.ga_cid}};
  var ga_cookie = {{cookie._ga}};
  var ga_storage = "ga_cid";
  //If the url parameter is available return that value
  if(ga_param!==undefined){
    return ga_param;
  }
  //without parameter, the localStorage is leading
  else if(window.localStorage && window.localStorage.getItem(ga_storage)!==null){
    var ga_cid = window.localStorage.getItem(ga_storage);
	return ga_cid;
  }
  //without localStorage, revert back to using the cookie's value
  else if(ga_cookie!==undefined){
    return ga_cookie.substr(6,ga_cookie.length);
  }
}

In order to make this work. The two scripts and fields (customTask & clientId) need to be set on both domains where Google Analytics tags are triggered. And please track the different domains in the same GA property, otherwise this cross-domain tracking workaround doesn’t really help :]

How does it work in practise?

If all things are set up correctly the customTask will automatically append the clientId-variable’s value in the chosen url-parameter to the links in the DOM with direct a user to a domain which you included in your crossDomains variable.

When the user clicks these links they are directed to that link’s page. Assuming that page is tracked with a GA pageview tag in GTM with the correct customTask and clientId scripts, this GA tag will be instructed to use this as the leading clientId value for further tracking of interactions.

Final thoughts

  • Safari’s ITP is here to stay and future developments could also impact other kinds of the browser’s storage capabilities.
  • localStorage therefore could also be impacted in the ITP updates.
  • There are other ways of working around the document.cookie ITP limitations, the solution in this blog is by no means the definitive one.
  • Privacy concerns will only further drive browsers to limit tools like Google Analytics in their tracking capabilities. It’s up to us to keep finding ways to optimize implementations, while also respecting a browser-user’s privacy settings and together with regulatory guidelines like GDPR.
  • If you’re a web or digital analytics consultant, keep an eye out for updates coming from WebKit or Mozilla’s browser blogs. These 2 players are on the forefront of these developments, and sooner or later the rest will follow.

Leave a Reply

Your email address will not be published. Required fields are marked *