David Vallejo - Web Analyst

Preventing duplicate transactions in Universal Analytics with Google Tag Manager

Web Analytics

One of the most common headaches while implementing the ecommerce tracking on a site is trying to match the tracked transactions by the shop backend to Google Analytics. As most tracking solutions are JavaScript based, there's a small chance of losing some of them and there's nothing we can do without playing with the measurement protocol and some server-side tracking.

Another problem that is usually present is having duplicated transactions. And this hopefully is something we can prevent with some code.

We can setup a tag to write a cookie each time a visitor views our "thank you" page, that is not a bad approach, but that way we won't be sure that the transaction has been really tracked on Google Analytics.

We're going to use the hitCallback feature available for Universal Analytics, to set the cookies just right after the data has been successfully sent to Google Analytics.

We'll need to set the hitCallback value in our Google Tag Manager Tag to a Custom JavaScript Variable. As I was pointed by Simo Ahava on Twitter , hitCallback is expecting a function, so we're going to return a function that does the following:

1. Grabs the current transactionId from the dataLayer
2. Checks for the "transactions" cookie
2.1. If if doesn't exists we'll create it with the current transactionId value
2.2. If the cookies already exists, we'll check the current values, and if the current transaction is not there, we'll add it.

To avoid having a new cookie for each transaction, we'll be using just one cookie with all the transactions joined by a pipe ( "|" ) symbol.

Ok, now every time that a transaction hit is sent to Google Analytics, the current transactionId will be added to our tracking cookie.

We'll need a 1st party variable too to grab the transacctions cookie this way:

You may noticed that the first 2 lines of the code is checking for the transactionId, this is because in this example we're using the Enhanced Ecommerce feature and populating the transaction info from the dataLayer, and we don't want to do anything for all the pageviews on the site but just for our thankyou page one. You may need to tune this for your needs.

Ok, let's move on. Now we'll need to add another customJS variable to check if the current transaction is already in the cookie, and we'll use this variable to create a blocking trigger for our tag.

I've named it as "Should I Track Transaction", (yeah, not the best name), but it helps to understand the trigger:

We only need to add this blocking rule to our pageview tag and we'll be finish.

Let's do a resume of the tracking flow:

  1. "Should I Track Transaction", will return "blockTransaction" if the current transactionId is present in our tracking Cookie
  2. "Block Transaction" Trigger will block the pageview tag firing if #1 is true.
  3. If the first 2 points are not met, the pageview tag will be fired.
  4. When the pageview tag is fired, the hitCallback function will be executed right after the transactions has been sent to Google Analytics Endpoint
  5. The hitCallback will execute the function returned by the variable "transactionCallback", which will be in charge of creating the cookie if is doesn't exist and adding the current transactionId to it.

I know that this will not be functional for some cases and there're a lot of different implementations, sending the transaction based on events, sending the transactions based on a macro value (enhanced ecommerce), but that's something you'll need to figure out as isn't there any stardard tracking solution. Hopefully you have learnt how hitcallbacks work in Google Tag Manager and you could get it working for your enviroment, if not drop a message in the post and I (or any other reader), will try to help you.

As could not be otherwise Sir Simo already did something similar for old ecommerce tracking some months ago.

transactionCallback Code

function()
{
  	// If isn't there a transaction ID, we don't need to do anything.
	if(!{{transactionId From DL}})
    	return;
  
	return function(){    
		var transactionId = {{transactionId From DL}};
		if({{transactions}}){
 			var trackedTransactions = {{transactions}}.split("|");
   			if(trackedTransactions.indexOf(transactionId)==-1){         
          		trackedTransactions.push(transactionId);
				var d = new Date();
    			d.setTime(d.getTime() + (180*24*60*60*1000));
    			var expires = "expires="+d.toUTCString();           
    			document.cookie = "transactions=" + trackedTransactions.join('|') + "; " + expires;
        	}
		}else{
      		var trackedTransactions = [];
      		trackedTransactions.push(transactionId);
  			var d = new Date();
    		d.setTime(d.getTime() + (180*24*60*60*1000));
    		var expires = "expires="+d.toUTCString();           
    		document.cookie = "transactions=" + trackedTransactions.join('|') + "; " + expires;
		}
	}
}

Should I track transaction Code

function()
{
	if(!{{transactionId From DL}})
       return;

    var transactionId = {{transactionId From DL}};	
	if({{transactions}}){
  		var trackedTransactions = {{transactions}}.split("|");
    	if(trackedTransactions.indexOf(transactionId)>-1)
        {
			return "blockTransaction";
        }
    }
}