Skip to content

The Definitive Approach for preventing duplicate transactions on Google Analytics – Using a Universal CustomTask

David Vallejo

It’s been a long time since I wrote my post about how to prevent duplicate transactions on Google Analytics. At that point, the customTask wasn’t a thing on the Google Analytics JS library, and the approach consisted of writing a cookie on each transaction and then work with some blocking triggers.

It’s a working solution for sure, but based on all the feedback I had over the years, it was not easy to understand for people. Things got worse even with the Enhanced Ecommerce since there’s no specific hit type to block ( remember that on EEC, any hit is used as a transport for the Ecommerce data ).

That’s why I’m releasing a completely new approach to prevent duplicate transactions on Google Analytics. It’s based on the customTask functionality and it will work out of the box independently on how you have set up your Enhanced Ecommerce Tracking, sound good yes?

If you wonder how are we going to achieve this, take a look at the following flow chart

Basically, we’ll check the current hit payload to find out if it has any transaction-related data, and, only, in that case, we’ll be removed the e-commerce related data from the hit, If that transaction has been already tracked on the current browser ( we’ll be using a cookie to keep track of recorded transactions, just as we used to do in our old solution )

To have this working the only thing we need to do it to create a new Variable in Google Tag Manager with the following for our “duplicate transactions blocking customTask” .

*Note that I tried t add as many comments as I could in the customTask code, so please take some time to understand how it works! 🙂

function() {
  return function(customTaskModel) {
    var originalSendHitTask = customTaskModel.get('sendHitTask');
    // Helper Function to grab the rootDomain
    // Will help on seeting the cookie to the highest domain level
    var getRootDomain = function() {
      var domain =;
      var rootDomain = null;
      if (domain.substring(0, 4) == "www.") {
        domain = domain.substring(4, domain.length);
      var domParts = domain.split('.');
      for (var i = 1; i <= domParts.length; i++) {
        document.cookie = "testcookie=1; path=/; domain=" + domParts.slice(i * -1).join('.');
        if (document.cookie.indexOf("testcookie") != -1) {
          var rootDomain = domParts.slice(i * -1).join('.');
          document.cookie = "testcookie=1; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; domain=" + domParts.slice(i * -1).join('.');
      return rootDomain;
    // The custom Task
    customTaskModel.set('sendHitTask', function(model) {
      try {
        // Let's grab the hit payload
        var rawHitPayload = model.get('hitPayload');
        // We're converting the payload string into a key=>value object
        var hitPayload = (rawHitPayload).replace(/(^\?)/, '').split("&").map(function(n) {
          return n = n.split("="),
            this[n[0]] = n[1],

        // Let's check if this hit contains a transaction info
        // if the hit contains a &pa parameter and the value equals to "purchase" this hits contains a transaction info        
        if ((hitPayload && && === "purchase")) {
          // Let's grab our the previous transactions saved in our cookie ( if any )  
          var transactionIds = document.cookie.replace(/(?:(?:^|.*;\s*)__transaction_ids\s*\=\s*([^;]*).*$)|^.*$/, "$1") ? document.cookie.replace(/(?:(?:^|.*;\s*)__transaction_ids\s*\=\s*([^;]*).*$)|^.*$/, "$1").split('|') : [];
          // if the current transaction ID is already logged into our cookie, let's perform the magic
          if (transactionIds.length > 0 && transactionIds.indexOf(hitPayload.ti) > -1) {            
            // EEC hit keys magic regex. The following regex will match all the payload keys that are related to the ecommerce
            var eecRelatedKeys = /^(pa|ti|ta|tr|ts|tt|tcc|pr(\d+)[a-z]{2}((\d+)|))$/;
            // Now we'll loop through all the payload keys and we'll remove the ones that are related to the ecommerce
            for (var key in hitPayload) {
              if (key.match(eecRelatedKeys)) {
            // not let's update the payload into the hit model! :)
            model.set('hitPayload', Object.keys(hitPayload).map(function(key) {
                return key + '=' + hitPayload[key];
            }).join('&'), true);            
          } else {
            // IF the execution arrived to this point. It means that this is a NEW transaction
            // Then, we'll do nothing to the payload but instead we'll be adding the current transaction ID to our cookie
            transactionIds = [hitPayload.ti].concat(transactionIds);
            var _expireDate = new Date();
            // This cookie will expire in 2 years
            _expireDate.setMonth(_expireDate.getMonth() + 24);
            document.cookie = "__transaction_ids=" + transactionIds.join('|') + ";expires=" + _expireDate + ";domain=" + getRootDomain() + ";path=/";            

        // Send the hit
      } catch (err) {
        // In case the above fails, we want to send the hit in any case!!!

We’re done. From now all this customTask will be taking care of detecting transactions traveling on the hits, writing it to a cookie and removing the transaction data from the hit if needed!

  • You don’t need a blocking trigger
  • You don’t need an extra condition on your firing trigger
  • You don’t need a variable for checking for the value of the cookie
  • It’s doesn’t matter how you’ve set up your e-commerce tracking, the customTask will work despite your current approach ( sending it with the default pageview, or an event, or if using the dataLayer data or based on a variable that builds up the e-commerce data for GTM ). 
  • You won’t need to block your default pageview on the confirmation page to have the ecommerce working without duplicates.

It will just simply work!

Of course, you may want to block some other tags from firing since the customTask will write all the data into a cookie, it would be accessible for you to use it at your need. Just grab the “__transaction_ids” cookie value, and search for your already recorded transactions