Skip to content

Instantly share code, notes, and snippets.

@taylorgorman
Last active March 19, 2024 07:51
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=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)}

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 = 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.

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