JavaScript race conditions en Google Tag Manager (GTM): cómo evitar fallos al trabajar con scripts asíncronos con un Custom Template

JavaScript es un lenguaje de programación ‘single threaded’, no puede ejecutar varias tareas a la vez. Hasta que no se ejecuta el ‘task’ A, no se puede arrancar con el ‘task’ B. Te pongo un ejemplo. Cuando un navegador carga una página, solicita un montón de recursos al servidor en el que se aloja la web (el documento .html, hojas de estilo CSS, archivos JS, etc.). Todas estas peticiones se procesan una a una, y cuanto mayor sea su número, más tardará el ‘site’ en cargarse.
Detalle de la pestaña 'Network' de la consola de depuración de mi navegador al cargar la url https://www.analyticsimplementations.com. Aquí se pueden ver todas las peticiones que el browser lanza para cargar la página.

El elemento script (responsable de cargar archivos JS en una página web) puede hacer uso del atributo async para agilizar este proceso. Cuando tu navegador empieza a renderizar un documento .html y se encuentra con un elemento script con este atributo, no se detiene, lo empieza a descargar en un segundo plano mientras sigue ejecutando el resto de peticiones. Cuando la descarga de este recurso se ha completado, se ejecuta el mismo. Esto es lo que se conoce como asincronía en JavaScript, y es la punta del iceberg de fenómenos complejos como por ejemplo los promises.

Podrías considerar que los scripts y tags que ejecutas desde Google Tag Manager (o cualquier otro gestor de etiquetas) son asíncronos. Si ejecutas un tag A y un tag B en un mismo evento (gtm.js, por ejemplo) uno no espera al otro, se empiezan a ejecutar a la vez y no hay forma de controlar cuál de los dos terminará antes. Esto puede traer problemas. Si el tag B necesita que el tag A se ejecute antes y esto no sucede, el tag B no funcionará bien y empezarás a ver errores JS en la consola de tu navegador. Esto es lo que se conoce como race conditions en JavaScript.

GTM pone a tu disposición dos métodos para controlar esto:

  1. La priorización de etiquetas. Aquí hay letra pequeña. La priorización de etiquetas no garantiza que el tag B se ejecute una vez se haya completado el script del tag A, sólo determina cuál de los dos empieza a ejecutarse antes.
  2. La secuenciación de etiquetas de etiquetas. Esta funcionalidad sí permite evitar race conditions, pero sólo si trabajas con templates o custom templates que hacen uso del método gtmOnSuccess()

Si estás cargando un script desde un tag de tipo HTML personalizado (Custom HTML Tag) y quieres evitar race conditions, debes seguir otra senda. Lo que te propongo a continuación es trabajar con una función callback, es decir, con una función que se ejecuta cuando se completa una tarea previa, en este caso la carga de un script. Esto no es nada nuevo y ya han corrido ríos de tinta al respecto (‘Everything you need to know about tag sequencing, Bounteous.com). Pero sí es un método con el que me gusta trabajar, siempre me ha funcionado bien.

Provocar un race condition en tu navegador

Lo primero que hay que conseguir es provocar un race condition. Para ello te propongo los siguientes dos scripts. Como verás, son muy sencillos:
window.setTimeout(function(){window.testObject = {'testKey':'testValue'}},3000);
Este primer script crea la variable global (en el objeto window) testObject. A través del método setTimeOut() se retrasa la ejecución del script 3 segundos para forzar el race condition.
window.dataLayer = window.dataLayer || [];

if(testObject){

    dataLayer.push({'event':'no race condition!'})
}

Este segundo script contiene una función con un if statement: si existe el objeto testObject, ejecuta un dataLayer.push(). ¿Y si no existe? Sorpresa: habrás incurrido en un race condition. ¿Por qué? Porque este código está llamando a una variable que aún no existe ya que el primer script aún no se ha cargado o no ha terminado de hacerlo.

Bien, voy a subir ambos archivos JS a una carpeta de mi servidor. Al primer script lo llamaré testGlobalVariable.js, al segundo raceConditionTest.js. A continuación voy a llamar a cada uno desde un Custom HTML Tag diferente que se ejecuten en el mismo evento: gtm.js. Fíjate lo que sucede:

A través de este tag de tipo HTML personlizado cargo el script testGlobalVariable.js
A través de este otro tag de tipo HTML personalizado cargo el script raceConditionTest.js
Ambos tags se empiezan a ejecutar en el mismo momento: en el evento gtm.js
El resultado: un fallo provocado por un race condition entre ambos scripts.
Un race condition en toda regla. El segundo script ya se ha cargado cuando el primero todavía no lo ha hecho al tener un setTimeOut() de 3 segundos. Puesto que este segundo script referencia al primero, se produce un error.

Una función callback para notificar de la carga del script

La solución a esto (una de ellas) es añadir un event listener a la carga del primer script apoyándote en el evento load. Cuando se haya cargado, una función callback ejecutará un dataLayer.push(). En ese momento puedes tener la seguridad de que el segundo script se puede ejecutar sin temor a incurrir en un race condition.

Para seguir esta senda, debes crear el elemento script y añadirlo a tu DOM con JavaScript, tal y como explica el gran Simo Ahava en su post ‘#GTMTips: Add a load listener to script elements’. Si añades load listeners a un elemento script que carga un recurso externo, esta forma de proceder es más segura.

Este es el código que debes implementar a través de un Custom HTML Tag (todo el mérito es del Sr. Simo Ahava, a cuyo artículo enlazo en el anterior párrafo):

<script>
  (function() {
    var dataLayer = window.dataLayer || [];
    var el = document.createElement('script');
	el.src = 'https://analyticsimplementations.com/tests/testGlobalVariable.js';
	el.async = 'true';
	el.addEventListener('load', function() {
	dataLayer.push({
	  'event':'script loaded'
	});
    });
    document.head.appendChild(el);
  })();
</script>

También debes actualizar el código presente en el archivo testGlobalVariable.js y retirar el setTimeOut() de 3 segundos. De lo contrario, por mucho que se cargue en el DOM el elemento script del anterior script, el recurso JS al que llama seguirá tardando 3 segundos en ejecutarse y se seguirá incurriendo en un race condition.

Así las cosas, voy a simplificar el archivo testGlobalVariable.js aún más y dejarlo así:

window.testObject = {'testKey':'testValue'}

Ahora sólo te queda actualizar el trigger del Custom HTML Tag que ejecuta el segundo script (raceConditionTest.js) para que se dispare en el momento en que la función callback del anterior código notifique al dataLayer que el script se ha cargado. Para ello debes usar un trigger de tipo Custom event (Evento personalizado)

Este trigger responde al evento 'script loaded', que es el que se empuja al dataLayer al completarse la carga del script.
El tag 'CHTML - Script loader - raceConditionTest.js', actualizado con el nuevo trigger.
El tag 'CHTML - Script creator - testGlobalVariable.js' se ejecuta en el evento gtm.js
Al completarse su ejecución, la función callback del anterior script empuja al dataLayer el evento 'script loaded'. Es en este momento cuando se ejecuta el tag 'CHTML - Script loader - raceConditionTest.js'.
El tag 'CHTML - Script loader - raceConditionTest.js' ejecuta un dataLayer.push() con el evento 'no race condition!'.
Una consola de depuración feliz y contenta sin errores JS.

Evita los race conditions con un Custom Template de GTM

Si has llegado hasta aquí te doy la enhorabuena y las gracias ¡Agradezco tu interés! Si haces scroll hasta arriba del todo verás que el titular de este post lee ‘JavaScript race conditions en Google Tag Manager (GTM): cómo evitar fallos al trabajar con scripts asíncronos con un custom template‘. Pues bien, te voy a enseñar a hacer el mismo ejercicio a través de una una plantilla personalizada en Google Tag Manager. El resultado será el mismo: un tag que cargará un archivo JS de forma asíncrona y hará un push al dataLayer cuando el recurso se haya cargado de forma correcta.

GTM recomienda de forma abierta usar, en la medida de lo posible, custom templates para ejecutar JavaScript (y no Custom HTML Tags ni Custom JavaScript Variables). Estas plantillas personalizadas permiten trabajar con el sandbox de JavaScript de Google Tag Manager, un entorno securizado que minimiza los riesgos al máximo. Tiene una desventaja: no puedes hacer uso de todas las APIS de tu navegador. Tus opciones vienen determinadas por las propias APIS de los custom templates. Sea como sea, la tarea de la que trata este post sí se puede acometer. 

He desarrollado un custom template en GTM muy sencillo que te permite importar recursos externos a través de un script que se carga de forma asíncrona.  Este es el código que debes incluir en tu custom template, aunque si lo prefieres puedes descargar el archivo .tpl del repositorio que he creado en Github para este custom template:  

//API's needed for this template to work
const injectScript = require('injectScript');
const createQueue = require('createQueue');
const dataLayerPush = createQueue('dataLayer');

//Inject script and upon doing so, push an event to the dataLayer
injectScript(data.src, data.gtmOnSuccess(dataLayerPush({'event':data.event})), data.gtmOnFailure);

El código que ves arriba es un buen ejemplo de cómo funciona el sandbox de JS de GTM. Para poder llevar a cabo cualquier tarea, debes importar las API’s correspondientes antes. Ahora fíjate sólo en la línea 7 del código. Se llama a la API injectScript, que admite tres argumentos:

  1. La url que contiene el recurso que quieres importar a través de tu script asíncrono
  2. gtmOnSuccess(). En este caso se usa esta llamada a modo de función callback: cuando el script se carga de forma correcta se ejecuta un dataLayer.push() con un evento.
  3. gtmOnFailure. Se podría usar para invocar a otra función si el script fallara.

Para determinar qué recurso quieres cargar y el evento que quieres empujar al dataLayer, debes configurar los siguientes campos:

Para poder importar recursos alojados en una url determinada, antes debes actualizar los permisos asociados con la API injectScript() en el custom template.

El resultado, a continuación:

El custom template se ejecuta en el evento gtm.js y llama al recurso externo alojado en https://analyticsimplementations.com/tests/testGlobalVariable.js. Cuando el script asíncrono se ha cargado de forma correcta se empuja al dataLayer el 'event' 'script loaded'
El tag 'CHTML - Script loader - raceConditionTest.js' se ejecuta en el evento 'script loaded'. El recurso que carga (raceConditionTest.js) empuja el evento 'no race condition!' al dataLayer. )

¿Y qué pasa si te encuentras con fallos CORS?

CORS son las siglas de Cross-Origin Resource Sharing, un término que hace alusión a cómo se comunican los navegadores y los servidores. Si el site midominio.com solicita un recurso al servidor soyotrodominio.com es posible que la carga de este recurso no se produzca y veas alertas CORS en la consola de tu navegador. ¿Por qué? Es una medida de seguridad. La comunicación entre end-points de diferente origen o dominio tiene que estar configurada de forma consecuente. 

Si usas el custom template o el CHTML Tag que te propongo en este post para llamar a un recurso que está alojado bajo el mismo dominio que tu site no tendrás problema. Pero si intentas llamar a un recurso alojado bajo otro dominio es posible que sí lo tengas. Para solucionar un problema de CORS tienes que tener acceso al servidor para configurar los response headers. Si este no es tu caso, poco puedes hacer al respecto. La buena noticia es que la mayoría de las CDN’s ya hacen este trabajo por ti. Sea como sea, a continuación te dejo dos enlaces que te serán de utilidad:

‘How to win at CORS’, Jakearchibald.com 

-‘CORS in 100 seconds‘, de Fireship

 

1 comentario en «JavaScript race conditions en Google Tag Manager (GTM): cómo evitar fallos al trabajar con scripts asíncronos con un Custom Template»

Los comentarios están cerrados.

pornance.net
www.fuck-videos.net
zettaporn.com