Skip to main content

Tagging y Obfuscación GA4

Cómo un evento GA4 viaja desde el browser del usuario hasta el contenedor sGTM, evitando ad-blockers en cada paso.

El problema que resuelve

Los ad-blockers (Brave, uBlock Origin, AdGuard) bloquean peticiones a dominios conocidos de Google:

||googletagmanager.com^
||google-analytics.com^
||analytics.google.com^

Si el sitio web hace una request directa a www.googletagmanager.com o www.google-analytics.com/g/collect, esa request es bloqueada antes de salir del browser.

Crimoo resuelve esto en dos capas:

  1. Custom Loader: sirve el script de GTM desde el dominio propio del cliente
  2. GA4 Bypass: intercepta y re-enruta los hits de GA4 a través del proxy

Flujo completo


crimoo.js — el script compuesto

/crimoo.js es el punto de entrada. El proxy lo genera dinámicamente con CustomLoaderService.generateFullScript() y tiene dos partes:

Parte 1: GTM Bootstrap

Un script estándar de GTM modificado para apuntar al dominio propio en vez de www.googletagmanager.com:

// GTM estándar de Google apuntaría a:
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-NKDCS9NQ';

// crimoo.js apunta a:
j.src = 'https://rc-XXXX.crimoo.com/assets/ce7a90';
// El proxy añade ?id=GTM-NKDCS9NQ server-side antes de ir a Google

El browser solo ve el dominio de Crimoo — nunca googletagmanager.com.

Parte 2: Identity Script (obfuscado)

Parchea las tres APIs de red del browser para interceptar eventos GA4:

// Intercepta fetch
window.fetch = function(url, opts) {
url = rp(url); // rp() reemplaza /g/collect con path obfuscado
url = ap(url); // ap() añade _iou y _ious + obfusca query params
return originalFetch(url, opts);
}

// Intercepta XMLHttpRequest
XMLHttpRequest.prototype.open = function(method, url) {
return originalOpen(method, rp(ap(url)));
}

// Intercepta sendBeacon
navigator.sendBeacon = function(url, body) {
return originalSendBeacon(rp(ap(url)), body);
}

Obfuscación en tres niveles

Nivel 1 — Path obfuscation (rp())

La función rp() reemplaza /g/collect con un path aleatorio del array _bp (bypass paths):

var _bp = ["/opt/927d", "/data/e3a1", "/metrics/f9c2"]; // configurado en ObfuscationConfig
var _gc = '/' + 'g' + '/' + 'co' + 'llect'; // /g/collect en partes (anti-static-analysis)

function rp(url) {
if (!url || !_bp.length) return url;
var i = url.indexOf(_gc);
// Solo intercepta si la URL contiene /g/collect Y el dominio de Crimoo
if (i === -1 || url.indexOf(DOMAIN) === -1) return url;
// Reemplaza /g/collect con path aleatorio del array
return url.substring(0, i) + _bp[Math.floor(Math.random() * _bp.length)] + url.substring(i + _gc.length);
}

Por qué importa: los ad-blockers también bloquean /g/collect como path conocido de GA4. Con un path aleatorio como /opt/927d/XXXX, la URL no matchea ninguna lista negra.

El path también incluye el container ID (3mxuoufac355d49acdf001) para que el proxy sepa a qué GTM pertenece el evento.

Nivel 2 — Query param obfuscation (eq())

Los query params de GA4 son reconocibles (v=2&en=page_view&tid=G-...). La función eq() los codifica en base64url bajo el param _hn:

var _qn = '_hn'; // query param name configurado en ObfuscationConfig

function eq(url) {
var qi = url.indexOf('?');
if (qi === -1) return url;
var base = url.substring(0, qi);
var qs = url.substring(qi + 1); // "v=2&en=page_view&tid=G-..."
var enc = btoa(qs).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
return base + '?' + _qn + '=' + enc;
// resultado: /opt/927d/XXXX?_hn=djI9Mi....
}

Request final que ve la red:

GET /opt/927d/3mxuoufac355d49acdf001?_hn=djI9Mn...

Un ad-blocker que inspeccione el tráfico no puede determinar que es un evento GA4.

Nivel 3 — API path obfuscation (ObfuscatedPathRewriter)

Además de los hits de GA4, los paths de API interna también se ofuscan para evitar detección por nombre:

Path ofuscadoPath realUso
/svc/a3f2/{containerId}/api/capture-config/{containerId}Config de captura pública
/dat/b7e1/api/iIdentity resolution

Estos paths se configuran en ObfuscationConfig como captureConfigPath e identityApiPath. A diferencia de los bypass paths de GA4, estos se reescriben en el proxy y se redirigen al management app (:8000) via ForwardingHandler, no al container Docker.


Decodificación en el proxy (Ga4BypassService)

El proxy recibe la request obfuscada y la restaura antes de publicar a Pub/Sub:

// 1. Restaurar path: /opt/927d/XXXX → /g/collect
String restoredPath = ga4BypassService.restorePath(modifiedPath, config);

// 2. Decodificar query params: _hn=base64 → v=2&en=page_view&tid=G-...
String realQuery = ga4BypassService.deobfuscateQueryParams(query, config);

El TaggingEvent que llega a gtm-fabric (y luego al sGTM) tiene el path y params originales de GA4 — el sGTM recibe exactamente lo mismo que recibiría de Google directamente.


Preview con ofuscación

Cuando un browser está en modo preview de GTM, los requests siguen pasando por el identity script (rp + eq + ap). La URL que llega al proxy está ofuscada igual que en producción.

La diferencia es que el proxy de-ofusca primero y luego detecta el modo preview. El container recibe el request limpio, como si viniera directo de Google:

Puntos clave:

  • La de-ofuscación ocurre antes de la decisión sync/async — el path y params ya están restaurados cuando se evalúa si es preview
  • El container nunca ve paths ofuscados — recibe /g/collect?v=2&... como si el request viniera de Google directamente
  • La única diferencia con el flujo normal es que preview usa proxyDirect (sync, espera response) en vez de proxyAsync (202 + Pub/Sub)

ObfuscationConfig — dónde se configura

Guardado en gtm_custom_loaders.obfuscation_config (JSONB en PostgreSQL):

{
"cookieName": "_iou",
"globalObject": "_wd",
"apiPath": "/opt/927d",
"debugKey": "dbg_mlta9i9y",
"bypassPaths": ["/assets/82c091", "/resources/a81c31", "/chunks/97d79d", "/bundles/1c9acf", "/content/586160"],
"gtagBypassPath": "/assets/ce7a90",
"queryParam": "_hn",
"captureConfigPath": "/svc/a3f2",
"identityApiPath": "/dat/b7e1",
"varNames": {
"CU": "hz",
"CS": "iw",
"DOMAIN": "kwy",
"CID": "djg",
"CONFIG_URL": "yph",
"API_URL": "azh"
}
}
CampoUso
cookieNameNombre base de las cookies de identidad (device: _iou, session: _ious)
globalObjectNombre del objeto global JS que almacena el estado del identity script
apiPathPath base para los endpoints de API en el identity script
debugKeyKey para activar el modo debug desde query params o localStorage
bypassPathsPaths alternativos para /g/collect. Se elige uno aleatoriamente en cada hit
gtagBypassPathPath que sirve el script de GTM (/gtm.js) desde el dominio propio
queryParamNombre del param que envuelve los query params codificados en Base64
captureConfigPathPath ofuscado para /api/capture-config (prefix match)
identityApiPathPath ofuscado para /api/i (exact match)
varNamesMapa de nombres ofuscados de variables JS (CU, CS, DOMAIN, etc.)

El gtagBypassPath (/assets/ce7a90)

Este path tiene una sola responsabilidad: servir el script de GTM desde el dominio propio.

Browser: GET https://rc-XXXX.crimoo.com/assets/ce7a90
Proxy: GET https://www.googletagmanager.com/gtm.js?id=GTM-NKDCS9NQ

El ?id=GTM-NKDCS9NQ es añadido server-side en ProxyHandler.handleCustomLoader():

if (customLoaderService.isGtagBypassPath(path, container.getObfuscationConfig())) {
String gtmLoaderPath = "/gtm.js?id=" + container.getWebGtmId();
return proxyToGoogle(request, response, gtmLoaderPath);
}

El browser solo ve /assets/ce7a90 — nunca googletagmanager.com. Google responde con el script GTM estándar, que el proxy devuelve tal cual al browser.


Identity params: _iou y _ious

Además de obfuscar los params de GA4, el identity script añade dos params propios:

ParamCookieTTLDescripción
_iou_iou730 díasDevice ID único (persistente)
_ious_ious30 minSession ID (se renueva por sesión)

Estos params permiten a Crimoo hacer identity resolution y linkear eventos GA4 con contactos del CRM, incluso sin login.

Request GA4 real que llega al sGTM:

POST /g/collect?v=2&tid=G-FSDYKDL52D&en=page_view&cid=1798635578&..._iou=d1051-9e8f...&_ious=s0072-fbff...

Por qué el sGTM no modifica el script

El flujo original intentaba que el sGTM sirviera el script GTM (en vez de Google), bajo la premisa de que el sGTM lo modificaría para apuntar a sí mismo como servidor. Sin embargo:

  1. El sGTM solo puede servir scripts para web containers explícitamente vinculados a él en el GTM UI
  2. El identity script ya maneja todo el routing de eventos independientemente
  3. Si el sGTM reinicia y no puede descargar su configuración de Google, devuelve 400 — rompiendo la carga de GTM en el browser

Por eso gtagBypassPath va directamente a Google. El sGTM se dedica únicamente a procesar los hits que le llegan vía Pub/Sub.