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.