En este post voy a explorar una implementación en la que llevo pensando mucho tiempo: usar la API de Firestore desde Google Tag Manager (GTM) Server-Side para generar una base de datos de tus usuarios. El objetivo es actualizar esta base de datos en tiempo real con información de las compras de cada uno de estos users para después leer estos datos y exponerlos en el dataLayer de Google Tag Manager. Todo esto, como te digo, en real-time. A mi me gusta pensar en este desarrollo como una especie de conato de Customer Data Platform (CDP), ya que te va a permitir trabajar con una foto individualizada de cada uno de tus usuarios en base a su comportamiento.
Firestore como base de datos a nivel de usuario que se actualiza en tiempo real
La implementación que desarrollo en este post se sustenta en Firestore, una base de datos NoSQL que pertenece a la suite de herramientas de Firebase. La estructura de estos databases se basa en colecciones que aglutinan documentos (más adelante verás esto en la práctica), y cada uno de estos documentos se puede explotar para alojar información a nivel muy segmentado: transacciones, registros, usuarios, etc. Esto es perfecto para registrar un histórico de datos de cada uno de tus usuarios: cuándo fue la última vez que visitó tu site, cuántas compras ha hecho, cuántos productos ha añadido a su carrito de la compra, qué importe total ha gastado en tu ecommerce, etc.
Firestore expone una API que te permite trabajar con estas bases de datos desde GTM Server-Side. Esto es algo que ya exploró el gran Simo Ahava hace algunos años en su post ‘Enrich Server-side data with Cloud Firestore’. Así, puedes crear, actualizar y leer estos documentos de Firestore desde tu servidor de tagging. Esto tiene mucho potencial: te permite enriquecer tus flujos de datos con todos estos datos…en tiempo real.
Partiendo de esta situación, en este desarrollo voy a hacer lo siguiente:
- He creado una colección de Firestore en un proyecto de Firebase. Voy a aprovechar la implementación de Enhanced Ecommerce de GA4 de este blog para que cada vez que se produzca una transacción (el evento
purchase
tiene lugar cuando se lee un post por completo), se genere un documento nuevo en la colección con información de dicha transacción. - El documento estará asociado a cada usuario simulando un
userID
, de tal manera que si el usuario tiene transacciones anteriores, el documento ya existente se actualizará - Cada vez que un usuario acceda a una ficha de producto (un post) leeré su documento en la colección de Firestore y expondré su información en el dataLayer
- Trabajaré con tres variables que me darán una foto más o menos completa del usuario: histórico de productos comprados, número de compras e importe total gastado.
Crea un proyecto de Firebase y habilita Cloud Firestore
Para trabajar con Cloud Firestore, tienes que crear un proyecto en Firebase. Al hacerlo, puedes vincularlo con un proyecto que tengas dado de alta en Google Cloud Platform (GCP). De lo contrario el proyecto de Firebase se reflejará de forma automática en GCP.
El siguiente paso es crear un Firestore Database, algo que debes hacer directamente desde Firebase. Firestore es una aplicación de pago, por lo que llegado a este punto tendrás que vincular tu proyecto de Firebase a una cuenta de facturación que tengas habilitada en Google Cloud Platform. Cuando llegues a este paso, ya solo queda crear una colección, que será la que aglutine todos los documentos que se vayan creando con cada purchase de los usuarios. Yo he nombrado mi colección transactions:
Todo este proceso es bastante sencillo, pero conviene que sepas qué estás haciendo. Firestore es, al fin y al cabo, una herramienta de pago. A continuación te dejo unos recursos que seguro te sirven de ayuda:
- Cloud Firestore Pricing | Get to know Cloud Firestore #3 Un vídeo del canal oficial de Firebase en YouTube donde se explica bien cómo y por qué se paga al usar Firestore
- ‘Firestore Pricing’, la documentación oficial de Google Cloud Platform sobre los costes de Firestore.
- ‘Get to know Cloud Firestore‘. Un playlist de vídeos sobre Firestore del canal de oficial de Firebase en YouTube.
- La documentación oficial de Cloud Firestore.
El desarrollo a desplegar desde Google Tag Manager (GTM)
Llegados a este punto tienes que ponerte manos a la obra a desarrollar la lógica que se encargará de:
- Crear un documento (o actualizar uno ya existente) en tu colección de Firestore cada vez que se produzca una transacción
- Cuando un usuario que ya ha hecho una compra acceda de nuevo a tu site, leer su documento correspondiente en Firestore y exponer esa información en el dataLayer de GTM
- Todo esto debe suceder en tiempo real, de tal manera que el dataLayer de Google Tag Manager siempre tenga la información actualizada de cada documento de Firestore de cada usuario
Fíjate, a continuación te detallo esta implementación a través de un diagrama:
Por resumir el anterior diagrama:
- Lanzaré una llamada desde el contenedor de Google Tag Manager implementado en mi site a mi contenedor de GTM Server-Side cada vez que un usuario realice una compra (recuerda que el evento
purchase
tiene lugar cuando un usuario lee un post completo). - A continuación, desde GTM Server-Side se llamará a la API de Firestore para crear un documento con la información de esta transacción.
- Cada vez que un usuario acceda a una ficha de producto (un post en este caso), se lanzará una llamada por http desde mi contenedor de GTM Client-Side a mi contenedor Server-Side. Se lanzará entonces una llamada a la API de Firestore para leer el documento asociado al usuario. El contenido de este documento se devolverá al navegador del usuario, desde donde se empujará al dataLayer de Google Tag Manager.
Crear (o actualizar) un documento en Firestore desde Google Tag Manager (GTM) con cada transacción
A partir de ahora te voy a ir desglosando, pieza a pieza, todos los elementos de esta implementación. He creado un repositorio en GitHub desde donde puedes descargar y usar el código que te voy a explicar a continuación. Ten en cuenta que para trabajar con los clientes que he desarrollado para Google Tag Manager Server-Side tendrás que descargar los correspondientes archivos .tpl e importarlos a tu contenedor desde la sección de Custom templates (todo esto está explicado en el archivo Readme del repositorio de GitHub).
Te voy a empezar explicando la implementación que he desarrollado para crear (o actualizar) un documento en Firestore con cada evento purchase
. Recuerda que este blog cuenta con una implementación de EEC de GA4, y que el evento purchase
tiene lugar cuando se lee un post por completo. Cuando esto suceda, voy a enviar un request por http desde el contenedor de Google Tag Manager que tengo implementado en mi site a mi contenedor de GTM Server-Side. Para ello usaré el siguiente javascript, que implementaré a través de un CHTML Tag:
<script>
(function(){
try{
//Fetch API
//Including GTM Server-side endpoint url and dummy userID
fetch('https://yourserversideendpoint.com/test?userID=0002',{
'method': 'POST',
'credentials': 'include',
//Populate variables to be included in body of POST
'body':JSON.stringify({
purchased_items:{{DLV - purchased_items}},
total_purchases:{{CJS - total_purchases populator}},
total_spent:{{CJS - total_spent_populator}}
})
})
//Handle response from fetch promise
.then(function(response){
return response.text()
});
}
catch(error){}
})();
</script>
Como ves, se trata de una llamada POST que he montado usando el Fetch API. Podría haber desarrollado una llamada de tipo XMLHttpRequest, pero trabajar con fetch
me parece más sencillo, sobre todo para manejar promesas. Fíjate en los headers de la llamada, como ves he añadido el parámetro credentials
con valor 'include'
. Esto es así porque en la comunicación entre navegador y servidor tendrás que lidiar con CORS, algo de lo que ya he escrito mucho en otros posts. Por otra parte, al ser una llamada POST aprovecho su body para transportar a GTM Server-Side las variables que quiero, a su vez, llevar a Firestore: purchased_items
, total_purchases
y total_spent
. Para informar estas variables me apoyo en el dataLayer y en unos Custom Javascript Variables, pero puedes popularlas de la manera que más oportuna te sea. Por otra parte, fíjate en que envío un userID
(ficticio, por cierro) en un queryString en la url de esta llamada. Usaré este identificador como nombre para crear el documento correspondiente en Firestore. Esto se puede customizar usando cualquier otro identificador, pero siempre debe ser uno asociado al usuario.
Cuando esta llamada llega a mi servidor de tagging, la intercepta un cliente que he desarrollado para este caso de uso. Lo he llamado ‘Firestore writer client’, y procesará el contenido del body de la llamada entrante para después llamar a la API de Firestore y grabar esta información en un documento dentro de la colección transactions que ya había creado previamente. Este es el código del cliente:
//Necessary Client API's
const claimRequest = require('claimRequest');
const Firestore = require('Firestore');
const getRequestBody = require('getRequestBody');
const getRequestHeader = require('getRequestHeader');
const getRequestMethod = require('getRequestMethod');
const getRequestPath = require('getRequestPath');
const getRequestQueryParameter = require('getRequestQueryParameter');
const JSON = require('JSON');
const returnResponse = require('returnResponse');
const setResponseBody = require('setResponseBody');
const setResponseHeader = require('setResponseHeader');
const setResponseStatus = require('setResponseStatus');
//If client intercepts valid incoming http resquest...claim it!
if(getRequestPath() === data.requestPath && getRequestHeader('origin') === data.requestOrigin && getRequestMethod() === 'POST'){
claimRequest();
//Store userID queryParam in const
const userID = getRequestQueryParameter('userID');
//Set response headers
setResponseHeader("access-control-allow-credentials", "true");
setResponseHeader("access-control-allow-origin", getRequestHeader("origin"));
//Turning requestBody JSON string into an object and storing it in input CONST
const input = JSON.parse(getRequestBody());
//Start working with the Firestore API
Firestore.write(data.collection + '/' + userID, input, {
projectId: data.projectId,
merge: true,
})
//Handle promise response
.then(function(response){
return response;
})
//Return response to client that sent the request
.then(function(response){
setResponseBody('I have written to the following Firestore document: ' + response);
setResponseStatus(200);
returnResponse();
});
}
Como te decía antes, el documento recibirá el nombre del userID
que se envía en el request por http entrante, y esto es justo lo que ata cada documento a un usuario en particular. Si es un usuario recurrente, el documento de Firestore que ya existe se actualizará con los datos de la nueva transacción. Si por el contrario es un usuario nuevo, se creará un nuevo documento con esta misma información. De esta manera, la colección de Firestore estará siempre actualizada.
El cliente ‘Firestore writer client’ necesita una serie de datos para su configuración:
- Origen de la llamada (
https://analyticsimplementations.com
en mi caso) - Ruta de la llamada (
/test
en mi caso) - Nombre del proyecto de Google Cloud en el que se aloja la colección de Firestore
- Nombre de la colección en la que escribir el document
- Nombre del documento (
userID
). Este identificador no se configura desde el client template, sino desde el propio código del cliente, en la línea 22. Siendo así, si quisieras apoyarte en otro identificador, tendrías que actualizar el código del cliente para adaptarlo al que mandaras desde GTM Client-side.
Estos últimos tres parámetros (nombre de proyecto de Google Cloud, nombre de colección y nombre de documento) son los necesarios para trabajar con la API de Firestore desde GTM Server-Side. Es importante que a la hora de configurar este client template desde el editor de plantillas de tu contenedor de GTM Server-Side, configures también estos permisos de manera consecuente:
Si no especificas un nombre de proyecto de Google Cloud, la API buscará la colección de Firestore en el mismo proyecto en el que hayas desplegado tu contenedor de Google Tag Manager Server-Side. En mi caso he preferido tener dos proyectos de Cloud diferentes: uno para Firebase – Firestore (que he llamado ‘Test project’) y otro para GTM Server-Side. Por esta razón, he tenido que dar acceso a mi servidor de tagging a mi colección de Firestore a través de un Service Account con el rol de Usuario de Cloud Datastore.
Leer los documentos de Firestore para exponer la información de cada usuario en el dataLayer de Google Tag Manager
Ya tienes un documento creado en tu colección de Firestore que contiene la información del purchase que acaba de hacer tu usuario. El objetivo ahora es que cada vez que ese usuario acceda a una ficha de producto (en este blog cada post es una ficha de producto), el dataLayer de Google Tag Manager se actualice con esta información. El proceso en este caso es similar. Cuando un usuario acceda a un post, desde mi contenedor de GTM Client-Side se lanzará una llamada (de nuevo usando el Fetch API) de tipo GET a mi contenedor de GTM Server-Side. Esta llamada contendrá en su url un queryString con el userID
del usuario, que como verás un poco más adelante se usará para buscar el documento de Firestore que contiene todas las transacciones de ese user. Este es el código de la llamada que lanzo esde GTM Client-Side:
<script>
(function(){
try{
//Store dataLayer in local variable
var dataLayer = window.dataLayer || [];
//Fetch API
//Including GTM Server-side endpoint url and dummy userID
fetch('https://yourserversideendpoint.com/test?userID=0002',{
'method': 'GET',
"credentials": "include"
})
//Handle response from fetch promise
.then(function(response){
//If Firestore document doesn't exist, Firestore API returns a 404 error
if(response.status === 404){
throw new Error('User with no transactions')
}
return response.json();
})
//Treat promise response and push to dataLayer
.then(function(response){
console.log(response.data);
var object = response.data;
object.event = 'firestore_response_ready'
dataLayer.push(object);
})
//Handle error response and push empty variables to dataLayer
.catch(function(response){
dataLayer.push({
purchased_items:[],
total_purchases:0,
total_spent:0,
event:'firestore_response_ready'
})
})
;
}catch(error){}
})();
</script>
¿Te has dado cuenta de que en la línea 15 del código estoy arrojando un error que después manejo de manera consecuente en la línea 28? Esto es así porque la API de Firestore en GTM Server-Side rechaza la promesa esperada si el documento que se está buscando en la colección no existe. Esto supone que si el usuario no ha hecho ninguna compra todavía y por lo tanto no existe un documento de Firestore asociado al mismo, la llamada Fetch que te ilustro en el código de arriba recibirá un error. Esto me viene como anillo al dedo para cubrir el caso de uso de los usuarios que no han realizado ninguna compra todavía. Manejando el error puedo ejecutar un dataLayer.push()
para exponer en el dataLayer las variables purchased_items
, total_purchases
y total_Spent
vacías. De esta manera, si este usuario termina haciendo una compra, estas variables se popularán con los datos de dicha compra y se creará un documento nuevo en mi colección de Firestore correctamente configurado.
Bien, cuando esta llamada de tipo GET llega a mi contenedor de GTM Server-Side, la intercepta otro cliente que también he desarrollado para tal efecto y que llamado ‘Firestore fetcher client’. Este cliente llama a la API de Firestore para leer el documento cuyo nombre coincide con el userID
entrante (y que por lo tanto está asociado al usuario) en mi colección y devolver el contenido de dicho documento en una promesa que procesa la llamada Fetch que he implementado en cliente. Esta llamada Fetch procesará esta respuesta y la empujará al dataLayer. Puesto que en esta comunicación cliente-servidor-Firestore entran en juego las promesas, incluyo en el dataLayer.push()
un event
con valor 'firestore_response_ready'
. Como verás más adelante, aprovecho este parámetro para tener la certeza de que la información del documento de Firestore ya está disponible en el dataLayer de Google Tag Manager. Son muchas cosas, lo sé.
Volviendo al cliente ‘Firestore fetcher client‘ desplegado en GTM Server-Side, este es su código:
//Necessary Client API's
const claimRequest = require('claimRequest');
const Firestore = require('Firestore');
const getRequestHeader = require('getRequestHeader');
const getRequestMethod = require('getRequestMethod');
const getRequestPath = require('getRequestPath');
const getRequestQueryParameter = require('getRequestQueryParameter');
const JSON = require('JSON');
const returnResponse = require('returnResponse');
const setResponseBody = require('setResponseBody');
const setResponseHeader = require('setResponseHeader');
const setResponseStatus = require('setResponseStatus');
//If client intercepts valid incoming http resquest...claim it!
if(getRequestPath() === data.requestPath && getRequestHeader('origin') === data.requestOrigin && getRequestMethod() === 'GET'){
claimRequest();
//Store userID queryParam in const
const userID = getRequestQueryParameter('userID');
//Config response headers
setResponseHeader("access-control-allow-credentials", "true");
setResponseHeader("access-control-allow-origin", getRequestHeader("origin"));
//Execute Firestore.read() API
Firestore.read(data.collection + '/' + userID, {
projectId: data.projectId,
})
.then(function(result){
return result;
})
.then(function(result){
setResponseBody(JSON.stringify(result));
setResponseStatus(200);
returnResponse();
});
}
Al igual que sucede con el anterior cliente que he desarrollado para esta implementación, a la hora de configurar este ‘Firestore fetcher client‘ en tu contendor de GTM Server-Side, hay una serie de parámetros que son obligatorios:
- Origen de la llamada (de nuevo
https://analyticsimplementations.com
en mi caso) - Ruta de la llamada (
/test
, al igual que antes) - Nombre del proyecto de Google Cloud en el que se aloja la colección de Firestore con la que estás trabajando
- Nombre de la colección de la que quieres recuperar el documento
- Nombre del documento (
userID
). Y de nuevo, este último parámetro (userID
) no se configura desde el Client template, sino que se establece directamente en el código de dicho cliente, en la línea 20. Ya sabes que si quisieras apoyarte en otro identificador, tendrías que actualizar el código del cliente para adaptarlo al que mandaras desde GTM Client-side.
Es importante que a la hora de configurar este cliente tengas en cuenta los permisos del mismo. Puesto que estás trabajando con la API de Firestore, tendrás que configurar estos permisos de manera consecuente, haciendo constar el nombre del proyecto de Google Cloud en el que está tu colección de Firestore así la ruta de dicha colección:
La implementación en funcionamiento, de principio a fin: de GTM Client-Side a Firestore, pasando por GTM Server-Side
Hasta ahora te he ido explicando todos los pasos de esta implementación uno a uno. ¡Enhorabuena si has llegado hasta aquí! Ha llegado el momento de mostrártela en funcionamiento. Voy a empezar simulando el caso de uso de un usuario que realiza una compra. Es un usuario nuevo, por lo que no ha realizado ninguna transacción previa. El user visualiza una ficha de producto, y se ejecuta el script que he desarrollado (implementado a través de un CHTML Tag) para lanzar una llamada de tipo GET a mi contenedor de GTM Server-Side. Usaré un userID
simulado con valor 0001. Date cuenta de que recurro a una constante para alojar la url de mi servidor de GTM Server-side y de que el trigger que asigno al CHTML Tag hace que sólo se ejecute en los posts de este blog (fichas de producto en la implementación de Enhanced Ecommerce de GA4 que tengo desplegada). Por lo demás, el código es el mismo que has podido ver un poco más arriba.
La llamada es recibida en mi contenedor de GTM Server-Side, en donde el cliente ‘Firestore fetcher client’ la intercepta, la procesa y lanza una llamada a la API de Firestore en busca de un documento en la colección cuyo nombre coincida con el userID
0001. Como no hay ninguno, la API devuelve un error que el script que he implementado en GTM Client-side maneja de manera consecuente para empujar al dataLayer las variables purchased_items
, total_purchases
y total_spent
vacías y preparadas para se informadas en caso de producirse una transacción. Este dataLayer.push()
incorpora, además, la variable event con un valor ‘firestore_response_ready’, que es mi testigo para saber que la respuesta de la API de Firestore se ha completado.
userID
0001 termina haciendo una transacción (recuerda que en este blog el evento purchase
se ejecuta cuando un usuario lee un post por completo). En este instante, se ejecuta una llamada de tipo POST desde mi contenedor de GTM Client-side, que envía a mi contenedor de GTM Server-side información sobre la compra realizada. Una vez más, implemento el script a través de un CHTML Tag, que en este caso tiene un trigger asignado para que se ejecute sólo en el evento purchase
) Esta llamada es ahora interceptada y procesada por el cliente ‘Firestore writer client‘, que lanza una llamada a la API de Firestore para actualizar el documento cuyo nombre coincida con el userID
0001 con la información de esta compra. si este documento no existe, se creará.
userID
0001, su correspondiente documento 0001 se crea en la colección ‘transactions’ de Firestore con la información de esta transacción, fíjate: userID
0001 vuelve a este blog y lee otro post. Entraría en juego la misma lógica que te acabo de explicar, pero con una diferencia: el usuario ya es recurrente y por lo tanto ya tiene un documento asociado en mi colección de Firestore. Siendo así, esta información se expondría en el dataLayer de Google Tag Manager (GTM) cuando el usuario accediera a un post. Fíjate que, una vez más, el método dataLayer.push()
incluye la variable event
con valor ‘firestore_response_ready’ para manejar bien la respuesta de la API de Firestore: ¿Y que sucedería si el userID
0001 leyera por completo el post y por lo tanto completara otra transacción? Que el documento 0001 de la colección de Firestore se actualizaría de nuevo con esta información, fíjate:
¿Ves ahora el potencial que tiene esta implementación? Gracias a que puedes llamara la API de Firestore desde Google Tag Manager Server-side, puedes disponer del histórico de transacciones de cada uno de tus usuarios en el dataLayer de GTM, listo para ser aprovechado. Te vuelvo a incluir una nueva captura de pantalla de este documento 0001 después de que este usuario haya realizado dos compras más:
Fíjate el nivel de granularidad que puedes conseguir por usuario: artículos comprados, número de transacciones, importe gastado, etc. Bajo mi punto de vista esto es algo realmente increíble.