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

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

	if(!{{transactionId From DL}})

    var transactionId = {{transactionId From DL}};	
  		var trackedTransactions = {{transactions}}.split("|");
			return "blockTransaction";


  • 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: 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: 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?

    • Hi Rohit

      Have you found a solution to this?

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

  • 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.

  • 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.

  • 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.

    • 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.
      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

  • 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)?

  • Hi David, thanks so much for the help!!
    I just have one question:

    Is there a way to update all transaction values, instead of blocking them? In my case, users can modify they “purchase” cuz it’s actually a booking, so revenue may change. For example:

    – A user book with us and total revenue is $40 USD.
    – The next day, this user modify their booking and now the revenue is $30 USD.
    – In Analytics i’ll see 2 transactions from the same transaction ID and with a revenue of $70 USD, although in reality it’s $30 USD.

    Any help would be appreciated!

    Regards 🙂

    • I realize this is two years after your question, but:

      You can send the difference. So in your example, you would send -10.00 which would adjust the revenue in the GA report. Or look into refund in EE.

  • Hey everybody,

    I have enhanced ecommerce implemented on our side. On our conversion page a Transaction Tag is triggered by a custom event. In this guide we are talking about adjusting a tag which is triggered on all pages, are you adjusting the general Google Analytics Page View tag or the Transaction tag here?

    Thank you in advance and best regards

  • This was very helpful. Been having dupe transaction problems for a while, mostly with mobile devices.

    I found it easier to test this by setting the path to “/” in the 2 lines of code that set the cookie:

    document.cookie = “transactions=” + trackedTransactions.join(‘|’) + “; path=/;” + expires;

    document.cookie = “transactions=” + trackedTransactions.join(‘|’) + “; path=/;” + expires;

    That way I can have a fake receipt page on a different path that I reload to make sure the transaction isnt’ being sent again.

  • Thanks – this was incredibly useful for me.

    I’m still unsure as to what causes the issue – on the platform I am currently experiencing the duplication issue on (Magento), the confirmation page can only be viewed – and thus the GA code called – once, then it cannot be re-navigated to.

    There is an overwhelming pattern in mobile devices on transactions impacted, perhaps mobile browser cache is the culprit.

  • Hi David,
    Thanks for the post.This helps me a lot.
    But I have little confusion on 1st party cookie.If customer bookmark the receipt page and if he access again in future, the receipt page will access but the purchase event should not push data to data layer.To do this we should compare all the transaction id’s in cookie with present receipt transaction id.But I have thousand’s of transactions placed earlier and it takes lot of time to fetch all the transaction id’s.Please help how can I achieve this without using 1st party cookie.
    Thanks in advance.

    • the transactions ids are saved per user. on their local browsers, do you have users with thousand transactions, how many of them are likely going to go back?.

      In any case, from the site perspective, I think when your site shows a confirmation page you’re pushing your transaction data to the dataLayer, just ask your developers to don’t push anything if the transaction timestamp is older than 5 minutes. That should be enough

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.