- Enforce a timeout of 2 seconds if one is not provided. See https://www.simoahava.com/gtm-tips/use-eventtimeout-eventcallback.
- Guarantee the callback, if provided, will execute even if Tag Manager fails or is blocked by the browser. See https://stackoverflow.com/questions/60489452/should-i-use-eventcallback-for-gtm-before-redirecting.
Plop this function right after your Tag Manager embed code to make it available to your entire app. Minified with https://www.toptal.com/developers/javascript-minifier.
/**
* Helper function to guarantee business logic
* @see https://gist.github.com/taylorgorman/bbe349b63dd4f395523c2cdbf06b0bae
*/
function safeDataLayerPush(a){let e=a;if(a.eventCallback){let t=!1;
function l(){t||(t=!0,a.eventCallback())}setTimeout(l,a.eventTimeout||2e3),
e.eventCallback=l}window.dataLayer=window.dataLayer||[],window.dataLayer.push(e)}
Simple event with no parameters. To be clear: this is effectively identical to dataLayer.push({ event: 'sign_up' })
because our function does nothing different if eventCallback
isn't provided.
safeDataLayerPush({ event: 'sign_up' })
Event with one parameter and a callback.
safeDataLayerPush({
event: 'generate_lead',
form_id: '2023-lead-magnet',
eventCallback: function(){ form.submit() },
})
Event with ecommerce data, a callback, and custom timeout.
safeDataLayerPush({
event: 'purchase',
ecommerce: {
value: 224.67,
// ...
},
eventCallback: function(){ window.location = '/order?id=' },
eventTimeout: 4000,
})
/**
* @param {Object} data - The same object you would give to dataLayer.push
*/
function safeDataLayerPush( data ) {
// We need to change this, but still access the original.
let newData = data
// Ensure callback, if provided, will always be executed.
if ( data.eventCallback ) {
const defaultTimeout = 2000
// Ensure callback is executed only once.
let callbackDone = false
function doCallback() {
if ( callbackDone ) return
callbackDone = true
data.eventCallback()
}
// If Tag Manager fails or is blocked by browser,
// callback must still execute.
setTimeout( doCallback, data.eventTimeout || defaultTimeout )
// Change the data layer push callback to our function
// that prevents duplicate executions.
newData.eventCallback = doCallback
}
// Function can work without callback.
window.dataLayer = window.dataLayer || []
window.dataLayer.push( newData )
}
Thanks for sharing!
Unfortunately, I think there's a bug as
let newData = data
creates a reference, rather than a copy. This means that when overridingnewData.eventCallback = doCallback
, we actually also overridedata.eventCallback
leading todoCallback
never calling the original callback, butdoCallback
ends up calling itself.