Skip to content

Instantly share code, notes, and snippets.

@taylorgorman
Last active January 20, 2025 21:17
Show Gist options
  • Save taylorgorman/bbe349b63dd4f395523c2cdbf06b0bae to your computer and use it in GitHub Desktop.
Save taylorgorman/bbe349b63dd4f395523c2cdbf06b0bae to your computer and use it in GitHub Desktop.
A safer dataLayer.push for Google Tag Manager

A safer dataLayer.push for Google Tag Manager

Goals

  1. Enforce a timeout of 2 seconds if one is not provided. See https://www.simoahava.com/gtm-tips/use-eventtimeout-eventcallback.
  2. 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.

Minified

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=Object.assign({},a);if(a.eventCallback){let t=!1;
function n(){t||(t=!0,a.eventCallback())}setTimeout(n,a.eventTimeout||2e3),
e.eventCallback=n}window.dataLayer=window.dataLayer||[],window.dataLayer.push(e)}

Use

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,
})

Unminified and documented

/**
 * @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 = Object.assign( {}, 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 )
}
@marckohlbrugge
Copy link

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 overriding newData.eventCallback = doCallback, we actually also override data.eventCallback leading to doCallback never calling the original callback, but doCallback ends up calling itself.

@taylorgorman
Copy link
Author

You are completely correct, @marckohlbrugge. Thanks, and fixed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment