Saltar al contenido
← artículos
Claude CodeAI CodingSoftware EngineeringServer-Side TrackingGoogle Tag ManagerVPS

El SaaS que reemplacé en una tarde

En una tarde con Claude Code saqué el server-side GTM de Stape y lo llevé a mi propio VPS, descubrí por qué Meta contaba casi el doble que GA4 y asumí la guardia. Lo que un ingeniero puede llevar in-house hoy, y lo que cuesta.

Stape me cortó. No hice upgrade.

La mañana del 30 de septiembre, cada hit de GA4 de este sitio volvía con un 404 de Stape. El plan free se había acabado. Desde las 22:00 de la noche anterior hasta las 17:00 de ese día, GA4 registró dos eventos. Mi evento de Lead solo pasa por el servidor, así que cualquier lead en esa ventana quedó invisible para GA4 y para Meta.

Me enojé. Después abrí Claude Code y, a las 17:00, los hits ya pasaban por un servidor que controlo yo. Me llevó más o menos una tarde de mi tiempo, repartida en un día normal de trabajo. Esa tarde es la parte interesante. Muestra lo que un ingeniero puede asumir en 2026, y lo que cuesta.

30 de septiembre, en números

19 h
con GA4 apagadodos eventos de las 22:00 a las 17:00
~1
tarde de mi tiemporepartida en un día de trabajo
70
testsmás un lab que corre la imagen real de Google offline
0
suscripciones nuevasel VPS ya estaba pago
El apagón fue la regla de Stape funcionando como está documentada. La tarde fue la parte que elegí yo.

No me bloquearon. Me pasé del plan free.

El plan free de Stape te da 10.000 requests al mes. Cuando se acaban, el container deja de servir tráfico hasta el siguiente ciclo de facturación. La regla está en su documentación, y es justa.

Yo iba en 13.355. Unos 8.700 de esos requests no los podía explicar, probablemente bots. Mis propias notas de dos días antes tenían la línea “hacer upgrade de Stape antes del tráfico pago”. La leí y seguí de largo.

El plan pago más barato cuesta US$ 17 al mes con facturación anual. Para un blog de este tamaño, es la decisión racional, y durante casi toda mi carrera habría sido la mía. Reemplazar Stape significaba aprender cómo el container server de Google lee una IP, cómo se comporta el runtime de templates en sandbox de GTM y cómo Meta cruza un evento de servidor con una persona. Fines de semana, para un blog que recién empieza.

Stape no hizo nada mal. La cuenta cambió debajo de mis pies.

Server-side GTM es una oficina de correo que puedes correr tú mismo

Sin él, cada tracker de tu página habla con su dueño directo desde el navegador del visitante. El pixel de Meta le manda a Meta, la tag de Google le manda a Google, y tú no ves nada de eso.

El server-side GTM, la versión de Google del server-side tagging, pone una oficina de correo en el medio. El navegador manda una sola carta, a tu servidor. El servidor decide quién recibe copia (GA4, la Conversions API de Meta, tu propio BigQuery), qué se agrega (ciudad y país, un email con hash) y qué se quita antes de que algo salga. Google distribuye esa oficina como una imagen Docker. Stape te alquila una copia corriendo, más unos extras pagos que llama power-ups.

Meta es la excepción. Su pixel mantiene una línea propia desde el navegador, así que Meta se entera de cada visita dos veces: una por el pixel, otra por tu oficina. Se supone que debe quedarse con una sola copia. Ese detalle decide una sección más abajo.

La analogía se rompe en un punto, y eso importa después: el remitente. Meta lo lee, porque la IP del visitante es una de las señales que usa para cruzar un evento con una persona.

Cuando abrí el capó, casi nada era Stape. Mi container server corría sin cambios en cualquier copia de la imagen de Google. Hasta la tag de Meta del propio Stape, un template público de la galería, funcionaba sin el hosting de Stape. Las partes específicas de Stape eran dos headers en mi Cloudflare Worker, la forma en que se resolvía el dominio de la cookie y un power-up que le daba al container una credencial de BigQuery. La migración fue apuntar el Worker a otro lugar.

Los power-ups eran la otra mitad. Stape lista 23, y los útiles están en los planes pagos. Nueve ahora tienen un equivalente mío, cinco de ellos de planes que nunca pagué. Algunos ya los había construido antes. La mayoría salió esa tarde.

Los power-ups de Stape, y lo que los reemplazó

Nueve de los 23 power-ups de Stape tienen equivalente, cinco de ellos de planes pagos. Dos salieron más débiles, y se quedan en la tabla.
Power-upPlan en StapeEl míoSaldo
Hosting10k free, después pagoUn stack de bento en el VPS que ya corrosin cuota
AI Traffic DetectionProDetectado en el sitio, enviado a GA4, Meta, BigQuery y al formulario de leadmás amplio
Ad Blocker InfoProUn chequeo por sesión: ¿cargó el pixel?igual
Cookie KeeperProRenueva cookies desde el servidor, no restaura una borradamás débil
Bot DetectionProUser agent y navigator.webdriver, sin base de botsmás débil
Dedicated IPEnterpriseLa IP fija del propio VPSigual
Click ID RestorerFreeRestaura gclid y msclkid cuando Safari los quitaigual
Custom LoaderFreeGoogle tag gateway a través del Workerigual
GEO HeadersFreeGeo de Cloudflare y la IP real, enviadas por el Workerigual
Google Service AccountFreeUna variable de entorno en el stackigual
Nueve de los 23 power-ups de Stape tienen equivalente, cinco de ellos de planes pagos. Dos salieron más débiles, y se quedan en la tabla.

Toda la arquitectura entra en una pantalla

El sitio ya mandaba sus hits a felipefontoura.com/io, un Cloudflare Worker en el mismo origen. Mismo origen significa que las cookies vuelven en una respuesta del propio sitio, así que Safari no las limita a 7 días, como hace con las cookies escritas por JavaScript o seteadas por un servidor en otra IP. Ese Worker antes reenviaba a Stape. Ahora reenvía a mi VPS.

Un hit, del navegador a tres destinos

Entrada

El navegador de un visitante manda un hit de GA4 a felipefontoura.com/io

  1. WORKERProxy en el mismo origen

    Deja pasar hits de GA4 y el preview de debug, nada más. Agrega la geo de Cloudflare, la IP real del visitante en un header Forwarded y una clave secreta.

  2. TRAEFIKPortería en el VPS

    Solo enruta al container cuando la clave está presente. Todo lo demás recibe un 404, incluido cada scanner que busca /.env.

  3. SGTMLa imagen del propio Google

    El mismo container server que tenía en Stape. Reenvía a GA4, a la Conversions API de Meta y a una tabla de BigQuery que es mía.

Salida

Entra un hit, salen tres destinos, en infraestructura que ya pagaba

Un hit, del navegador a tres destinos: flujo de 3 pasos desde “El navegador de un visitante manda un hit de GA4 a felipefontoura.com/io”, con resultado “Entra un hit, salen tres destinos, en infraestructura que ya pagaba”.

El stack vive en bento, el instalador open source que convierte mi VPS en un Docker Swarm con Traefik. Ya corre mis agentes y mis automatizaciones. El stack de sGTM es un servicio más ahí, con merge esa misma tarde y una documentación lo bastante buena para alguien que nunca vio mi setup.

Una dirección IP pudo hundir toda la migración

Aquí es donde se rompe la analogía de la oficina de correo. Un sobre de papel mantiene el remitente hasta el final. En internet, cada proxy que pasa una solicitud le pone su propia dirección. Cada visitante de mi blog iba a parecer vecino de un data center de Cloudflare, y Meta no iba a cruzar a casi nadie.

El arreglo de siempre es una nota que dice quién la mandó de verdad: el header X-Forwarded-For. Traefik, la portería de mi VPS, tacha esa nota y escribe quién le entregó la solicitud, que es el Worker. Podría decirle a Traefik que confíe en las direcciones de Cloudflare, pero eso confía en cualquiera que corra en Cloudflare, no solo en mi Worker. Traefik no toca el header estándar Forwarded. Entonces el Worker escribe ahí la IP del visitante, encima de cualquier cosa que haya mandado el navegador, y la clave secreta es su credencial. Sin credencial, no entra.

Esa era la teoría. La documentación de Google decía que el container lee la IP del Forwarded, y un test local con Traefik mostró que el header sobrevivía. Aun así no hice deploy con eso. Claude armó un lab: la imagen real de Google corriendo en un container sin ruta a internet, recibiendo hits falsos por el mismo camino. La IP del visitante llegó al payload de Meta, en IPv4 y en IPv6. Un X-Forwarded-For falsificado fue ignorado. Ese era el mayor riesgo de la migración, y quedó cerrado antes de que algo tocara producción.

Un día después, el Events Manager de Meta mostró lo que llevaba un PageView.

Lo que llegó a Meta con un PageView, 1 de octubre

Las filas completas vienen del servidor. La fila del email son personas que ya llenaron un formulario, que hoy soy sobre todo yo.
ParámetroEventos que lo llevan
Dirección IP y user agent100%
Ciudad, estado y país100%
Código postal88%
Email, con hash32%
Las filas completas vienen del servidor. La fila del email son personas que ya llenaron un formulario, que hoy soy sobre todo yo.

Cuando alguien llena el formulario, el navegador guarda un hash SHA-256 de su email, teléfono y nombre, nunca el texto plano, y cada evento siguiente lo lleva, incluido un PageView común.

El agente me dio las manos. Las decisiones siguieron siendo mías.

Construyo software de producción hace veinticinco años, y el server-side tracking nunca fue mi especialidad. Esa tarde, Claude Code leyó tres repositorios y dos exports recientes de GTM, escribió el stack de Swarm, rehízo el Worker, escribió cuatro templates propios de GTM y un harness que corre su código real contra la configuración de mi container. Abrió los pull requests, en inglés, en un repo público, y de paso arregló un CI que estaba en rojo desde julio.

Ese es el poder que no tenía antes: alcance. Antes, el límite de lo que podía construir en una tarde era el límite de lo que ya sabía. Ahora el límite es lo que puedo verificar.

Verificar fue donde se fue mi parte de la tarde.

Lo que hizo el agente

  1. 01Comparó mis containers de GTM en producción con el repo de templates
  2. 02Escribió el stack, el cambio en el Worker y cuatro templates de GTM
  3. 03Armó 70 tests y el lab offline
  4. 04Hizo el review de producción que pedí y corrigió lo que encontró
  5. 05Abrió los pull requests open source, en inglés

Lo que decidí yo

  1. 01No reconstruir Stape. Arreglar lo que su ausencia rompió
  2. 02Meta, GA4 y BigQuery tienen que contar lo mismo
  3. 03Los bots fuera de todos los destinos, BigQuery incluido
  4. 04Mantener la navegación sin recarga. El tracking se adapta al sitio
  5. 05Revisarlo como si fuera a producción, sin overengineering
  6. 06Sumar tests donde un error silencioso cuesta dinero
La columna de la izquierda se llevó casi toda la tarde. La de la derecha es la razón por la que la izquierda valió el deploy.

El agente quería dejar el tráfico de bots en BigQuery con un flag is_bot, para que yo viera cuánto ruido se cortaba. Le dije que no. No me interesa contar bots en ningún lado, así que la columna también se fue. Cuando pregunté si el sitio había perdido la navegación sin recarga, la respuesta fue no, y le pedí que me mostrara el diff igual.

La IA no reemplaza a quien piensa. Multiplica a quien sabe construir. Repito esa frase tanto que se volvió mi vara de medir, y esa tarde fue una medición limpia.

El bug de verdad estaba en mis números, no en Stape

Perder la cuota me obligó a una auditoría que debí hacer antes. En el día y medio antes del apagón, Meta recibió unos 249 PageViews. GA4 contó unos 139. Mismo sitio, mismos visitantes, casi el doble. No se puede comprar anuncios con números que discrepan así.

Primero, cómo evita Meta contar una página dos veces. Se entera de cada página por el pixel y por mi servidor, que arma la copia de Meta a partir del hit de GA4. Se queda con una sola cuando las dos llevan el mismo event ID. Piensa en dos copias de un recibo: mismo número, una venta. Números distintos o sin número, y Meta no tiene forma de saber que es la misma venta.

Cinco cosas hicieron que los dos discreparan. Con los datos que tenía, no puedo ordenarlas por peso. Puedo señalar cada una en el código.

Cinco razones por las que Meta y GA4 discrepaban

  1. 1

    Ruido mapeado a PageView

    El servidor traducía nombres de evento de GA4 a eventos de Meta, con PageView como valor por defecto. Cada evento sin fila en la tabla, user_engagement y scroll incluidos, llegaba a Meta como un PageView más. Había quitado ese valor por defecto dos días antes, la noche del 28 de septiembre. Seguía dentro de la ventana.

    user_engagement → PageView
    scroll          → PageView
  2. 2

    Navegación sin recarga

    Astro cambia de página sin recargar, así que GTM solo corre en la primera. Después, cada herramienta seguía los cambios por su cuenta. El pixel mandaba su copia sin número de recibo. La copia del servidor, armada a partir del seguimiento de historial de GA4, conservaba el número de la primera página, así que Meta la descartaba como repetida. Cada página después de la primera llegaba a Meta sin los datos del servidor.

    pixel:   PageView, sin event_id
    server:  PageView, event_id de la página 1
  3. 3

    Bots filtrados en un solo lugar

    El filtro de bots solo protegía la Conversions API. Un bot que corre JavaScript igual disparaba el pixel y GA4, y solo GA4 descarta por su cuenta los bots conocidos.

  4. 4

    Los extras automáticos del pixel

    Una opción llamada Meta-enabled Conversions API estaba activada sin que nadie la marcara, porque el template trata un campo ausente como sí. Eventos automáticos también. Fuentes que GA4 y BigQuery nunca ven.

  5. 5

    Dos relojes

    GA4 reporta en hora de São Paulo. La API de Meta responde en UTC. Comparar "el mismo día" arranca con tres horas de diferencia.

Nada de esto fue culpa de Stape. Todo fue mío, y la migración me hizo mirar.

El arreglo fue una regla aplicada en todos lados. Cada cambio de página ahora empuja un solo evento, que dispara el pixel y la tag de GA4 juntos, con un número de recibo. Los bots ni siquiera cargan GTM, y el servidor repite el chequeo para lo que se salte la página. El comportamiento automático del pixel está apagado, y solo los eventos con fila en la tabla llegan a Meta. Un test falla si el patrón de bots del sitio y el del servidor alguna vez se separan.

Llevarlo in-house significa que la guardia es mía

Todo lo que salió mal esa tarde me tocaba atraparlo a mí. Sin ticket de soporte, sin status page. Una parte la atraparon los tests, otra el review, y una cosa llegó a los datos de producción antes de que alguien la atrapara.

Lo que se rompió en el camino, y quién lo atrapó

Los page views falsos salieron de la IP 203.0.113.77, un rango reservado para documentación, con utm_campaign=lab. GA4 puede filtrarlos. Meta se los queda para siempre.
Qué pasóLo atrapóArreglo
La primera corrida del lab mandó 3 page views falsos al pixel real de Meta y a GA4tardeEl container resuelve DNS por su cuenta e ignoró el archivo hosts. Ahora el lab no tiene ruta hacia afuera y lo verifica antes de aceptar un hit.
Los hits sin user agent pasaban el filtro de botstestsGTM convierte una variable vacía en el texto "undefined". El chequeo ahora lo maneja.
Un utm_source armado a propósito podía meter HTML en una cookie, el formulario de lead y mi herramienta de emailreviewAhora solo pasan caracteres de hostname. El caso malicioso es un test.
Cualquier sitio podía apagar mi tracking para sus propios visitantesreviewMi link de opt-out funcionaba incrustado como imagen. Ahora el Worker solo acepta navegaciones reales.
Un secret faltante habría mandado cada hit a un 404 silenciosoreviewAhora el Worker responde 500 con un mensaje que dice qué secret falta.
Las compras enviadas por webhook se habrían clasificado como botsreviewEl chequeo de bots ahora corre solo en hits del navegador.
Los page views falsos salieron de la IP 203.0.113.77, un rango reservado para documentación, con utm_campaign=lab. GA4 puede filtrarlos. Meta se los queda para siempre.

El agente encontró los últimos cuatro en un review. Los encontró porque le pedí uno, planteado como “lo que haríamos antes de producción, sin overengineering”. Ese pedido son los veinticinco años. El review es el agente.

El resto de la cuenta es continuo. Si el VPS se cae, GA4 y Meta se caen con él. Elegí no construir un fallback con este volumen, y esa elección ahora es mía. Stape revisa cada solicitud contra bases de bots privadas y públicas, y una base privada no la puedo replicar. La imagen solo corre en amd64. Las actualizaciones, los logs y la rotación de claves son míos. Nada de eso es caro. Todo eso es mío.

Tráelo in-house cuando la capa es fina y puedes revisar el trabajo

El agente no hizo que valga la pena reemplazar cualquier SaaS. Movió la línea para un tipo: una capa fina sobre algo abierto, donde puedo atrapar al agente cuando se equivoca. No conocía bien este dominio. Pude armarle un lab, y con eso alcanzó.

Alquilarlo o correrlo

Córrelo tú

Server-side GTM, en mi caso. Google distribuye la imagen, yo ya corro el VPS y puedo atrapar al agente cuando se equivoca.

Alquílalo y duerme

El valor está en la operación: un SLA, compliance, datos agregados de miles de clientes. Entenderlo no te deja copiarlo.

Alquílalo mientras aprendes

El agente lo va a escribir igual. No vas a saber qué bugs salieron con él.

Alquílalo

Nada que ganar y nada que juzgar. Paga la factura.

2×2 Matrix: horizontal axis capa del SaaS (from fina to gruesa); vertical axis atrapar los bugs (from no puedo to puedo). Quadrants: Córrelo tú (puedo atrapar los bugs · fina capa del SaaS); Alquílalo y duerme (puedo atrapar los bugs · gruesa capa del SaaS); Alquílalo mientras aprendes (no puedo atrapar los bugs · fina capa del SaaS); Alquílalo (no puedo atrapar los bugs · gruesa capa del SaaS).

El agente movió la línea en la columna de la izquierda. La de la derecha sigue siendo de los proveedores. Hasta Stape tenía una pieza ahí, su base de bots, y la dejé ir a propósito.

Antes de cancelar tu próxima suscripción, revisa lo que yo tenía a favor.

Lo que hizo que valiera la pena llevar este in-house

  • Obligatorio:
    El núcleo es abierto y lo mantiene otroGoogle publica la imagen del container server. Yo la corro, no la escribo.
  • Obligatorio:
    La infraestructura ya existeSwarm, Traefik, TLS y DNS ya corrían para otros stacks. El costo nuevo fue un servicio.
  • Obligatorio:
    Puedes testear sin tocar producciónTests para la lógica, un lab offline para la imagen real. La única fuga pasó antes de que el lab quedara aislado, y ese es el punto.
  • Obligatorio:
    El modo de falla lo absorbes túUna tarde apagada en mi blog me cuesta datos. En el checkout de un cliente le costaría dinero. Otra decisión.
  • Obligatorio:
    Ganas algo que el SaaS no te estaba dandoConteos que coinciden, mi propia tabla en BigQuery y power-ups del plan Pro sin el plan Pro.
  • Anti-pattern:
    Cancelar solo para ahorrarte la suscripciónDiecisiete dólares al mes es más barato que una mala tarde. Hazlo por control o por capacidad, no por la factura.

El plan free no mató un SaaS. Mató un default.

Durante casi toda mi carrera, “construir o comprar” para una capa así tenía una respuesta por defecto: comprar, porque construir significaba semanas fuera de mi especialidad. Ese default era, en realidad, una afirmación sobre mi alcance. La tarde del 30 de septiembre lo movió.

Lo que tengo ahora y no tenía antes es un alcance de ingeniero que crece con lo que puedo revisar, no con lo que ya sé. Lo que sigo teniendo es la guardia. La pregunta que decide si deberías llevar algo in-house ya no es “¿puedo construirlo?”. Es “si el agente se equivoca, ¿me voy a dar cuenta?”.

Server-side GTM self-hosted, respuestas rápidas

¿Qué pasa cuando te pasas del plan free de Stape?

El plan free incluye 10.000 requests al mes. Según la documentación de Stape, cuando se acaba la cuota el container deja de servir tráfico hasta el siguiente ciclo de facturación, y la cuota no se acumula. Los planes pagos empiezan en US$ 17 al mes con facturación anual, para hasta 500.000 requests.

¿Hay una alternativa gratis a Stape?

Sí, si puedes correr un servidor. Stape hospeda la imagen del container server del propio Google, y Google publica esa imagen para que cualquiera la corra. En un VPS que ya pagas, no hay suscripción nueva.

La mayoría de los power-ups se pueden reconstruir. Yo reconstruí nueve, cinco de ellos de planes pagos de Stape. Lo que no se puede reconstruir es la parte gestionada: la base de bots de Stape y alguien vigilando el uptime. El precio real de la opción gratis es operarla tú.

¿Se puede hacer self-hosting del server-side Google Tag Manager?

Sí. Google publica el container server como imagen Docker. Corres dos servicios a partir de ella, uno de tagging y otro de preview de debug, y los apuntas al config string de un container server provisionado manualmente en GTM.

Yo lo corro como un stack en bento, mi instalador open source de VPS, detrás de Traefik.

¿Necesito un proxy delante de un sGTM self-hosted?

No, un subdominio que apunte directo al container funciona. Yo uso un proxy en el mismo origen igual: las cookies vuelven desde el origen del propio sitio, los scanners nunca llegan al container y solo pasan las solicitudes con la clave del proxy. Un subdominio que apunta a otro servidor pierde la parte de las cookies, porque Safari también limita las cookies seteadas desde otra IP.

Si usas un proxy, manda la IP del visitante en un header que tu reverse proxy no sobrescriba. En mi caso fue Forwarded, no X-Forwarded-For.

¿Hostear sGTM es gratis?

No hay suscripción nueva si ya corres un servidor. Los dos servicios piden unos cientos de megabytes de RAM, y los streaming inserts de BigQuery cuestan centavos con el volumen de un blog.

El costo real es la operación: actualizaciones, monitoreo y cada caída siendo tuya.

¿Por qué Meta y GA4 muestran conteos distintos de page views?

En mi sitio, cinco cosas: eventos sin mapear que llegaban a Meta como PageViews extra, páginas cambiadas sin recarga que cada herramienta seguía por su cuenta con event IDs que no se emparejaban, bots filtrados en un solo lugar, los eventos automáticos del pixel y reportes en husos horarios distintos.