Google Tag Manager (GTM) server-side supone un cambio de paradigma en las implementaciones con este gestor de etiquetas. Un contenedor web de GTM (ojo, también hay contenedores iOS, Android y AMP) se implementa en un ‘site’ y lanza peticiones por http directamente desde el navegador de un usuario a los servidores de herramientas de terceros como Google Analytics. Con GTM server-side esto cambia. El contenedor se implementa en un servidor (Google Cloud, AWS, etc.) y actúa como un proxy: recibe requests por http desde un navegador (por ejemplo) y los deriva a un end-point final.
El esquema clásico de Google Tag Manager de tags, triggers y variables sigue vigente, pero se une a la ecuación una nueva pieza que es fundamental: el cliente. El cliente es el encargado de interceptar los ‘requests’ entrantes a tu servidor de tagueo y procesar la información para que pueda ser usada por los tags. Sin un cliente que recepcione estas peticiones entrantes tu implementación de GTM server-side no va a funcionar. ¡Es así de crudo! Es un elemento realmente crítico.
GTM server-side pone a tu alcance una serie de clientes que puedes usar desde ya mismo. Todos ellos están orientados -como es lógico- a ser usados en implementaciones basadas en otros productos de Google, como por ejemplo GA4.
El ‘event object’: el equivalente al dataLayer de GTM en tu ‘tagging server’
Es importante entender el papel que juega un cliente en Google Tag Manager server-side. Puede cumplir muchos cometidos. Si quisieras, podrías desarrollar una API a través de un cliente que solicite a un servicio un recurso. Esto es, de hecho, lo que te propongo en el primer post que escribí en este blog: ‘Trabajar con un dataLayer alojado en servidor (server-hosted) en Google Tag Manager (GTM). No obstante, lo más habitual es que un cliente intercepte un ‘request’ por http y genere un ‘event object’ con él.
Este ‘event object’ es un objeto que se forma con la información que transporta el ‘request’: los query strings de la url, los request headers, el request body, etc. Viene a ser el equivalente al dataLayer de GTM en un contenedor web. El cliente genera este objeto y ejecuta una instancia de tu contenedor de GTM server-side. De esta forma, los tags que se ejecuten en este momento harán uso de la información presente en este objeto
Como te decía al comienzo de este post, GTM server-side incluye unos clientes por defecto. Todos ellos están orientados a ser usados en flujos de datos de productos de Google. ¿Qué pasa si quieres trabajar con ‘requests’ de otros ‘players’? Google Tag Manager te anima a que en estos casos uses un flujo de datos de GA4. El cliente de GA4 genera un objeto cuyos datos pueden ser explotados por tags de otras herramientas. Esto es justo lo que pasa con Facebook CAPI, por ejemplo.
Puedes seguir la anterior senda, o puedes desarrollar un cliente propio.
Un request por http es una comunicación que se establece entre dos 'end-points'
El cliente que te propongo a continuación es capaz de interceptar y digerir cualquier petición por http de tipo GET y POST que llegue a tu servidor de tagueo. ¿Y qué es exactamente una petición http? Es una comunicación que se establece entre dos ‘end-points’ conforme a un protocolo establecido. Un buen ejemplo es la comunicación que se establece entre un navegador y un servidor. Un navegador lanza un request por http a un servidor solicitando un recurso (una foto, por ejemplo). El servidor evalúa esta petición y responde al navegador con el recurso solicitado.
Si inspeccionas la pestaña ‘Network’ de la consola de depuración de tu navegador puedes ver todas las peticiones por http que lanza tu navegador…y las respuestas que reciben. Así, para poder leer este post, estas son algunas de las peticiones que ha lanzado tu navegador al servidor en el que se aloja mi ‘site’:
El cliente para interceptar peticiones en GTM server-side
El ‘client template’ para Google Tag Manager server-side que he desarrollado acepta cualquier petición GET o POST entrante a tu servidor de tagueo. Genera un objeto con los siguientes componentes de dicha petición:
- Los ‘query strings’ de la url de la solicitud
- Los siguientes ‘request headers’: origen, país y ciudad (estos dos últimos los genera AppEngine en Google Cloud). Se incluyen además los encabezados que recogen la IP del usuario y el User-Agent. Como verás más adelante, puedes anonimizar el ‘event object’ excluyendo estos parámetros del mismo.
- Puedes, además, incluir cualquier encabezado adicional que consideres. Lo único que tienes que hacer es editar la parte correspondiente del código.
- Si la petición es de tipo POST y cuenta con un request body, las propiedades y valores del cuerpo de la petición
- Si quieres, puedes incluir el valor de ciertas cookies en el ‘event object’ que genere el cliente. Si es así, es muy importante que estas cookies estén ‘seteadas’ en el mismo dominio que el de tu ‘tagging server’. Si no, el cliente no las va a procesar porque no se van a enviar en la propia petición http.
//API's needed to make this client template work
const claimRequest = require('claimRequest');
const getCookieValues = require('getCookieValues');
const getRequestBody = require('getRequestBody');
const getRequestHeader = require('getRequestHeader');
const getRequestMethod = require('getRequestMethod');
const getRequestPath = require('getRequestPath');
const getRequestQueryParameters = require('getRequestQueryParameters');
const JSON = require('JSON');
const logToConsole = require('logToConsole');
const returnResponse = require('returnResponse');
const runContainer = require('runContainer');
const setResponseHeader = require('setResponseHeader');
const setResponseStatus = require('setResponseStatus');
//API's saved for reuse
const requestBody = getRequestBody();
const requestMethod = getRequestMethod();
const requestPath = getRequestPath();
const requestQueryParameters = getRequestQueryParameters();
//Code starts here
//Logic to check if incoming request origin is allowed to be claimed
let allowedOrigins = data.allowedOrigins.toLowerCase().split(',');
let admitedRequest;
allowedOrigins.forEach((value,index,array)=>{
array[index]= 'https://' + value;
});
allowedOrigins.forEach((value)=>{
if(getRequestHeader("origin") === value){
admitedRequest = true;
}
});
if(requestPath === data.requestPath && admitedRequest === true){
claimRequest();
//Code logic to be executed if http request type is GET
if(requestMethod === 'GET'){
//Set response headers to avoid CORS
setResponseHeader("access-control-allow-credentials", "true");
setResponseHeader("access-control-allow-origin", getRequestHeader("origin"));
//Generate an object from the request url's query parameters
let eventObject = requestQueryParameters;
//Generate an event_name property for within eventObject to run the virtual cntainer instance
eventObject.event_name = requestQueryParameters[data.eventQueryParam] ? requestQueryParameters[data.eventQueryParam] : 'no_event';
//Set aditional properties within eventObject from the incoming http request headers
eventObject.page_referrer = getRequestHeader('referer');
eventObject['x-params-country'] = getRequestHeader('X-Appengine-Country');
eventObject['x-params-city'] = getRequestHeader('X-Appengine-City');
//Uncoment the following line of code and configure to include any additional properties to eventObject from the desired request headers. Must include one line per requesHeader
//eventObject[propertyName] = getRequestHeader('Header name');
//Set aditional properties within eventObject from the incoming http request selected cookies. These cookies must be set at the same domain as the server tagging server
if(data.cookieCheckbox){
let cookies = data.cookieName.split(',');
for(let i = 0; i < cookies.length; i++){
eventObject['x-params-'+ cookies[i]+ '-cookie-value'] = getCookieValues(cookies[i]).toString();
}
}
//If request request anonimization checkbox not checked, include user ip and user agent as properties of eventObject
if(!data.anonymizeCheckbox){
eventObject['x-params-user-Ip'] = getRequestHeader('X-Appengine-User-Ip');
eventObject['x-params-user-agent'] = getRequestHeader('User-Agent');
}
//Generate a new object (containerEventParams) from eventObject (excluding the eventObject[data.eventQueryParam] property that was used to generate the eventObject[event_name] property. This way we avoid object property duplication.
let containerEventParams = {};
for(const property in eventObject ){
if(property !== data.eventQueryParam){
containerEventParams[property] = eventObject[property];
}
}
//Run container
runContainer(containerEventParams, () => returnResponse());
}
//Code logic to be executed if http request type is POST
else if(requestMethod === 'POST'){
//Set response headers to avoid CORS
setResponseHeader("access-control-allow-credentials", "true");
setResponseHeader("access-control-allow-origin", getRequestHeader("origin"));
//Code logic to be executed if POST http request includes a request body
if(requestBody){
//Parse the request body JSON into an object
const body = JSON.parse(requestBody);
//Generate an object from the request url's query parameters
let eventObject = requestQueryParameters;
//Include the request body properties and values into eventObject
for(const property in body){
eventObject[property] = body[property];
}
//Generate an event_name property for within eventObject to run the virtual cntainer instance
eventObject.event_name = requestQueryParameters[data.eventQueryParam] ? requestQueryParameters[data.eventQueryParam] : 'no_event';
//Set aditional properties within eventObject from the incoming http request headers
eventObject.page_referrer = getRequestHeader('referer');
eventObject['x-params-country'] = getRequestHeader('X-Appengine-Country');
eventObject['x-params-city'] = getRequestHeader('X-Appengine-City');
//Uncoment the following line of code and configure to include any additional properties to eventObject from the desired request headers. Must include one line per requesHeader
//eventObject[propertyName] = getRequestHeader('Header name');
//Set aditional properties within eventObject from the incoming http request selected cookies. These cookies must be set at the same domain as the server tagging server
if(data.cookieCheckbox){
let cookies = data.cookieName.split(',');
for(let i = 0; i < cookies.length; i++){
eventObject['x-params-'+ cookies[i]+ '-cookie-value'] = getCookieValues(cookies[i]).toString();
}
}
//If request request anonimization checkbox not checked, include user ip and user agent as properties of eventObject
if(!data.anonymizeCheckbox){
eventObject['x-params-user-Ip'] = getRequestHeader('X-Appengine-User-Ip');
eventObject['x-params-user-agent'] = getRequestHeader('User-Agent');
}
//Generate a new object (containerEventParams) from eventObject (excluding the eventObject[data.eventQueryParam] property that was used to generate the eventObject[event_name] property. This way we avoid object property duplication.
let containerEventParams = {};
for(const property in eventObject ){
if(property !== data.eventQueryParam){
containerEventParams[property] = eventObject[property];
}
}
//Run container
runContainer(containerEventParams, () => returnResponse());
}
//Code logi to be executed if POST request does not include a request body
else if(!requestBody){
//Generate an object from the request url's query parameters
let eventObject = requestQueryParameters;
//Generate an event_name property for within eventObject to run the virtual cntainer instance
eventObject.event_name = requestQueryParameters[data.eventQueryParam] ? requestQueryParameters[data.eventQueryParam] : 'no_event';
//Set aditional properties within eventObject from the incoming http request headers
eventObject.page_referrer = getRequestHeader('referer');
eventObject['x-params-country'] = getRequestHeader('X-Appengine-Country');
eventObject['x-params-city'] = getRequestHeader('X-Appengine-City');
//Uncoment the following line of code and configure to include any additional properties to eventObject from the desired request headers. Must include one line per requesHeader
//eventObject[propertyName] = getRequestHeader('Header name');
//Set aditional properties within eventObject from the incoming http request selected cookies. These cookies must be set at the same domain as the server tagging server
if(data.cookieCheckbox){
let cookies = data.cookieName.split(',');
for(let i = 0; i < cookies.length; i++){
eventObject['x-params-'+ cookies[i]+ '-cookie-value'] = getCookieValues(cookies[i]).toString();
}
}
//If request request anonimization checkbox not checked, include user ip and user agent as properties of eventObject
if(!data.anonymizeCheckbox){
eventObject['x-params-user-Ip'] = getRequestHeader('X-Appengine-User-Ip');
eventObject['x-params-user-agent'] = getRequestHeader('User-Agent');
}
//Generate a new object (containerEventParams) from eventObject (excluding the eventObject[data.eventQueryParam] property that was used to generate the eventObject[event_name] property. This way we avoid object property duplication.
let containerEventParams = {};
for(const property in eventObject ){
if(property !== data.eventQueryParam){
containerEventParams[property] = eventObject[property];
}
}
//Run container
runContainer(containerEventParams, () => returnResponse());
}
}
}
Para usar el código puedes copiar y pegarlo al crear tu plantilla de cliente en tu contenedor de GTM server-side. También puedes descargar el archivo .tpl que encontrarás en el repositorio que he creado en mi perfil de Github.com para este desarrollo. No te olvides de configurar los permisos de tu ‘custom template’ antes de guardarlo.
Campos a configurar para usar el cliente
Una vez hayas usado el anterior código para crear un client custom template en tu contenedor de Google Tag Manager server-side, estos son los campos que tendrás que configurar para poder usarlo:
Bien, ahora una foto del mismo cliente una vez configurado:
La configuración que ves en la anterior foto es la que voy a usar para mostrarte el funcionamiento del cliente. Cuando lo hayas configurado tú, tendrás que entrar en modo ‘preview’ en tu contenedor de GTM server-side. Listo, tu cliente está preparado para interceptar peticiones. Ahora toca enviar estos ‘requests’.
El cliente en funcionamiento: envío de un request por http de tipo GET a Google Tag Manager server-side...
Puedes enviar una petición por http a tu contenedor de GTM server-side desde cualquier servicio, como por ejemplo Postman o tu propio navegador. Para ilustrar este post yo lo voy a hacer desde un contenedor web de Google Tag Manager. Para ello he desarrollado dos scripts que te muestro a continuación. Tanto estos scripts como el propio cliente están preparados para evitar fallos CORS. Ten esto en cuenta si tú lanzas tu petición desde otro origen diferente. Y como siempre, te recomiendo que veas el siguiente vídeo: ‘CORS in 100 seconds’, de Fireship. Explica este fenómeno muy bien.
Fíjate, este el código que he desarrollado para lanzar una petición http de tipo GET desde mi contenedor de GTM de tipo web a mi servidor de ‘tagging’:
<script>
(function() {
var httpRequest = new XMLHttpRequest();
httpRequest.withCredentials = true;
httpRequest.open('GET','https://<your end point goes here>/prueba?en=addToCart&price=25&sku=87098&category=books&product_name=test-product&user_type=returning',true);
httpRequest.withCredentials = true;
httpRequest.send();
})();
</script>
...y envío de un request por http de tipo POST
<script>
(function () {
var httpRequest = new XMLHttpRequest();
var bodyObject = {testProperty1:'testValue1',testProperty2:'testValue2',testProperty3:'testValue3',testProperty4:'testValue4',testProperty5:'testValue5', testProperty6:'testValue6'};
httpRequest.withCredentials = true;
httpRequest.open('POST','https://<your end point goes here>/prueba?en=test_event&user_type=new&category=testing&page_name=test-page',true);
httpRequest.send(JSON.stringify(bodyObject));
})();
</script>
Reflexiones finales
Google Tag Manager (GTM) server-side es en verdad un cambio de paradigma en lo que se refiere a este gestor de etiquetas. Tu servidor de tagueo puede -en efecto- ser un proxy para derivar hits a herramientas GA4 o Facebook. Pero puede ser mucho más que eso. Puedes convertir tu implementación de GTM server-side en un verdadero generador de API’s, por ejemplo.
Sea el que sea el uso que des a GTM server-side, el cliente siempre va a ser el epicentro de ello.
Es muy posible que jamás uses este desarrollo en tus implementaciones. Es mucho más operativo seguir las directrices de Google y explotar los beneficios de enviar a GTM server-side un flujo de datos de GA4 que luego aprovechar. Siendo esto así, ganas mucho si te sumerges en las tripas de un cliente y entiendes -de verdad- cómo funciona esta nueva pieza de Google Tag Manager.