Unos de los quebraderos de cabeza a la hora de realizar un seguimiento de una web se presenta cuando nos encontramos con un servicio que está «incrustado» dentro de un iframe.
Pongámonos en situación. Tenemos una web que necesita de un servicio externo, por ejemplo una empresa que vende viajes o un hotel que tiene un apartado de reservas.
Este servicio, con toda su lógica y pasos, se realiza a través de otra web que «sirve» a la principal el contenido a partir de un iframe, es decir que dentro de una ventana del navegador tenemos contenidos de dos webs:
- La web principal que es a la que accede el usuario (web padre).
- La web secundaria que sirve los contenidos a través del iframe (web hija).
En este contexto el problema es que en el navegador se cargan dos sitios webs diferentes.
Y para poder hacer un seguimiento de lo que ocurre en el iframe, hasta ahora, debíamos transferir las cookies del dominio principal al dominio del iframe, es decir que el navegador permitiera la transferencias de cookies de terceros. Y actualmente esto es un problema, ya que como puedes ver en la imágen inferior, la mayoría de navegadores bloquean por defecto las cookies de terceros.
Entonces, ¿Cómo podemos hacer ahora para poder realizar el seguimiento de los eventos del iframe sin necesidad de pasar esas cookies? La solución pasa por un nuevo enfoque: Reenviar los eventos del data layer de la web hija a la web padre.
Reenviar los eventos del data layer de la web hija a la web padre. Cómo hacerlo
Antes de nada, vamos a explicar como funciona este proceso.
La idea es poder pasar los eventos, y evidentemente las variables asociadas a cada uno de ellos, desde la capa de datos (data layer) de la web hija, la que sirve el contenido del iframe, a la capa de datos (data layer) de la web padre, la principal, de forma diferenciada.
De esta forma podremos gestionar todos los eventos, tantos los nativos de la web principal como los que ocurren en el iframe y que hemos transferido, desde un único punto que es el contendor de Google Tag Manager de la web padre. A partir de ahí, podremos trabajar con este contenedor para definir las variables, activadores y etiquetas necesarias, siguiendo la metodologçia CVAE.
Así es como funcionaria:
Página Principal | iframe |
En cuanto se carga GTM. carga una etiqueta Custom HTML que escuchará los mensajes que llegan desde el iframe | En cuanto se carga GTM, se carga una etiqueta Custom HTML que envía un mensaje childReady |
Cuando recibe el mensaje childReady del iframe, responde con un mensaje parentReady | |
Esta misma una etiqueta Custom HTML prepara los mensajes del datalayer a la espera del mensaje parentReady | |
En cuanto recibe el mensaje parentReady, captura los mensajes de su propio datalayer y los envía al datalayer de la web padre | |
Cada mensaje compatible del datalayer de la web hija es agregado al data layer de la web padre |
Para identificar los eventos que provienen de la web hija, o sea que se ejecutan en el iframe, en la etiqueta Custom HTML añadiremos un prefijo antes de ser enviado al datalayer de la web padre. De esa manera si en la web hija se lanza un evento Container Loaded (gtm.js) se enviará al datalayer de la web padre cómo iframe.gtm.js (el prefijo lo podremos personalizar a nuestro gusto).
Para poder implementar esta solución, antes de nada, debemos de tener creado e insertado en cada sitio web un contenedor de Google Tag Manager. Es decir, deberemos crear dos contenedores, uno para cada web. Una vez hecho esto, seguiremos el siguiente procedimiento.ver como hacerlo.
Configuración en la web padre
En el contenedor configurado para recoger las etiquetas en la web padre, la web principal, deberemos crear una etiqueta Custom HTML que activaremos en todas las páginas, o en aquellas en las que tengamos incrustado el iframe. A continuación podéis ver el código de esta etiqueta.
<script>
(function() {
// ID de seguimiento de la propiedad de Universal Analytics que vayas a usar.
// Si vas a usar GA4, puedes comentar la siguiente linea.
var trackingId = 'UA-XXXXX-Y';
// Tiempo máximo en milisegundos para esperar a que se cargue el rastreador de GA.
// Si usas GA4, la siguiente línea es irrelevante y puedes obviarla o comentarla.
var maxGATime = 2000;
// Dominio de la web hija ("https://iframe-dominio.com") con la que desea comunicarse
// (la web que sirve el iframe).
// Nota: poner el nombre del dominio con el protocolo, pero sin la barra final
var childOrigin = 'https://iframe-dominio.com';
// // No es necesario tocar nada de lo que va a continuación
var pollInterval = 200;
var postCallback = function(event) {
if (event.origin !== childOrigin) return;
if (event.data !== 'childReady' && !event.data.event) return;
if (event.data === 'childReady') {
// Enviar evento que indica que el datalayer de la web padre está listo
event.source.postMessage('parentReady', event.origin);
var pollCallback = function() {
// Detener el sondeo si se alcanza el tiempo máximo
maxGATime -= pollInterval;
if (maxGATime <= 0) window.clearInterval(poll);
// Continuar solo si GA está cargado y el rastreador es accesible
var ga = window[window['GoogleAnalyticsObject']];
if (ga && ga.getAll) {
// Obtener un rastreador que coincida con el ID de rastreo que proporcionaste
var tracker = ga.getAll().filter(function(t) {
return t.get('trackingId') === trackingId;
}).shift();
// Enviar mensaje de vuelta al iframe con ID de cliente
if (tracker) {
event.source.postMessage({
event: 'clientId',
clientId: tracker.get('clientId')
}, event.origin);
}
// Detener el sondeo si no se ha hecho todavía
window.clearInterval(poll);
}
};
// Comenzar a sondear para el rastreador de Google Analytics
var poll = window.setInterval(pollCallback, pollInterval)
}
// Enviar el mensaje (push datalayer) de la capa de datos del iframe a la capa de datos de la web padre
if (event.data.event) {
window.dataLayer.push(event.data);
}
};
// Empezar a escuchar mensajes de la web hija (iframe)
window.addEventListener('message', postCallback);
})();
</script>
Evidentemente, el script que muestro arriba es una maqueta que deberemos de modificar, pero tranquilo, que es poco lo que debes personalizar. A continuación te cuento.
Personalización para Universal Analytics
// ID de seguimiento de la propiedad de Universal Analytics que vayas a usar.
// Si vas a usar GA4, puedes comentar la siguiente linea.
var trackingId = 'UA-XXXXX-Y';
// Tiempo máximo en milisegundos para esperar a que se cargue el rastreador de GA.
// Si usas GA4, la siguiente línea es irrelevante.
var maxGATime = 2000;
La variable trackingID define el Id de seguimiento de la propiedad de Universal Analytics. Por otro lado la variable maxGATime establece, en milisegundos, el tiempo máximo que se ha de esperar a que cargue la etiqueta de Google Analytics. Por defecto este tiempo está configurado en 2000 milisegundos (dos segundos). Puedes ampliarlo, pero tampoco te pases.
Estas dos variables sólo se usan si quieres enviar los mensajes/eventos del iframe a una propiedad de Universal Analytics. En el caso de trabajar con GA4, estas dos variables no son necesarias y puedes eliminarlas, comentarlas o dejarlas como están.
Inclusión dominio web hija
// Dominio de la web hija ("https://iframe-dominio.com") con la que desea comunicarse
// (la web que sirve el iframe).
// Nota: poner el nombre del dominio con el protocolo, pero sin la barra final
var childOrigin = 'https://iframe-dominio.com';
Con la variable childOrigin definiremos el dominio de la web hija, es decir la web que sirve el iframe a la web principal. Aquí debes escribir toda la url, incluido el protocolo hasta la primera barra, importante: sin incluir esta barra final. Si, por ejemplo la url de tu home de la web hija fuera http://carlosmdh.es/home.php, el valor de esta variable debe ser http://carlosmdh.es
Recuerda que no debes incluir la barra final, si la incluyes no funcionará.
El resto del código no es necesario tocarlo. Su función básicamente es controlar los tiempos de reacción y saber cuándo pueden recibir los mensajes/eventos, desde el datalayer de la web hija al datalayer de la web principal cuando este esté preparado para recibirlo.
El siguiente paso será configurar el proceso en la web hija que envíe los mensajes/eventos. Veamos cómo.
Configuración en la web hija
Vamos a ver el escenario en el que la página hija sólo interactúa desde el iframe, es decir no se accede a la web hija directamente.
Como en la configuración anterior, vamos a necesitar tener insertado un contenedor de Google Tag Manager en la web hija. Mi recomendación es que sea independiente del contenedor de la web principal, así podremos tener mejor controlado el proceso y podremos usar este segundo contenedor para otros escenarios si fuera necesario.
En este segundo contenedor crearemos, al igual que en el caso anterior, una etiqueta Custom HTML en la que albergaremos el código javascript. A continuación te dejo el código de la etiqueta:
<script>
(function() {
// Si no está activado el iframe, no hace nada
try {
if (window.top === window.self) return;
} catch(e) {}
// Cambia el valor a false para evitar que los mensajes de capa de datos
// se envíen al padre (en este caso no tiene sentido)
var sendDataLayerMessages = true;
// Modifica el prefijo que usarás en el nombre del evento
// y bajo el cual se insertarán todos los eventos y datos
// de la capa de datos en el datalayer del padre (por defecto el prefijo es iframe)
var dataLayerMessagePrefix = 'iframe';
// Configura el dominio al que se enviarán los datos
// del data Layer ("https://www.dominio.com". (Dominio padre).
var parentOrigin = 'https://www.parent-domain.com';
// Tiempo máximo en milisegundos para sondear el marco principal para la señal de listo
var maxTime = 2000;
// A partir de aquí NO TOQUES NADA
var pollInterval = 200;
var parentReady = false;
var postCallback = function(event) {
if (event.origin !== parentOrigin) return;
if (event.data.event !== 'clientId' && event.data !== 'parentReady') return;
if (event.data.event === 'clientId') {
window.dataLayer.push({
event: 'clientId',
clientId: event.data.clientId
});
}
if (event.data === 'parentReady' && !parentReady) {
window.clearInterval(poll);
if (sendDataLayerMessages) startDataLayerMessageCollection();
parentReady = true;
}
};
var pollCallback = function() {
// Si se alcanza el tiempo máximo, deja de sondear
maxTime -= pollInterval;
if (maxTime <= 0) window.clearInterval(poll);
// Envía un mensaje a los padres de que iframe está
// listo para recuperar la identificación del cliente
window.top.postMessage('childReady', parentOrigin);
};
var createMessage = function(obj) {
if (!Array.isArray(obj) && typeof obj === 'object') {
var flattenObj = JSON.parse(JSON.stringify(obj));
var message = {};
// Agrega metadatos sobre la página de la web hija en el mensaje
message[dataLayerMessagePrefix] = {
pageData: {
url: document.location.href,
title: document.title
}
};
for (var prop in flattenObj) {
if (flattenObj.hasOwnProperty(prop) && prop !== 'gtm.uniqueEventId') {
if (prop === 'event') {
message.event = dataLayerMessagePrefix + '.' + flattenObj[prop];
} else {
message[dataLayerMessagePrefix][prop] = flattenObj[prop];
}
}
}
if (!message.event) message.event = dataLayerMessagePrefix + '.Message';
return message;
}
return false;
};
var startDataLayerMessageCollection = function() {
// Envía el contenido de la capa de datos hija a la capa de datos padre
window.dataLayer.forEach(function(obj) {
var message = createMessage(obj);
if (message) window.top.postMessage(message, parentOrigin);
});
// Crear el oyente push para mensajes futuros
var oldPush = window.dataLayer.push;
window.dataLayer.push = function() {
var states = [].slice.call(arguments, 0);
states.forEach(function(arg) {
var message = createMessage(arg);
if (message) window.top.postMessage(message, parentOrigin);
});
return oldPush.apply(window.dataLayer, states);
};
};
// Comienza a sondear la página principal con el mensaje "childReady"
var poll = window.setInterval(pollCallback, pollInterval);
// Empezar a escuchar mensajes de la página principal
window.addEventListener('message', postCallback);
})();
</script>
De la misma forma que en el primer script, este es un maqueta que deberemos de modificar, pero de nuevo que no cunda el pánico que las personalizaciones son mínimas. A continuación te cuento.
Definir el prefijo
// Modifica el prefijo que usarás en el nombre del evento
// y bajo el cual se insertarán todos los eventos y datos
// de la capa de datos en el datalayer del padre (por defecto el prefijo es iframe)
var dataLayerMessagePrefix = 'iframe';
La variable dataLayerMessagePrefix define el prefijo que añade el script cuando envía los eventos desde la capa de datos de la web hija a la capa de datos de la web padre. Su valor por defecto es iframe.
Si te sientes cómodo con ese valor, perfecto, pero si deseas puedes modificarlo. Eso sí, quédate con ese valor que luego vas a necesitarlo cuando crees las Variables y los activadores en Google Tag Manager.
Definir el origen
// Configura el dominio al que se enviarán los datos
// del data Layer ("https://www.dominio.com". (Dominio padre).
var parentOrigin = 'https://www.parent-domain.com';
La variable parentOrigin indica el dominio al que se van a enviar los mensajes/eventos. Al igual que en el caso anterior debes escribir toda la url, incluido el protocolo hasta la primera barra y sin incluir la barra final. Si, por ejemplo la url de tu home de la web principal fuera http://carlosmdh.es/home.php, el calor de esta variable debe ser http://carlosmdh.es
Recuerda que no debes incluir la barra final, si la incluyes no funcionará.
Definir el tiempo de espera a la web padre
Igual que en el caso anterior podemos definir un tiempo máximo para que el iframe intente enviar los datos a la capa de datos de la web principal, por efecto es 2000 milisegundos pero lo podemos modificar en casos especiales.
// Tiempo máximo en milisegundos para sondear el marco principal para la señal de listo
var maxTime = 2000;
Y, al igual que en la etiqueta anterior, el resto del código no es necesario tocarlo. Su función básicamente es controlar los tiempos de reacción y saber cuándo se pueden enviar los mensajes/eventos, desde el datalayer de la web hija al datalayer de la web principal.
Únicamente destacar dentro de este código, las líneas que os muestro a continuación. Su función es agregar las metadatos (url y Título) de la página en la web hija, la que sirve el iframe, antes de enviarlo al datalayer de la página padre.
// Agrega metadatos sobre la página de la web hija en el mensaje
message[dataLayerMessagePrefix] = {
pageData: {
url: document.location.href,
title: document.title
}
};
Y para que entienda un poco qué es lo que está ocurriendo, si miramos cualquier evento en la página hija esto es lo que veremos:
{
event: "actionProductView",
gtm: {uniqueEventId: 1},
data: {
items: [
{
id: "191732",
name: "NCC 1701 Enterprise",
brand: "My Brand",
category: "",
price: 15
}
]
}
}
Y este seria el push que se enviaria al datalayer de la web padre:
{
event: "star.trek.actionProductView",
gtm: {uniqueEventId: 17, start: 1664879689805},
star: {
trek: {
pageData: {
url: "https://iframe.cominio.com/bookingwidget" +
"/vendor/36875/id/191732?",
title: "Nave clase Constelación de segunda mano a buen precio"
},
data: {
items: [
{
id: "191732",
name: "NCC 1701 Enterprise",
brand: "Star Trek",
category: "",
price: 15
}
]
}
}
}
}
Si te das cuenta, ha añadido el prefijo al evento, en este caso startrek y los metadata (star.trek.pagedata.url y star.trek.pagedata.title).
Resultado final
Tras hacer el procedimiento, tendrás todos los eventos, y sus datos, en la capa de datos del contenedor de la web padre, la que contiene el iframe.
A partir de ahí, podrás crear las variables, los activadores, y las etiquetas que necesites para enviar los datos a tu herramienta (Google Analytics 4, Google Ads, Facebook Ads…).
Si te atascas con esto último, crear las variables, activadores y etiquetas, hazmelo saber y preparo otro artículo con un caso práctico de una web de reservas en la que tuve que aplicar este enfoque. Además vendría con bonus debido a una errónea implementación de la solución por parte del desarrollador del SaaS de reservas que va en el iframe.
Resumen
Es cierto, como decía Iñaki Gorostiza en un tuit hace unos días que el seguimiento de iframes es un poco engorroso, pero espero que este enfoque te sirva para poder hacerlo de una forma más sencilla y fiable.
Y una última aclaración. Los dos códigos JavaScript que te muestro han sido creados, no podía ser de otra manera por Simo Ahava, Dios le bendiga, que es por quien descubrí esta solución. Si quieres ver el artículo en el que habla de este enfoque, te adelanto que es un poco más técnico, te dejo aquí un enlace al mismo => Cookieless tracking for cross-sites iframes