Upcoming LunaMetrics Seminars
Seattle, Nov 3-7 New York City, Nov 17-21 Washington DC, Dec 1-5 Los Angeles - Anaheim, Dec 8-12

Duplicate Transactions in Google Analytics – The Check and the Fix

duplicate-trans-big

By far the most common issue I’ve come across with ecommerce sites; duplicate transactions can inflate revenue and ecommerce metrics, altering your attribution reports and making you question your data integrity.

When talking about where to put the ecommerce tracking code, Google suggests the following for Universal Analytics:

… If successful, the server redirects the user to a “Thank You” or receipt page with transaction details and a receipt of the purchase. You can use the analytics.js library to send the ecommerce data from the “Thank You” page to Google Analytics.”

The missing step here is to ensure that either A) the user cannot access the page more than once or B) you have logic in place to make sure the transaction is only sent once. The biggest issues I’ve seen are when this receipt page is automatically emailed to the customer, with the ability for them to return as frequently as they please, each time sending a duplicate transaction.

Many people incorrectly assume this is something that is handled through Google Analytics processing, or that if it does occur, it is a bug. In reality, it is simply an implementation issue and one that is often overlooked.

Within a session, Google Analytics will filter out duplicate transactions provided they have the same information. But if a visitor comes back later that day, or two weeks later, and another transaction is sent, then these will show up in your reports.

Check Your Data

How do we check if this issue exists in your ecommerce data? There’s a fairly simple Custom Report you can create to check for this. I’ve created a template which, if you have the appropriate permissions, you can attempt to import via this link or you search the Solutions Gallery for “Duplicate Transactions.” If you cannot import the Custom Report for some reason, simply create the report yourself with this setup (jpg).

Adjust your date range to at least a month. If you have Transaction IDs that have multiple transactions, then you’re either A) sending in duplicate transactions or B) reusing transaction IDs, both of which should be corrected.

When Do Duplicate Transactions Occur?

The following scenarios are the most likely culprits for sending in the duplicate information:

  • Returning to the page via emailed link or bookmark
  • Refreshing the page
  • Navigating to a different page, and returning via back button
  • Page restoring from a closed browser session or on a smartphone

As you implement your solution, try checking each of these scenarios to make sure you’re completely covered!

Server-Side Is Better

There are several schools of thought around fixing duplicate transactions. My background is more on client-side implementations, specifically with Google Tag Manager. However, in general, if you have the resources and time to spend, I would recommend handling this issue server-side.

Without going into specifics, I would add in some sort of server-side logic to ensure that the ecommerce analytics code is only delivered once to the page. This could be using a database to record and check to see if the ecommerce info has already been sent.

It could also be some sort of server-side variable that is similarly checked. Another option I’ve seen is to redirect the user away from the receipt page after the ecommerce info has been sent to Google Analytics, then preventing the user from returning to that page.

Sometimes a page refresh doesn’t require fully reloading the page from the server, however, so make sure to test all of the above scenarios.

A Two-Pronged Approach

Not all of us have access to the server though, and sometimes we just need a solution. My tactic for dealing with duplicate transactions uses two different methods to attempt to determine if the transaction has already been sent.

  1. A browser cookie records the transaction ID
  2. A timestamp on the transaction serves as a backup

Cookies by themselves can filter out most duplicate transactions, but can be less than 100% effective due to privacy settings and user preferences. Someone can clear their cookies, browse in incognito mode, or pull up the same receipt on two different devices.

For that purpose, I also use a timestamp to help determine how old the transaction is. This timestamp should come from the page immediately before the receipt page, so very little time should pass. We can set this to be 15 or 30 minutes to be safe, just in case there’s some kind of validation check or third party system before they hit the receipt.

Here is the general user flow that we’ll follow. We’ll check to see if a cookie with this transaction ID exists. If it does, then we know it’s a repeat transaction, and we won’t send the ecommerce information to Google Analytics.

If there’s no cookie, we’ll check the timestamp. If there’s no timestamp, then we know it’s days or weeks old, from before the date we put our new process went into place, so we’ll label this as missing.

If there is a timestamp, how old is it? If it’s more than 30 minutes old, then we’ll assume it’s an old transaction and we’ll label this as expired.

Lastly, if there’s no cookie and it’s been less than 30 minutes, we’ll call this a new transaction. We’ll set a new cookie on this computer and then proceed with the checkout as normal.

Flow for Duplicate Checking

Stopping Duplicate Transactions via Google Tag Manager

The instructions I’m including use Google Tag Manager, but feel free to adapt for your site using straight JavaScript. Going into this, I’ll assume you have Google Tag Manager already installed set up on your site, that you have a receipt page, and that the data layer is properly formatted for ecommerce.

To get the full functionality of this solution, you will need access to update the site or have a developer you can call to help you get the timestamp into place.

We will need to create the following inside of Tag Manager:

MACRO – Data Layer Variable – “transactionId”

This macro will simply return the transaction ID from the correctly formatted data layer on the receipt page.

dupe-transactionId

MACRO – Data Layer Variable – “timeStamp”

This will be something we need to add to the site itself. We need to get the timestamp of the transaction, either from some server-side code, or by passing this value through the submit form. Either way, this piece does require you to update the site itself.

You can add a hidden field to your form and after the submit order button is pressed, use JavaScript to populate that field with new Date().getTime(). Then print this value onto the data layer on the receipt page. This needs to be calculated before the page loads and repeated onto the data layer, because this is supposed to be the time that the order was submitted, not the time that page was loaded.

Then we can use a simple macro to pull out this value.

dupe-timestamp

RULE – “Receipt Page – Transaction Present”

This rule checks to see if we’re on the receipt page and if there is a transaction present.

dupe-receiptpage

TAG – Custom HTML – “Duplicate Transaction Checking”

Here is where the magic happens. This Custom HTML will take care of all of the work, checking for cookies, setting cookies, and checking the timestamp. The result is then pushed to the data layer with a custom event.

<script type="text/javascript">
function checkCookies() {
    var cookievalue = "test";
    var cname = "";
    cname =  "TID_{{transactionId}}=";
    var ca = document.cookie.split(';');
    //Checks for existing Cookie
    for(var i=0; i<ca.length; i++){
      var ck = ca[i].trim().toString();
      if (ck.indexOf(cname)==0) {
          cookievalue = ck.substring(cname.length).toString();
          break;
      };
    };
    // Cookie is found, so repeat transaction
    if (cookievalue>0){
        dataLayer.push({'transactionType':'repeat'});
        dataLayer.push({'event':'transactionChecked'});
    } else {
        //Check time as a backup
        var validateDate = {{timeStamp}};
        var currentTime = new Date().getTime();
        if (validateDate > 0) {
            var minutes = Math.round((currentTime-validateDate)/1000/60)
            //Set expiration time for new cookie
            var d = new Date();
            d.setTime(d.getTime()+(365*24*60*60*1000));
            var expires = "expires="+d.toGMTString();
                if(minutes < 30) {
                    //less than 30 minutes, so good transaction!
                    document.cookie = "TID_{{transactionId}}=" + validateDate + "; " + expires;
                    dataLayer.push({'transactionType':'new'});
                    dataLayer.push({'event':'transactionChecked'});
                } else {
                    //older than 30 minutes, so expired transaction
                    document.cookie = "TID_{{transactionId}}=" + validateDate + "; " + expires;
                    dataLayer.push({'transactionType':'expired'});
                    dataLayer.push({'event':'transactionChecked'});
                };
        } else {
            //no timestamp found, so must be old
        	document.cookie = "TID_{{transactionId}}=" + currentTime + "; " + expires;
        	dataLayer.push({'transactionType':'missing'});
        	dataLayer.push({'event':'transactionChecked'});
        };
    };
};
checkCookies()
</script>

MACRO – Data Layer Variable – “transactionType”

Now that the Tag has checked if the transaction is a duplicate, we’ll use this macro to pull out the result.

dupe-transtype

RULE – “New Transactions Only”

We’ll create a rule that uses the event and transaction type that gets pushed from the Duplicate Transaction Tag.

dupe-newrule

TAG – Google Analytics – “GA Ecommerce Transaction”

Finally, put it all together with a Google Analytics transaction Tag, with the Firing Rule set to “New Transactions Only.”

dupe-ga-ecommerce

Parting Thoughts

That’s all there is to it! It’s a little complicated, but when you break it down step by step, it should make sense logically. I would recommend setting up a test property to send these ecommerce transactions to until you’re sure that this is working properly, then, make the switch at a time when there are few people using the site.

Questions/comments? Did you have duplicate transactions on your site?

Jon Meck

About Jon Meck

Jon Meck is our Technical Marketing Manager, promoting our services and trainings to the world. He has a jack-of-all-trades background, working for companies large and small in Social Media, Website Design and Maintenance, and Analytics. He is an Excel enthusiast, he loves efficiency, and he is strong proponent of the "Work Smarter, Not Harder" mantra. Outside of work, Jon enjoys late nights working on pet projects, running races, and spending time with his family.

http://www.lunametrics.com/blog/2014/07/07/duplicate-transactions/

2 Responses to “Duplicate Transactions in Google Analytics – The Check and the Fix”

Michael N. says:

Very great article and very interesting.

I am however facing a different issue with transactions GA report:

Number of transactions per transaction ID = 1 (no duplicate) – I even used your own report to double check (same as one I use)

Reported revenue for many transaction ID’s is very high – let’s say 1,125.00€ but if I click on the transaction ID to get the details, it reports 375 € (which correspond to the listed items , quantity & total).

While transactions for that ID = 1, its revenue = 3x the actual order value.

Any clue what could be the issue? It happens for many transactions and clearly inflates to the total revenue.

First time I see this…
Any tip would be welcome :-)

Cheers.

Michael

Jon Meck Jon Meck says:

Hi Michael,

Sounds like an issue with your tracking code. When you send in the transaction, you have to fill out the transactionTotal, as well as each product price/quantity. So you probably have one of two issues:

- The calculated transactionTotal that you’re passing in is wrong
– Certain products are not being included in the transaction that you’re sending to Google

Do some checking and let me know if that was the issue!
-Jon

Leave a Reply