Preventing duplicate transactions in Universal Analytics with Google Tag Manager

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";
        }
    }
}

 

David Vallejo

Google Analytics Consultant and implementer. I have some experience with Google Tag Manager
Follow me: @thyng

Author: David Vallejo

Google Analytics Consultant and implementer. I have some experience with Google Tag Manager Follow me: @thyng

12 thoughts on “Preventing duplicate transactions in Universal Analytics with Google Tag Manager”

  1. Hey David,
    Thank you for the efforts taken to come up with a solution to avoid duplicate transaction.

    Frankly, I am not so good with JavaScript and used Tag Manager for the first time because your solution looked promising.
    I followed step by step given in the article and matched every value / code as per instructions and even compared screenshots to avoid any error (obviously changed GA Tracking ID 😛 )
    Still, I am not able to get it working.

    I have problem at two steps:

    1. In this screenshot: https://www.thyngster.com/wp-content/uploads/img_55c8367f5af28.png where Value of page is {{gaPagePath}}
    Is that a new variable, if yes, there is no code given for it. As an alternative, I used {{Page Path}} which was available variable.

    2. In this screenshot: https://www.thyngster.com/wp-content/uploads/img_55c8359f22c4e.png where Data Variable name mentioned should be same for all? Your statement “You may need to tune this for your needs.” was confusing. I use WooCommerce platform so how do I need to tune it?

    I’ve spent hours figuring out but in vain.

    Could you please shed some light and help me out on this?

    1. Hi Rohit

      Have you found a solution to this?

      I am having the same issue and can’t find anything specific to woocommerce.

  2. Hi,

    Interesting article, just a quick question though – if you track transactions via an event, will the fix still work? My transactions are sent via an event, not pageview. I assume the principle will still apply, just a case of changing the tag type above from pageview to event.

    Is that the case?

    Many thanks.

  3. n my Google Analytic E-commerce overview total transactions count shows wrong, actually here on the date of 04-Oct-2016 total transactions are 2 only but there count shows 4.

  4. Just in case anyone else is having the same problem — my site has different pages for transaction confirmations, which meant Chrome was creating a different cookie for each transaction confirmation page, instead of appending each transactionId to the same cookie. Adding `”path=/;”` in each instance of setting the cookie in the transactionCallbackCode fixed it for me 🙂

    Also, @Rohit and @Marko, the {{gaPagePath}} in the fields to set area looks to me to be a custom way that David wants to pass his page path to GA. This tutorial should work just fine without it.

    And about getting your transaction ID into the DataLayer — you have to put enhanced ecommerce code on your conversion pages that pushes this value to the DataLayer in order for you to capture it.

    WooCommerce may have a plugin, but I’m not familiar with that community enough to know.

    To check what values are available in the dataLayer, open up JavaScript console while on a transaction confirmation page and type `dataLayer`. This will show you all the objects available in your dataLayer.

    If there is no `impressions` or `purchase` type object, then your transaction values aren’t currently being pushed to your dataLayer, and you will need to address that before you can use this tutorial. If the object does exist, you can use DOM notation to access it with a dataLayer value.

    1. Thanks for your reply @Loryn. You are correct, there is a plugin in WordPress. I’ve used it for the sake of simplicity and it works seamlessly. If you run WordPress and you are not 100% comfortable with javascript, the DataLayer and the DOM , I highly recommend it. The plugin is DuracellTomi from Geiger Tamas. https://en-au.wordpress.org/plugins/duracelltomi-google-tag-manager/
      If you are starting fresh and don’t have any tracking in place, it’s really easy to install . A little more thought is required when trying to replace existing Tag Manager/Analytics setups.
      Hope this helps

  5. Excellent post, thank you.

    I was wondering 2 things:
    1) Should we address the fact that we are appending new IDs to the cookie from here to infinity? Can we run into performance or other issues if there are hundreds (or thousands?) of IDs?

    2) What do you think using referring information as back up for occasional cookie deletion: fire tag only if previous page was /checkout (or similar)?

Leave a Reply

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