gtm-proxy
Reverse proxy HTTP/HTTPS de alto rendimiento. Es el punto de entrada de todos los eventos de tagging del cliente final.
Nota: El servicio
gateway(Go + Echo) existe en el repositorio pero no forma parte del flujo de tagging en producción. El flujo real pasa porgtm-proxy.
Rol de gtm-proxy en el sistema
gateway (Go + Echo)
Endpoints
| Método | Path | Descripción |
|---|---|---|
| GET | /health | Health check |
| GET | /gtm.js?id=GTM-XXXX | Servir custom loader script |
| GET | /gtm/load.js | Proxy a Google GTM |
| POST/GET | /collect | Capturar evento de tagging |
| POST/GET | /g/collect | Capturar evento GA4 |
| POST | /api/i | Identity resolution |
| GET | /api/capture-config/:gtmId | Config de captura pública |
| POST | /api/cache/invalidate | Invalidar Redis cache |
Flujo de decisión — /collect
Identity Resolution — /api/i
gtm-proxy (Java 21 + Netty)
Routing decision tree completo
GA4 Bypass (AdBlock evasion)
gtm-proxy permite configurar paths obfuscados para evitar ad-blockers. Hay dos tipos de paths:
gtagBypassPath — sirve el script de GTM desde el dominio propio:
bypassPaths — reciben los hits de GA4 obfuscados:
Ver Tagging y Obfuscación para el flujo completo.
⚠️ Nota sobre el log
↳ ga4_bypass:— este log NO indica que el evento fue bypassed a Google. Solo indica que elGa4BypassServicedeobfuscó el path obfuscado al/g/collectoriginal. La decisión sync vs Pub/Sub se toma DESPUÉS, en el bloque siguiente delProxyHandler.
Forwarding del CAPI Gateway (/capi/*)
Cuando el browser hace POST /capi/meta/v24.0/{pixelId}/events contra api.crimoo.com, gtm-proxy enruta el request a gtm-fabric vía ForwardingHandler (no Pub/Sub):
Headers que viajan en la cadena:
X-Crimoo-Token— autentica el container (validado porGatewayTokenFilter)X-Meta-Access-Token— token del pixel Meta, reenviado agraph.facebook.com
Ver CAPI Gateway para el flujo completo.
Fail-fast del Pub/Sub publisher
PubSubEventPublisher valida con TopicAdminClient.getTopic() que el topic existe en el proyecto GCP configurado (graceful-splice-493501-q7) antes de inicializar el Publisher. Si el topic no existe o la SA no tiene permisos → IllegalStateException y el bean falla → el proxy no levanta. Esto evita el escenario silencioso de publishes que "fallan exitosamente" hacia un topic inexistente.
SSL dinámico por SNI
gtm-proxy usa AsyncMapping de Netty para resolver certificados por dominio. Los certs se obtienen de node-api (PostgreSQL) via HTTP, no del filesystem.
Ciclo de vida del certificado:
- gtm-fabric emite cert via Let's Encrypt (ACME http-01 challenge)
- Cert se guarda en PostgreSQL via node-api (
SslCertificateentity) - gtm-proxy fetch el cert por HTTP:
GET /internal/fabric/certificates?domain={domain} - Se parsea el PEM y se construye un
SslContextde Netty - Se cachea en memoria (1h TTL)
- El SNI handler lo sirve en el TLS handshake
Arquitectura de threads:
El SNI callback corre en el event loop de Netty (thread epoll). Como el fetch del cert usa WebClient.block() (operación bloqueante), NO puede ejecutarse en el event loop. Por eso el SNI callback delega al thread pool ssl-resolver:
Cache de SSL (NodeApiSslContextProvider):
- Positive cache (1h TTL):
SslContextpor dominio - Negative cache (5min TTL): dominios sin certificado — solo se cachean respuestas null genuinas
- Errores transitorios (timeout, red): NO se cachean — reintenta en la siguiente conexión
- Skip
*.crimoo.com: Cloudflare maneja estos certificados - Invalidación:
POST /internal/ssl-reload {domain}limpia cache y precarga el cert fresco - Thread safety:
ConcurrentHashMappara ambos caches,ssl-resolverthread pool (2 threads) para fetches
Renovación de certificados:
CertificateRenewalScheduler(node-api) renueva certs 30 días antes de expirar (diario a las 2 AM UTC)- Tras renovar, debería llamar
proxyNotifier.notifyReload()para invalidar el cache del proxy (pendiente de implementar)
Flujo async completo en gtm-proxy
Routing por dominio
gtm-proxy tiene dos backends HTTP según el Host del request:
| Host | Backend | Propósito |
|---|---|---|
api.crimoo.com | NodeApiForwardingHandler → http://host.docker.internal:3000 | Requests del dashboard (ui-angular) |
| Cualquier otro (dominios GTM del cliente) | ForwardingHandler → http://host.docker.internal:8000 | Requests de los sitios web que usan GTM |
Esto resuelve el problema de routing para rutas /api/* que existen tanto en node-api como en gtm-fabric.
CORS
El proxy gestiona CORS de forma diferente según el dominio de destino:
Para api.crimoo.com — whitelist explícita de orígenes permitidos:
https://crimoo.comhttps://www.crimoo.comhttps://crimoo-bf067.web.apphttps://crimoo-bf067.firebaseapp.comhttp://localhost:4200
Si el Origin no está en la whitelist → 403 Forbidden. Los headers CORS completos (Access-Control-Allow-Origin, Access-Control-Allow-Credentials, etc.) los añade el proxy, y los headers access-control-* del upstream (node-api) son filtrados para evitar duplicados.
Para dominios GTM — reflect-origin: Access-Control-Allow-Origin replica el Origin del request. Los requests del CRM Inspector debug panel usan credentials:'omit' en fetch para evitar conflictos CORS con credenciales.
CustomLoaderService
⚠️ Nota: Actualmente existe código duplicado de generación de scripts entre gtm-proxy (CustomLoaderService.java) y gtm-fabric (CustomGTMLoaderService.java). El proxy es el que sirve el script en runtime. Pendiente de refactor para eliminar la duplicación.
Captura de eventos del DataLayer
El script de identidad incluye un polling (setInterval cada 500ms) que revisa window.dataLayer por nuevos eventos. Todos los eventos se registran en __events del objeto global, con campos matched (boolean) y ruleName para indicar si coinciden con una regla existente. Este mecanismo es robusto contra GTM sobrescribiendo dataLayer.push.