Race Conditions en Data Collection: qué son y cómo solucionarlas
Asíncronía, problemas de timings, desajuste en los tiempo de carga... Seguro que todo esto te suena, ¿verdad?
¿Qué es una Race Condition?
En términos sencillos, dentro del mundo del desarrollo una race condition es una situación en la que dos o más operaciones intentan acceder o modificar los mismos recursos al mismo tiempo, sin una coordinación clara en el orden de ejecución. Esto puede resultar en comportamientos impredecibles, datos incorrectos o procesos incompletos, especialmente en entornos asincrónicos como el navegador web, donde los tiempos de carga de scripts y eventos pueden variar enormemente.
Las race conditions son particularmente problemáticas en sistemas de gestión de etiquetas (TMS) como Google Tag Manager, donde es común que múltiples etiquetas dependan de una secuencia o disponibilidad de datos específicos. Sin un control adecuado, se puede perder información crítica o, peor aún, se puede recolectar información incorrecta.
Ejemplos clásicos dentro de Data Collection
Race Condition en Thank you Page
Imaginemos que tienes una etiqueta de conversión que depende de que se haya completado una compra y que los datos relevantes estén presentes en el dataLayer. Supón que la etiqueta de conversión se dispara en la página de confirmación, pero en algunos casos, debido a la latencia de la red o al tiempo de procesamiento de otros scripts, el dataLayer aún no tiene los datos completos cuando la etiqueta intenta acceder a él, o peor, no se llega a ejecutar nunca y el usuario cierra la página. Como resultado, la conversión podría registrarse sin la información correcta o no registrarse en absoluto. Un clásico bastante más habitual de lo que uno piensa.
Race Condition en la Gestión de Consentimiento
Otro caso común de race condition se presenta en la gestión del consentimiento, donde GTM espera que el consent.update esté configurado antes de disparar etiquetas que dependen de dicho consentimiento. En algunas implementaciones, debido a la carga asincrónica de los CMP (Consent Management Platforms) o el retraso en el dataLayer, el evento consent.update puede no estar disponible inmediatamente, y la vista previa de GTM muestra un mensaje de advertencia en rojo en la vista previa, indicando que se ha intentado leer el estado del consentimiento antes de que se hubiese establecido. ¿Te suena?
Estrategias para evitarlas
Ahora que entendemos el problema —y lo hemos sufrido en nuestras propias carnes, seguro 100%—, vamos a analizar cómo podemos evitarlo utilizando diferentes estrategias de programación y herramientas disponibles en JavaScript y en los TMS.
1. Uso de dispatchEvent para coordinar eventos
Una solución efectiva para evitar que una etiqueta se ejecute antes de que el dataLayer esté listo es usar un evento personalizado (CustomEvent) que notifique cuándo los datos están disponibles.
Ejemplo:
// Función para obtener datos de una API y actualizar el dataLayer con un evento específico
function fetchDataAndUpdateDataLayer(url, eventName) {
fetch(url)
.then(response => response.json())
.then(data => {
dataLayer.push({
'event': eventName,
'data': data
});
document.dispatchEvent(new CustomEvent('apiReady'));
})
.catch(error => {
console.error("Error al obtener datos de la API:", error);
});
}
// Listener para 'apiReady' que ejecuta la lógica dependiente de los datos
document.addEventListener('apiReady', function() {
console.log("Datos de la API disponibles en el dataLayer, ejecutando lógica dependiente.");
// Aquí podrías disparar una etiqueta o función dependiente de estos datos
});
// Ejemplo de uso de la función
fetchDataAndUpdateDataLayer("https://api.example.com/user-data", "userDataReady");
Este enfoque modular y flexible es ideal para implementar en escenarios donde múltiples fuentes de datos pueden estar en juego y cada llamada debe estar perfectamente sincronizada con la carga de etiquetas.
Al hilo de esto, os ofrezco un desarrollo de la casa para elevar al cubo la gestión de eventos sin tener que iterar sobre múltiples eventListeners:
// Creamos un proxy para interceptar los cambios en el dataLayer const dataLayer = new Proxy([], { set: function(target, property, value) { target[property] = value; // Detectamos si el evento que necesitamos está en el dataLayer if (value.event === 'userDataReady') { console.log("userDataReady detectado en el dataLayer, ejecutando lógica dependiente."); // Ejecutamos la lógica necesaria cuando se detecta el evento específico executeDependentLogic(value.data); } return true; } }); // Función genérica para obtener datos de una API y actualizar el dataLayer con un evento específico function fetchDataAndUpdateDataLayer(url, eventName) { fetch(url) .then(response => response.json()) .then(data => { // Añadimos los datos necesarios al dataLayer tras la respuesta de la API dataLayer.push({ 'event': eventName, 'data': data }); }) .catch(error => { console.error("Error al obtener datos de la API:", error); }); } // Función que se ejecuta cuando el evento 'userDataReady' es detectado en el dataLayer function executeDependentLogic(data) { // Aquí podrías disparar una etiqueta, enviar datos adicionales, o cualquier otra lógica console.log("Lógica dependiente ejecutada con los datos:", data); } // Ejemplo de uso: Llamada a la API y especificación de un evento único fetchDataAndUpdateDataLayer("https://api.example.com/user-data", "userDataReady");En esta implementación, usamos un proxy para monitorear el
dataLayery ejecutar automáticamente una lógica específica cuando se detecta un evento particular, en este caso,userDataReady. Al definir eldataLayercomo un proxy, podemos interceptar cada vez que se realiza unpushen él. Cuando el proxy detecta que el evento agregado esuserDataReady, activa la funciónexecuteDependentLogicy le pasa los datos asociados a este evento.Este enfoque elimina la necesidad de configurar listeners explícitos cada vez que queremos ejecutar una acción dependiente de un evento en el
dataLayer. En lugar de eso, el proxy se encarga de observar todos los cambios en eldataLayery de activar la lógica dependiente de forma automática cuando encuentra un evento que coincide conuserDataReady.La función
executeDependentLogicse convierte en el punto donde centralizamos la lógica específica para este evento. Por ejemplo, en lugar de esperar manualmente con listeners o eventos personalizados, el proxy ejecuta cualquier acción necesaria, como disparar una etiqueta o procesar datos, en el momento en que detecta el evento.Este enfoque hace que la gestión de múltiples eventos en el
dataLayersea más eficiente y reduce la necesidad de listeners repetitivos, permitiendo una implementación más limpia y adaptada a entornos de data collection complejos. Vamos, una fumada muy guapa.
2. Uso de callbacks() para garantizar el orden de ejecución
Un callback es una función que se ejecuta una vez que otra función ha terminado su tarea. Esto asegura un orden de ejecución cuando hay dependencias directas entre tareas.
Ejemplo de callback para la ejecución secuencial:
function loadConversionData(callback) {
// Simulamos la carga de datos del servidor o dataLayer
setTimeout(() => {
dataLayer.push({ 'event': 'conversion', 'transactionId': '67890', 'value': 200 });
console.log("Datos de conversión cargados.");
callback(); // Llamamos al callback cuando los datos están listos
}, 1000); // Simulamos un retraso de 1 segundo
}
function executeConversionTag() {
console.log("Ejecutando etiqueta de conversión.");
}
// Ejecutamos loadConversionData y pasamos executeConversionTag como callback
loadConversionData(executeConversionTag);En este ejemplo, executeConversionTag solo se ejecutará después de que loadConversionData haya completado la carga de datos.
Dentro de Google Tag Manager, tenemos una opción similar, especialmente útil para coordinar el flujo de eventos: el método
eventCallback. Este método permite asociar una función que se ejecutará después de que se complete eldataLayer.push(). De esta forma, podemos asegurarnos de que cualquier acción dependiente de los datos en eldataLayerocurra solo después de que estos se hayan procesado.El uso de
eventCallbackes particularmente útil en casos donde queremos que una acción, como una redirección, se ejecute solo después de que los datos de un evento, como un clic en un producto, se hayan enviado aldataLayer. A continuación, te mostramos un ejemplo práctico de su implementación:Ejemplo de Uso de
eventCallbacken un Evento de Clic en Producto:dataLayer.push({ 'event': 'newsletterSubscription', 'subscription': { 'email': subscriberObj.email, 'firstName': subscriberObj.firstName, 'source': 'Homepage Signup Form' }, 'eventCallback': function() { // Redirige a la página de agradecimiento solo después de procesar el evento window.location.href = '/thank-you'; } });Para asegurar que los datos de una suscripción se registren en el
dataLayerantes de redirigir al usuario, usamoseventCallback. Este método permite ejecutar una acción (como una redirección) solo después de que el evento haya sido procesado.
3. Implementación de Promises y async/await para la sincronización de tareas asíncronas
Para casos en los que tienes múltiples dependencias que deben completarse antes de continuar, las Promises o async/await pueden ofrecer un control robusto. Esto es especialmente útil si manejas llamadas a APIs o recursos externos.
Ejemplo usando Promises:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
dataLayer.push({ 'event': 'fetchComplete', 'data': 'sample data' });
resolve("Datos cargados exitosamente.");
}, 1500); // Retraso de 1.5 segundos
});
}
fetchData().then((message) => {
console.log(message); // "Datos cargados exitosamente."
console.log("Ejecutando lógica después de fetchData");
}).catch((error) => {
console.error("Error cargando datos: ", error);
});Con async/await:
async function executeDataFlow() {
await fetchData();
console.log("Datos listos, ejecutando lógica dependiente.");
}
executeDataFlow();Este método garantiza que executeDataFlow se ejecute solo después de que fetchData haya completado su tarea, ayudando a evitar conflictos de sincronización.
Poco más que decir. ¿Dime si has visto algo más limpio? El único pero es que async/await no funciona en todos los TMS —no miro a nadie, GTM—, por lo que si decides inyectar algo de esto, tendrás que probar con una promesa…
4. Uso de MutationObserver para monitorizar cambios en el DOM
Si tu etiqueta depende de la existencia de elementos específicos en el DOM (como cuando usas un TMS que necesita detectar cambios en el contenido de la página), el MutationObserver es una herramienta muy potente. Permite escuchar cambios en el DOM y responder a ellos solo cuando ciertos elementos están disponibles.
Ejemplo de MutationObserver:
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (document.querySelector('#conversionElement')) {
console.log('Elemento de conversión detectado en el DOM.');
// Ejecuta la lógica dependiente
observer.disconnect(); // Detenemos la observación después de detectarlo
}
});
});
// Observamos el cuerpo del documento en busca de cambios
observer.observe(document.body, { childList: true, subtree: true });Este enfoque permite que tu código reaccione a la aparición de elementos específicos en el DOM, eliminando las race conditions que pueden surgir por la carga asíncrona de contenido. En SPA, te vas a hartar de utilizar mutationObserver, créeme.
5. Implementación de estrategias de reintentos (Retries)
Cuando no puedes garantizar el orden de carga o necesitas intentar varias veces para obtener un recurso, implementar un sistema de reintentos con límite es una solución efectiva. Esto es útil en situaciones donde la disponibilidad de los datos puede variar.
Ejemplo de retry con setTimeout:
function tryLoadData(attempts = 5) {
if (checkDataLayerData()) {
console.log("Datos listos, ejecutando etiqueta.");
} else if (attempts > 0) {
setTimeout(() => tryLoadData(attempts - 1), 500); // Intento cada 500ms
} else {
console.log("Límite de intentos alcanzado, abortando.");
}
}
function checkDataLayerData() {
return dataLayer.some(item => item.event === 'purchase');
}
tryLoadData(); // Iniciamos la función de reintentoEsta función intenta obtener los datos necesarios hasta cinco veces antes de abortar, aumentando las probabilidades de éxito en entornos asíncronos.
Personalmente, he utilizado más veces de las que pensaba este tipo de funciones… Es una solución bastante útil, pero tenemos que ajustar muy bien los intentos para no afectar en el rendimieto del sitio web y, lo que es más importante, que funcione correctamente un alto porcentaje de las veces.
Conclusión
Las race conditions son un desafío común en nuestro campo, especialmente en entornos de TMS como GTM. Afortunadamente, existen múltiples estrategias y herramientas en JavaScript para minimizar su impacto y asegurar que las etiquetas y scripts se ejecuten en el orden correcto, con los datos completos y precisos. Desde el uso de eventos personalizados y callbacks hasta herramientas avanzadas como MutationObserver, dominar estas técnicas te permitirá asegurar la calidad y confiabilidad de tus datos.
Espero que este artículo te haya sido útil para abordar las race conditions en tus implementaciones y mejore la precisión de tus resultados en data collection. ¡Me cuentas!




