Pular para o conteúdo
← artigos
Claude CodeAI CodingSoftware EngineeringServer-Side TrackingGoogle Tag ManagerVPS

O SaaS que eu substituí em uma tarde

Em uma tarde com Claude Code tirei o server-side GTM do Stape e levei para o meu próprio VPS, descobri por que o Meta contava quase o dobro do GA4 e assumi o plantão. O que um engenheiro consegue trazer para dentro hoje, e quanto custa.

O Stape me cortou. Eu não fiz upgrade.

Na manhã de 30 de setembro, todo hit de GA4 deste site voltava com um 404 do Stape. O plano free tinha acabado. Das 22h da noite anterior até as 17h daquele dia, o GA4 registrou dois eventos. Meu evento de Lead só passa pelo servidor, então qualquer lead nessa janela ficou invisível para o GA4 e para o Meta.

Fiquei bem frustrado. Depois abri o Claude Code e, às 17h, os hits já passavam por um servidor que eu controlo. Levou mais ou menos uma tarde do meu tempo, espalhada num dia normal de trabalho. Essa tarde é a parte interessante. Ela mostra o que um engenheiro consegue assumir em 2026, e quanto custa.

30 de setembro, em números

19 h
com o GA4 apagadodois eventos das 22h às 17h
~1
tarde do meu tempoespalhada num dia de trabalho
70
testesmais um lab rodando a imagem real do Google offline
0
assinaturas novaso VPS já estava pago
O apagão foi a regra do Stape funcionando como documentado. A tarde foi a parte que eu escolhi.

Não fui bloqueado. Eu passei do plano free.

O plano free do Stape dá 10.000 requests por mês. Quando acabam, o container para de servir tráfego até o próximo ciclo de cobrança. A regra está na documentação deles, e é justa.

Eu estava em 13.355. Uns 8.700 desses requests eu não conseguia explicar, provavelmente bots. Minhas próprias notas de dois dias antes tinham a linha “fazer upgrade do Stape antes do tráfego pago”. Li e segui em frente.

O plano pago mais barato custa US$ 17 por mês na cobrança anual. Para um blog desse tamanho, é a decisão racional, e durante quase toda a minha carreira teria sido a minha. Substituir o Stape significava aprender como o container server do Google lê um IP, como se comporta o runtime de templates em sandbox do GTM e como o Meta casa um evento de servidor com uma pessoa. Fins de semana, para um blog que está só começando.

O Stape não fez nada de errado. A conta é que mudou debaixo de mim.

Server-side GTM é uma central de correio que você mesmo pode rodar

Sem ele, cada tracker da sua página fala com o dono direto do navegador do visitante. O pixel do Meta manda para o Meta, a tag do Google manda para o Google, e você não vê nada disso.

O server-side GTM, a versão do Google de server-side tagging, coloca uma central de correio no meio. O navegador manda uma carta só, para o seu servidor. O servidor decide quem recebe cópia (GA4, a Conversions API do Meta, o seu próprio BigQuery), o que é acrescentado (cidade e país, um e-mail com hash) e o que é removido antes de qualquer coisa sair. O Google distribui essa central como uma imagem Docker. O Stape aluga uma cópia rodando, mais uns adicionais pagos que ele chama de power-ups.

O Meta é a exceção. O pixel dele mantém uma linha própria a partir do navegador, então o Meta fica sabendo de cada visita duas vezes: uma pelo pixel, outra pela sua central. Ele deveria guardar só uma cópia. Esse detalhe decide uma seção mais abaixo.

A analogia quebra num ponto, e isso importa depois: o remetente. O Meta lê o remetente, porque o IP do visitante é um dos sinais que ele usa para casar um evento com uma pessoa.

Quando abri o capô, quase nada era Stape. Meu container server rodava sem mudança em qualquer cópia da imagem do Google. Até a tag de Meta do próprio Stape, um template público da galeria, funcionava sem a hospedagem do Stape. As partes específicas do Stape eram dois headers no meu Cloudflare Worker, o jeito como o domínio do cookie era resolvido e um power-up que dava ao container uma credencial do BigQuery. A migração foi apontar o Worker para outro lugar.

Os power-ups eram a outra metade. O Stape lista 23, e os úteis ficam nos planos pagos. Nove agora têm um equivalente meu, cinco deles de planos que eu nunca paguei. Alguns eu já tinha construído antes. A maioria veio naquela tarde.

Os power-ups do Stape, e o que entrou no lugar

Nove dos 23 power-ups do Stape têm equivalente, cinco deles de planos pagos. Dois saíram mais fracos, e continuam na tabela.
Power-upPlano no StapeO meuSaldo
Hospedagem10k free, depois pagoUma stack do bento no VPS que eu já rodosem cota
AI Traffic DetectionProDetectado no site, enviado ao GA4, Meta, BigQuery e ao formulário de leadmais amplo
Ad Blocker InfoProUma checagem por sessão: o pixel carregou?igual
Cookie KeeperProRenova cookies pelo servidor, não restaura um apagadomais fraco
Bot DetectionProUser agent e navigator.webdriver, sem base de botsmais fraco
Dedicated IPEnterpriseO IP fixo do próprio VPSigual
Click ID RestorerFreeRestaura gclid e msclkid quando o Safari removeigual
Custom LoaderFreeGoogle tag gateway pelo Workerigual
GEO HeadersFreeGeo da Cloudflare e o IP real, enviados pelo Workerigual
Google Service AccountFreeUma variável de ambiente na stackigual
Nove dos 23 power-ups do Stape têm equivalente, cinco deles de planos pagos. Dois saíram mais fracos, e continuam na tabela.

A arquitetura inteira cabe numa tela

O site já mandava os hits para felipefontoura.com/io, um Cloudflare Worker na mesma origem. Mesma origem significa que os cookies voltam numa resposta do próprio site, então o Safari não limita a 7 dias, como faz com cookies gravados por JavaScript ou setados por um servidor em outro IP. Esse Worker antes encaminhava para o Stape. Agora encaminha para o meu VPS.

Um hit, do navegador a três destinos

Entrada

O navegador de um visitante manda um hit de GA4 para felipefontoura.com/io

  1. WORKERProxy na mesma origem

    Deixa passar hits de GA4 e o preview de debug, mais nada. Acrescenta a geo da Cloudflare, o IP real do visitante num header Forwarded e uma chave secreta.

  2. TRAEFIKPortaria no VPS

    Só encaminha para o container quando a chave está presente. Todo o resto leva um 404, inclusive cada scanner procurando /.env.

  3. SGTMA imagem do próprio Google

    O mesmo container server que eu tinha no Stape. Ele encaminha para o GA4, para a Conversions API do Meta e para uma tabela do BigQuery que é minha.

Saída

Um hit entra, três destinos saem, numa infraestrutura que eu já pagava

Um hit, do navegador a três destinos: fluxo de 3 etapas a partir de “O navegador de um visitante manda um hit de GA4 para felipefontoura.com/io”, resultando em “Um hit entra, três destinos saem, numa infraestrutura que eu já pagava”.

A stack fica no bento, o instalador open source que transforma o meu VPS num Docker Swarm com Traefik. Ele já roda meus agentes e minhas automações. A stack do sGTM é mais um serviço ali, com merge naquela tarde e uma documentação boa o bastante para quem nunca viu o meu setup.

Um endereço IP podia ter afundado a migração inteira

É aqui que a analogia da central de correio quebra. Um envelope de papel mantém o remetente até o fim. Na internet, cada proxy que repassa uma requisição coloca o próprio endereço nela. Todo visitante do meu blog ia parecer morador de um data center da Cloudflare, e o Meta não ia casar quase ninguém.

A correção de sempre é um bilhete dizendo quem mandou de verdade: o header X-Forwarded-For. O Traefik, a portaria do meu VPS, risca esse bilhete e escreve quem entregou a requisição para ele, que é o Worker. Eu poderia mandar o Traefik confiar nos endereços da Cloudflare, mas aí ele confia em qualquer um rodando na Cloudflare, não só no meu Worker. O Traefik não mexe no header padrão Forwarded. Então o Worker escreve o IP do visitante ali, por cima de qualquer coisa que o navegador tenha mandado, e a chave secreta é o crachá dele. Sem crachá, não entra.

Essa era a teoria. A documentação do Google dizia que o container lê o IP do Forwarded, e um teste local com Traefik mostrou que o header sobrevivia. Mesmo assim não fiz deploy com base nisso. O Claude montou um lab: a imagem real do Google rodando num container sem rota para a internet, recebendo hits falsos pelo mesmo caminho. O IP do visitante chegou no payload do Meta, em IPv4 e em IPv6. Um X-Forwarded-For forjado foi ignorado. Esse era o maior risco da migração, e ele foi fechado antes de qualquer coisa tocar produção.

Um dia depois, o Events Manager do Meta mostrou o que um PageView carregava.

O que chegou ao Meta com um PageView, 1º de outubro

As linhas cheias vêm do servidor. A linha do e-mail são pessoas que já preencheram um formulário, o que hoje é principalmente eu.
ParâmetroEventos com o parâmetro
Endereço IP e user agent100%
Cidade, estado e país100%
CEP88%
E-mail, com hash32%
As linhas cheias vêm do servidor. A linha do e-mail são pessoas que já preencheram um formulário, o que hoje é principalmente eu.

Depois que alguém preenche o formulário, o navegador guarda um hash SHA-256 do e-mail, do telefone e do nome, nunca o texto puro, e todo evento seguinte leva isso junto, inclusive um PageView comum.

O agente me deu as mãos. As decisões continuaram minhas.

Eu construo software de produção há vinte e cinco anos, e server-side tracking nunca foi a minha especialidade. Naquela tarde, o Claude Code leu três repositórios e dois exports recentes do GTM, escreveu a stack do Swarm, refez o Worker, escreveu quatro templates customizados de GTM e um harness que roda o código real deles contra a configuração do meu container. Abriu os pull requests, em inglês, num repo público, e de quebra consertou um CI que estava vermelho desde julho.

Esse é o poder que eu não tinha antes: alcance. Antes, o limite do que eu conseguia construir numa tarde era o limite do que eu já sabia. Agora o limite é o que eu consigo verificar.

Verificar foi onde a minha parte da tarde foi parar.

O que o agente fez

  1. 01Comparou meus containers do GTM em produção com o repo de templates
  2. 02Escreveu a stack, a mudança no Worker e quatro templates de GTM
  3. 03Montou 70 testes e o lab offline
  4. 04Fez o review de produção que eu pedi e corrigiu o que achou
  5. 05Abriu os pull requests open source, em inglês

O que eu decidi

  1. 01Não reconstruir o Stape. Consertar o que a falta dele quebrou
  2. 02Meta, GA4 e BigQuery precisam contar a mesma coisa
  3. 03Bot fora de todos os destinos, BigQuery incluído
  4. 04Manter a navegação sem reload. O tracking se adapta ao site
  5. 05Revisar como se fosse para produção, sem overengineering
  6. 06Aumentar os testes onde um erro silencioso custa dinheiro
A coluna da esquerda tomou quase toda a tarde. A da direita é o motivo de a esquerda valer o deploy.

O agente queria manter o tráfego de bot no BigQuery com uma flag is_bot, para eu ver quanto ruído foi cortado. Eu disse não. Não tenho interesse em contar bot em lugar nenhum, então a coluna saiu também. Quando perguntei se o site tinha perdido a navegação sem reload, a resposta foi não, e pedi para ver o diff mesmo assim.

A IA não substitui quem pensa. Ela multiplica quem sabe construir. Repito essa frase tanto que ela virou régua, e aquela tarde foi uma medição limpa.

O bug de verdade estava nos meus números, não no Stape

Perder a cota forçou uma auditoria que eu devia ter feito antes. No dia e meio antes do apagão, o Meta recebeu uns 249 PageViews. O GA4 contou uns 139. Mesmo site, mesmos visitantes, quase o dobro. Não dá para comprar anúncio com números que discordam desse jeito.

Primeiro, como o Meta evita contar uma página duas vezes. Ele fica sabendo de cada página pelo pixel e pelo meu servidor, que monta a cópia do Meta a partir do hit do GA4. Ele só guarda uma quando as duas carregam o mesmo event ID. Pense em duas vias de um recibo: mesmo número, uma venda. Números diferentes ou sem número, e o Meta não tem como saber que é a mesma venda.

Cinco coisas fizeram os dois discordarem. Com os dados que eu tinha, não consigo ordenar por peso. Consigo apontar cada uma no código.

Cinco motivos para Meta e GA4 discordarem

  1. 1

    Ruído mapeado para PageView

    O servidor traduzia nomes de evento do GA4 para eventos do Meta, com PageView como padrão. Todo evento sem linha na tabela, user_engagement e scroll incluídos, chegava ao Meta como mais um PageView. Eu tinha removido esse padrão dois dias antes, na noite de 28 de setembro. Ainda estava dentro da janela.

    user_engagement → PageView
    scroll          → PageView
  2. 2

    Navegação sem reload

    O Astro troca de página sem recarregar, então o GTM só roda na primeira. Depois disso, cada ferramenta acompanhava as trocas por conta própria. O pixel mandava a cópia dele sem número de recibo. A cópia do servidor, montada a partir do rastreamento de histórico do GA4, mantinha o número da primeira página, então o Meta descartava como repetida. Toda página depois da primeira chegava ao Meta sem os dados do servidor.

    pixel:   PageView, sem event_id
    server:  PageView, event_id da página 1
  3. 3

    Bot filtrado num lugar só

    O filtro de bot só protegia a Conversions API. Um bot que roda JavaScript ainda disparava o pixel e o GA4, e só o GA4 descarta bots conhecidos por conta própria.

  4. 4

    Os extras automáticos do pixel

    Uma opção chamada Meta-enabled Conversions API estava ligada sem ninguém ter marcado, porque o template trata campo ausente como sim. Eventos automáticos também. Fontes que o GA4 e o BigQuery nunca veem.

  5. 5

    Dois relógios

    O GA4 reporta no horário de São Paulo. A API do Meta responde em UTC. Comparar "o mesmo dia" já começa com três horas de diferença.

Nada disso foi culpa do Stape. Tudo foi meu, e a migração me fez olhar.

A correção foi uma regra aplicada em todo lugar. Cada troca de página agora empurra um evento só, que dispara o pixel e a tag do GA4 juntos, com um número de recibo. Bot nem carrega o GTM, e o servidor repete a checagem para o que pular a página. O comportamento automático do pixel está desligado, e só eventos com linha na tabela chegam ao Meta. Um teste falha se o padrão de bot do site e o do servidor algum dia divergirem.

Trazer para dentro significa que o plantão é meu

Tudo o que deu errado naquela tarde era meu para pegar. Sem ticket de suporte, sem status page. Parte os testes pegaram, parte o review pegou, e uma coisa chegou aos dados de produção antes de alguém pegar.

O que quebrou no caminho, e quem pegou

Os page views falsos saíram do IP 203.0.113.77, uma faixa reservada para documentação, com utm_campaign=lab. O GA4 consegue filtrar. O Meta guarda para sempre.
O que aconteceuQuem pegouCorreção
A primeira rodada do lab mandou 3 page views falsos para o pixel real do Meta e para o GA4tarde demaisO container resolve DNS por conta própria e ignorou o arquivo hosts. Agora o lab não tem rota para fora e confere isso antes de aceitar um hit.
Hits sem user agent passavam pelo filtro de bottestesO GTM transforma uma variável vazia no texto "undefined". A checagem agora trata isso.
Um utm_source forjado podia levar HTML para um cookie, o formulário de lead e a minha ferramenta de e-mailreviewAgora só passam caracteres de hostname. O caso malicioso virou teste.
Qualquer site podia desligar o meu tracking para os visitantes delereviewMeu link de opt-out funcionava embutido como imagem. Agora o Worker só aceita navegação de verdade.
Um secret ausente teria mandado todo hit para um 404 silenciosoreviewAgora o Worker responde 500 com uma mensagem dizendo qual secret está faltando.
Compras enviadas por webhook teriam sido classificadas como botreviewA checagem de bot agora roda só em hits do navegador.
Os page views falsos saíram do IP 203.0.113.77, uma faixa reservada para documentação, com utm_campaign=lab. O GA4 consegue filtrar. O Meta guarda para sempre.

O agente achou as últimas quatro num review. Achou porque eu pedi um, enquadrado como “o que a gente faria antes de produção, sem overengineering”. Esse pedido são os vinte e cinco anos. O review é o agente.

O resto da conta é contínuo. Se o VPS cair, o GA4 e o Meta caem junto. Escolhi não construir um fallback nesse volume, e essa escolha agora é minha. O Stape confere cada requisição contra bases de bots privadas e públicas, e uma base privada eu não tenho como replicar. A imagem só roda em amd64. Atualizações, logs e rotação de chave são comigo. Nada disso é caro. Tudo isso é meu.

Traga para dentro quando a camada é fina e você consegue conferir o trabalho

O agente não fez todo SaaS valer a substituição. Ele mudou a linha para um tipo: uma camada fina sobre algo aberto, onde eu consigo pegar o agente quando ele erra. Eu não conhecia bem esse domínio. Consegui montar um lab para ele, e isso bastou.

Alugar ou rodar

Rode você

Server-side GTM, no meu caso. O Google distribui a imagem, eu já rodo o VPS e consigo pegar o agente quando ele erra.

Alugue e durma

O valor está na operação: um SLA, compliance, dados agregados de milhares de clientes. Entender isso não deixa você copiar.

Alugue enquanto aprende

O agente vai escrever de qualquer jeito. Você não vai saber quais bugs foram junto.

Alugue

Nada a ganhar e nada a julgar. Pague a fatura.

2×2 Matrix: horizontal axis camada do SaaS (from fina to grossa); vertical axis pegar os bugs (from não consigo to consigo). Quadrants: Rode você (consigo pegar os bugs · fina camada do SaaS); Alugue e durma (consigo pegar os bugs · grossa camada do SaaS); Alugue enquanto aprende (não consigo pegar os bugs · fina camada do SaaS); Alugue (não consigo pegar os bugs · grossa camada do SaaS).

O agente mudou a linha na coluna da esquerda. A coluna da direita continua sendo dos fornecedores. Até o Stape tinha uma peça ali, a base de bots, e eu abri mão dela de propósito.

Antes de cancelar a sua próxima assinatura, confira o que jogava a meu favor.

O que fez valer a pena trazer este para dentro

  • Obrigatório:
    O núcleo é aberto e outra pessoa mantémO Google publica a imagem do container server. Eu rodo, não escrevo.
  • Obrigatório:
    A infraestrutura já existeSwarm, Traefik, TLS e DNS já rodavam para outras stacks. O custo novo foi um serviço.
  • Obrigatório:
    Dá para testar sem tocar em produçãoTestes para a lógica, um lab offline para a imagem real. O único vazamento aconteceu antes de o lab ficar isolado, e é esse o ponto.
  • Obrigatório:
    O modo de falha é seu para absorverUma tarde apagada no meu blog me custa dados. No checkout de um cliente, custaria dinheiro a ele. Outra decisão.
  • Obrigatório:
    Você ganha algo que o SaaS não estava dandoContagens que batem, minha própria tabela no BigQuery e power-ups do plano Pro sem o plano Pro.
  • Anti-pattern:
    Cancelar só para economizar a assinaturaDezessete dólares por mês é mais barato que uma tarde ruim. Faça por controle ou capacidade, não pela fatura.

O plano free não matou um SaaS. Matou um padrão.

Durante quase toda a minha carreira, “construir ou comprar” para uma camada assim tinha uma resposta padrão: comprar, porque construir significava semanas fora da minha especialidade. Esse padrão era, na verdade, uma afirmação sobre o meu alcance. A tarde de 30 de setembro mudou isso.

O que eu tenho agora e não tinha antes é um alcance de engenheiro que cresce com o que eu consigo conferir, não com o que eu já sei. O que eu continuo tendo é o plantão. A pergunta que decide se você deve trazer algo para dentro não é mais “eu consigo construir?”. É “se o agente errar, eu vou perceber?”.

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

O que acontece quando você passa do plano free do Stape?

O plano free inclui 10.000 requests por mês. Segundo a documentação do Stape, quando a cota acaba o container para de servir tráfego até o próximo ciclo de cobrança, e a cota não acumula. Os planos pagos começam em US$ 17 por mês, na cobrança anual, para até 500.000 requests.

Existe uma alternativa gratuita ao Stape?

Existe, se você consegue rodar um servidor. O Stape hospeda a imagem do container server do próprio Google, e o Google publica essa imagem para qualquer um rodar. Num VPS que você já paga, não entra assinatura nova.

A maioria dos power-ups dá para reconstruir. Eu reconstruí nove, cinco deles de planos pagos do Stape. O que não dá para reconstruir é a parte gerenciada: a base de bots do Stape e alguém vigiando o uptime. O preço real da opção gratuita é operar você mesmo.

Dá para hospedar o server-side Google Tag Manager por conta própria?

Dá. O Google publica o container server como imagem Docker. Você roda dois serviços a partir dela, um de tagging e outro de preview de debug, e aponta os dois para o config string de um container server provisionado manualmente no GTM.

Eu rodo como uma stack no bento, meu instalador open source de VPS, atrás do Traefik.

Preciso de um proxy na frente de um sGTM self-hosted?

Não, um subdomínio apontando direto para o container funciona. Eu uso um proxy na mesma origem mesmo assim: os cookies voltam da origem do próprio site, scanners nunca chegam ao container e só passam requisições com a chave do proxy. Um subdomínio apontando para outro servidor perde a parte dos cookies, porque o Safari também limita cookies setados a partir de outro IP.

Se você usar um proxy, mande o IP do visitante num header que o seu reverse proxy não sobrescreva. No meu caso foi o Forwarded, não o X-Forwarded-For.

Hospedar o sGTM é de graça?

Não entra assinatura nova se você já roda um servidor. Os dois serviços pedem algumas centenas de megabytes de RAM, e os streaming inserts do BigQuery custam centavos no volume de um blog.

O custo real é operação: atualizações, monitoramento e cada queda sendo sua.

Por que o Meta e o GA4 mostram contagens diferentes de page views?

No meu site, cinco coisas: eventos sem mapeamento chegando ao Meta como PageViews extras, páginas trocadas sem reload que cada ferramenta acompanhava por conta própria com event IDs que não pareavam, bot filtrado num lugar só, os eventos automáticos do pixel e relatórios em fusos diferentes.