Skip to main content

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 por gtm-proxy.

Rol de gtm-proxy en el sistema

gateway (Go + Echo)

Endpoints

MétodoPathDescripción
GET/healthHealth check
GET/gtm.js?id=GTM-XXXXServir custom loader script
GET/gtm/load.jsProxy a Google GTM
POST/GET/collectCapturar evento de tagging
POST/GET/g/collectCapturar evento GA4
POST/api/iIdentity resolution
GET/api/capture-config/:gtmIdConfig de captura pública
POST/api/cache/invalidateInvalidar 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 el Ga4BypassService deobfuscó el path obfuscado al /g/collect original. La decisión sync vs Pub/Sub se toma DESPUÉS, en el bloque siguiente del ProxyHandler.

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 por GatewayTokenFilter)
  • X-Meta-Access-Token — token del pixel Meta, reenviado a graph.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:

  1. gtm-fabric emite cert via Let's Encrypt (ACME http-01 challenge)
  2. Cert se guarda en PostgreSQL via node-api (SslCertificate entity)
  3. gtm-proxy fetch el cert por HTTP: GET /internal/fabric/certificates?domain={domain}
  4. Se parsea el PEM y se construye un SslContext de Netty
  5. Se cachea en memoria (1h TTL)
  6. 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): SslContext por 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: ConcurrentHashMap para ambos caches, ssl-resolver thread 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:

HostBackendPropósito
api.crimoo.comNodeApiForwardingHandlerhttp://host.docker.internal:3000Requests del dashboard (ui-angular)
Cualquier otro (dominios GTM del cliente)ForwardingHandlerhttp://host.docker.internal:8000Requests 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.com
  • https://www.crimoo.com
  • https://crimoo-bf067.web.app
  • https://crimoo-bf067.firebaseapp.com
  • http://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.