Preventing duplicate transactions in Google Analytics when the JavaScript sending to GA is buried in Google Tag Manager
Links to an example of the problem: https://platform.funraise.io/transaction/1294609/profile https://analytics.google.com/analytics/web/#/report/conversions-ecommerce-transaction/a341980w565503p537677/_r.drilldown=analytics.transactionId:1294609&explorer-table.plotKeys=%5B%5D
This is the string donating produces:
https://www.example.org/checkout/success/thank-you-for-saving-lives?first_name=ben&last_name=melancon&address1=51%20Hano%20Street&city=Boston&state=Massachusetts&postal=02134&country=United%20States&email=ben%40agaric.coop&donation_id=1316894&amount=5&payment_type=card&frequency=o
https://tagmanager.google.com/#/container/accounts/1699674669/containers/7376334/workspaces/85/tags
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
(function() {
var GA_TRACKING_ID = 'UA-xxxxxx-1';
function getParameterByName(name) {
var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}
function getDonationIds() {
var cookies = decodeURIComponent(document.cookie).split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i];
// Remove any whitespace from cookie strings.
while (cookie.charAt(0) == ' ') {
cookie = cookie.substring(1);
}
if (cookie.indexOf("donation_ids=") == 0) {
// Return the value of our donation transaction ID cookie.
return cookie.substring(13, cookie.length);
}
}
return "";
}
function setDonationId(donation_id) {
var cookie = 'donation_ids=' + donation_id;
if (old_donation_ids = getDonationIds()) {
cookie += ',' + old_donation_ids;
}
var date = new Date();
date.setTime(date.getTime() + 10*365*24*60*60*1000);
cookie += ";expires=" + date.toUTCString() + ";path=/";
document.cookie = cookie;
}
function hasDonationId(donation_id) {
var has_donation_id = false;
var donation_ids = getDonationIds().split(',');
for (var i = 0; i < donation_ids.length; i++) {
if (donation_ids[i] == donation_id) {
has_donation_id = true;
}
}
if (!has_donation_id) {
setDonationId(donation_id);
}
return has_donation_id;
}
function ready(fn) {
if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading"){
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
function createThankYouMessage() {
var elem = document.getElementById('message');
if (getParameterByName("first_name")!=null) {
var message = "Dear " + getParameterByName("first_name") + ",";
} else {
var message = " ";
}
elem.innerText = message;
}
function sendDonationToGA() {
// The createThankYouMessage function is being called inside of sendDonationToGA
// because sendDonationToGA is only being called when the document is ready, i.e., ready(sendDonationToGA);
createThankYouMessage();
ga('create', GA_TRACKING_ID, 'auto');
var referrer = getParameterByName('referrer')
if(referrer && typeof referrer === 'string' && referrer.length > 0) {
referrer = decodeURIComponent(referrer)
} else {
referrer = document.referrer
}
ga('send', 'pageview');
// We check a cookie where we store donation transaction IDs to ensure
// this donation hasn't already been sent to Google Analytics.
if (hasDonationId(getParameterByName('donation_id'))) {
return;
}
ga('require', 'ecommerce');
ga('ecommerce:addTransaction', {
id: getParameterByName('donation_id'),
revenue: getParameterByName('amount'),
shipping: '0',
tax: '0'
});
ga('ecommerce:addItem', {
'id': getParameterByName('donation_id'),
'name': 'Donation',
'sku': getParameterByName('payment_type'),
'category': getParameterByName('frequency'),
'price': getParameterByName('amount'),
'quantity': '1'
});
ga('ecommerce:send');
}
ready(sendDonationToGA);
})();
</script>
<!-- Facebook Pixel Code -->
<script>
function getParameterByName(name) {
var match = RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
};
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '1717800408475308');
fbq('track', 'PageView');
fbq('track', 'Purchase', {
value: getParameterByName('amount'),
currency: 'USD',
frequency: getParameterByName('frequency'),
monthly: ((getParameterByName('frequency')=='m') ? true : false),
});
</script>
<noscript><img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=1717800408475308&ev=PageView&noscript=1&ev=Purchase&cd[value]=amount&cd[currency]=USD"
/></noscript>
<!-- End Facebook Pixel Code -->
<meta name="keywords" content="donor">
<!-- Care2 Tracking Pixel Code -->
<script>!function (w, d, e, u, m, t, s) {
if (w.care2Targeting) return;
m = w.care2Targeting = function (params) {
m.callMethod ? m.callMethod.apply(m, [params]) : m.queue.push(params)
};
m.push = m;
m.version = '1.0';
m.queue = [];
t = d.createElement(e);
t.async = !0;
t.src = u;
s = d.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t, s);
}(window, document, 'script', '//dingo.care2.com/targeting/pixel.js');
care2Targeting({
clientid: '362',
email: getParameterByName('email'),
value: getParameterByName('amount'),
repeating: ((getParameterByName('frequency')=='m')? true : false)
});
</script>
<!-- End Care2 Donation Pixel Code -->
<script type="text/javascript">
window.VWO = window.VWO || [];
window.VWO.push(['track.revenueConversion', getParameterByName('amount')]);
</script>
Some of my summary of the work from ticket: https://devcollaborative.plan.io/issues/8788
The current approach to donation tracking is to do it from within custom JavaScript in custom UTM tags, as documented here https://devcollaborative.plan.io/issues/8847
There was no way to embed a custom JavaScript snippet from within Funraise at the point of a donation being made. Funraise also imposes a lot of restrictions on how this could be rearchitected. And rather than rearchitect anything, I have opted to add the check against duplicate transactions directly in the custom code.
This is simpler than all the steps of the custom task approach; because we send to Google Analytics in custom code, it’s most straightforward to put the check to refrain from sending in the same code.
Reloading old donation paths will produce a duplicate transaction, as the cookie has not been set. Likewise, if people clear cookies, switch browsers or browser sessions, share their donation-thanks URL with other people, or anything like that, the duplicate transaction will still happen— this only works, and the only way it can work practically without replacing Funraise’s processing, is to have the donor’s browser store a cookie with the transaction ID and check against that. (This is the same as the approaches you found.)
Important note: This only affects the donation form at https://www.example.org/donate/save-lives-end-hunger (and any other that ends up at the thank you page https://www.example.org/checkout/success/thank-you-for-saving-lives )
Resources consulted
- https://www.w3schools.com/js/js_cookies.asp (yeah they’re better now)
- https://www.simoahava.com/analytics/prevent-google-analytics-duplicate-transactions-with-customtask/
- https://support.google.com/tagmanager/answer/6279951?hl=en
- https://www.analyticsmania.com/post/ecommerce-tracking-with-google-tag-manager/#duplicate-transactions
- https://www.thyngster.com/preventing-duplicate-transactions-in-universal-analytics-with-google-tag-manager/
- https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce
Alternative approaches looked at
Triggers:
- https://www.bounteous.com/insights/2015/07/22/art-double-negative-using-trigger-exceptions-gtm/
- https://www.simoahava.com/analytics/trigger-guide-google-tag-manager/