Google Tag Manager (GTM)
GTM é o container central que orquestra tags client-side sem precisar deploy de front — em particular Google Ads e TikTok, que existem só como tag no container. Meta Pixel, Kwai, Taboola, Floodlight e Webtrends têm SDK próprio no front (o container não os carrega, mas consome os eventos deles no dataLayer compartilhado). Esta página cobre como o front carrega o GTM e como ele é configurado por brand.
:::warning Existe um segundo container em algumas brands
Quatro brands rodam também um container GTM secundário que lê de um dataLayer customizado (window.anaLayer) — e na 7k o GTM primário está desligado em produção, então lá só o secundário existe. Antes de debuggar, leia anaLayer.
:::
Modelo de carregamento
GTM é carregado eagerly por default: initGTM roda de forma síncrona no mount, então window.dataLayer existe imediatamente e todo pushGTMEvent seguinte entra no container — nenhum evento é perdido.
:::note Histórico — o gating por LCP/Lighthouse foi REMOVIDO em 2026-06
O front tinha um scheduler que segurava todo script de terceiro atrás de uma cadeia de gates (conclusão do LCP, primeira interação, idle callback, delay fixo) mais um branch que jogava tudo pra 25s quando detectava auditoria sintética (Lighthouse/PSI). Isso melhorava o número do Lighthouse mas quebrava marketing: eventos disparados antes dos gates eram descartados (pushGTMEvent é no-op até o GTM inicializar, e não havia fila de replay), usuários que convertiam sem interagir nunca eram rastreados, e a auditoria media uma página que usuário real nunca via.
Hoje scheduleThirdParty(fn) simplesmente chama fn() (modulo o guard de prerender e o kill switch da brand), e não existe mais branch de bot/UA — auditoria sintética e usuário real carregam exatamente os mesmos scripts. Referência: app/utils/third-party-scheduler.client.ts.
:::
As três lanes de carregamento
| Lane | Quem usa | Quando roda |
|---|---|---|
| Eager (default) | GTM, Meta Pixel, Taboola, Kwai, Webtrends, Floodlight | No mount, imediatamente |
| Engagement (opt-in por brand) | os mesmos acima, quando a brand liga | Filas primadas no mount; download na 1ª interação real OU no deadline |
| Deferred | Clarity, Hotjar, RA (selo de reputação) | scheduleThirdPartyDeferred — idle OU 1ª interação OU timeout de segurança, o que vier primeiro |
Fora dessas, o Pendo tem lane própria (scheduleThirdPartyOnInteraction — só carrega após interação real). Ver Pendo.
O kill switch featuresConfig.thirdPartyScriptsDisabled da brand vale pra todas as lanes: com ele ligado, toda chamada é no-op.
Guard de prerender (Speculation Rules)
Tanto o dispatcher de analytics quanto o scheduler seguram tudo enquanto document.prerendering === true, e dão flush no evento prerenderingchange. Motivo: evitar pageview/conversão fantasma de páginas que o browser pré-renderizou mas o usuário nunca visitou. É um mecanismo de correção de atribuição — se você vê contagem menor que o esperado em páginas com Speculation Rules, é esperado, e está certo.
Estratégia de carregamento dos scripts de marketing
analyticsConfig.marketingScriptsStrategy controla quando o script baixa — não o que é medido.
// overrides/<brand>/app/config/analytics/analytics.ts
{
marketingScriptsStrategy: "engagement", // "eager" (default) | "engagement"
marketingScriptsMaxDelayMs: 5000, // deadline; default 5000
}
"eager"(default) —gtm.js,fbevents.js, Taboola, Kwai, Webtrends e Floodlight baixam no mount."engagement"— o download só acontece na primeira interação real do visitante (pointer/tecla/touch/wheel —scrollé excluído de propósito, porque dispara sem intenção via scroll restoration / CLS) OU no deadlinemarketingScriptsMaxDelayMs, o que vier primeiro.
Nenhum evento é descartado no modo engagement. As filas nascem síncronas no mount, via prime:
| Prime | Cria |
|---|---|
primeGTMQueue | window.dataLayer + o consent default do GCM v2 |
primeFBPixelStub | stub do fbq já com fbq('init') |
primeKwaiQueue | kwaiQueue |
Cada prime liga um flag que o guard de dispatch aceita, então todo evento disparado antes do script chegar fica enfileirado e é processado em ordem quando ele carrega. Taboola e Floodlight não precisam de prime: Taboola só recebe conversões de funil profundo (disparadas muito depois do init) e o Floodlight enfileira via gtag independente do init.
Perda possível: apenas visitantes que saem antes do deadline sem interagir (bounce ultra-rápido) — cujos eventos, na prática, nunca chegariam a ser enviados de todo modo.
Racional: tira ~700ms de long tasks de marketing da janela de carregamento (TBT de lab / INP de campo). O mesmo comportamento vale pra todos os clientes — sem branch de bot ou auditoria. Reversível por brand com 1 linha.
Hoje ligado em: 7k-bet-br (engagement, deadline 5000ms), como experimento de performance. As demais brands estão em eager. Vale avaliar com marketing comparando pageviews GA4/Meta/Kwai do dia da ativação contra o baseline.
Configuração por brand
Brand-specific via overrides/<brand>/app/config/analytics/analytics.ts:
export const analyticsConfig: AnalyticsConfig = {
gtmId: undefined, // ← undefined = fallback API (brand.settings.analytics.gtmId)
gtmProxyUrl: null, // ← null = CDN padrão
gtmScriptUrl: null, // ← null = monta a URL a partir de gtmId + proxy
disableGtm: false, // ← kill switch local
// ...
};
Resolução de prioridade
1. analyticsConfig (override de brand)
- gtmId definido (string) → usa esse valor, sobrescreve API
- gtmId === undefined → cai no API fallback
- gtmId === null → desativa GTM explicitamente
2. brand.settings.analytics (vindo do BFF/backoffice)
- usado quando o local é undefined
3. null → tracker desativado
O contrato é 3-state e undefined ≠ null — o código distingue os dois de propósito (usar ?? colapsaria os dois e furaria o disable por brand).
A maioria das brands usa gtmId: undefined e deixa o backoffice resolver via API, o que permite trocar container sem deploy de front. A exceção é 7k-bet-br, que pina o valor (ver tabela abaixo).
Modos de carregamento (3 cenários)
Modo 1 — CDN padrão Google (default)
{ gtmId: "GTM-XXXXXXX" }
Carrega de https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX.
Modo 2 — proxy base URL (gtmProxyUrl)
{
gtmId: "GTM-XXXXXXX",
gtmProxyUrl: "https://stape.brand.com",
}
Carrega de https://stape.brand.com/gtm.js?id=GTM-XXXXXXX. Hoje nenhuma brand usa este modo — quem está em Stape usa o Modo 3.
Modo 3 — URL completa (gtmScriptUrl)
{
gtmId: undefined, // ID continua vindo da API (usado no fallback noscript)
gtmScriptUrl: "https://stape.brand.com/7eqkozwmdmh.js?3qwt=aWQ9R1RNLVRaNVdXQk03&sort=desc",
}
Sobrescreve gtmId e gtmProxyUrl na montagem da URL. É o modo usado quando o path do script não segue /gtm.js?id=XXX — caso típico do Stape com path ofuscado e o container ID codificado em base64 dentro de um query param de nome arbitrário.
Os dois setups Stape vivos usam este modo:
| Brand | gtmScriptUrl (prod) | Container decodificado |
|---|---|---|
betpontobet-bet-br | https://stape.betpontobet.bet.br/7eqkozwmdmh.js?3qwt=aWQ9R1RNLVRaNVdXQk03&sort=desc | GTM-TZ5WWBM7 |
donald-bet-br | https://stape.donald.bet.br/ecisgxpky.js?6p8e2gv8=aWQ9R1RNLTU2WldSTVhE&page=1 | GTM-56ZWRMXD |
Nas duas o gtmScriptUrl é undefined em dev.
disableGtm
Kill switch local. Quando true, o GTM não carrega independente do gtmId resolvido.
- Dev local: as brands setam
disableGtm: isDev() ? true : <valor de prod>pra evitar tracking durante desenvolvimento. - Prod: usado tanto pra brands sem container configurado quanto pra desligar o caminho
dataLayernuma brand que migrou pro anaLayer.
Configuração nas brands
Snapshot — a fonte viva é overrides/*/app/config/analytics/analytics.ts; ver também Por Brand → Config Map.
| Brand | gtmId | Stape (gtmScriptUrl) | disableGtm prod | anaLayer |
|---|---|---|---|---|
7k-bet-br | pinado GTM-59XBN5T7 | não | true | ✅ GTM-59XBN5T7 |
betpontobet-bet-br | API | ✅ GTM-TZ5WWBM7 | false | ✅ GTM-N3X5XTD3 |
donald-bet-br | API | ✅ GTM-56ZWRMXD | false | ✅ GTM-5D9C7445 |
cl-bet7k-com | API | não | true | ✅ GTM-MPMFX38W |
ng-7k-bet | API | não | true | — |
fi-7k-bet | API | não | true | — |
pb-bet | API | não | true | — |
rj-bet | API | não | true | — |
demais (casateste-com, ph-state77-com, pt-state77-com, state77-com, x2b-bet) | API | não | false (default do base) | — |
:::danger 6 brands estão com o GTM DESLIGADO em produção
7k-bet-br, cl-bet7k-com, fi-7k-bet, ng-7k-bet, pb-bet e rj-bet têm disableGtm: true em prod.
Consequência: nessas brands nenhuma tag do container dispara — e como Google Ads e TikTok existem só como tag no container, as conversões dessas duas plataformas não são registradas nessas brands. Meta Pixel, Kwai, Taboola e Floodlight não são afetados (têm SDK próprio no front e são carregados independentemente do disableGtm).
Os casos são diferentes entre si: a 7k migrou pro anaLayer (o tagueamento continua, em outro container); fi-7k-bet e ng-7k-bet estão marcadas no código como desativação temporária; pb-bet e rj-bet ainda têm TODO: Fill in tracking IDs when available. Vale confirmar com marketing quais dessas são intencionais hoje.
:::
:::caution 7k: gtmId pinado + disableGtm: true não é contradição
O container GTM-59XBN5T7 está pinado no analytics.ts de propósito (o backoffice ainda aponta pro container antigo, mais pesado), e o disableGtm está true em prod porque esse mesmo container passou a ser carregado só pelo analayer.ts (l=anaLayer). Deixá-lo nos dois lugares carregaria o mesmo container 2x, com page_view duplicado.
Pra voltar o dataLayer antigo: PROD_DISABLE_GTM_VALUE = false e desligar o anaLayer.
:::
Pra modificar GTM ID de uma brand:
- Sem deploy: muda
brand.settings.analytics.gtmIdno backoffice da brand (BFF). Próxima request lê o valor novo. Só funciona nas brands emgtmId: undefined. - Com deploy: edita
overrides/<brand>/app/config/analytics/analytics.ts→ PR + merge + deploy.
Whitelist de eventos por brand
analyticsSchemaConfig.enabledSpecificEvents permite que uma brand libere apenas N chaves de evento pelo dispatcher GTM; todo o resto no-opa silenciosamente.
// overrides/<brand>/app/config/analytics/schema.ts
export const analyticsSchemaConfig: AnalyticsSchemaConfig = {
enabledSpecificEvents: [
"pageView", "signUp", "login",
"pixGenerated", "astropayDeposit", "creditCardDeposit",
"oxxoDeposit", "speiDeposit", "worldlineDeposit",
"firstDeposit", "purchase",
],
};
Sem override (o default), enabledSpecificEvents é null e todos os eventos disparam.
Hoje ligado em betpontobet-bet-br e donald-bet-br, com as mesmas 11 chaves acima. Razão dupla: os containers dessas brands não têm trigger pros eventos mais novos, e os eventos carregam PII (hasheada) que um trigger catch-all encaminharia.
É por isso que "por que search não dispara no Betpontobet?" não é bug: o evento está fora da whitelist. Pra liberar um evento novo: (1) cria a tag + trigger no container da brand, (2) adiciona a chave camelCase na lista, (3) deploy. O sync é manual — deploy sem o passo 2 no-opa o evento (default seguro).
fetch protection
O front aplica uma proteção global em window.fetch ao carregar o GTM. Razão:
containers GTM frequentemente carregam scripts de analytics que monkey-patcham window.fetch com interceptors. Esses interceptors tipicamente assumem que o primeiro argumento é uma string URL, mas o React Router internamente chama fetch(Request) com objeto Request — quebrando o interceptor com erros como Cannot read properties of undefined (reading 'includes').
A proteção (em app/utils/metrics/gtm.ts) usa Object.defineProperty pra:
- interceptar qualquer assignment futuro de
window.fetch; - wrapear o fetch patcheado num try/catch;
- em caso de erro, fazer fallback pro nativo (erros legítimos de rede são re-lançados normalmente).
Resultado: GTM e seus scripts continuam funcionando, mas fetch quebrado não derruba navegação.
Eventos disparados
Lista canônica em Catálogo de eventos. Resumo:
page_view— toda navegação SPAsign_up— signup bem-sucedidologin— login bem-sucedidopix_generated/astropayDeposit/creditCardDeposit/ etc — quando user inicia depósitopix_confirmado_ftd/pix_confirmado— quando depósito é confirmado (FTD ou rebill)first_deposit,purchase— réplicas de FTD pra GA4 ecommercewithdraw(event namesaque) — saque solicitadoview_game_page,view_promotion,select_promotion,search,view_item_list— eventos de UI
Cada evento dispara também via outros canais (Meta Pixel, Kwai, Taboola, Floodlight, Pendo) — ver a matriz completa em Catálogo de eventos.
Google Consent Mode v2
GTM honra GCM v2 quando os defaults de consent são injetados no dataLayer antes dele carregar — o que o initGTM (e o primeGTMQueue, no modo engagement) garante. A política de default vigente está em Consent LGPD.
O wrapper gtag usado pra emitir os comandos de consent fica em window.__gtagConsent (nome deliberadamente neutro).
Como debuggar
GTM Preview Mode
- Acessa o container GTM (https://tagmanager.google.com)
- Clica em Preview (canto superior direito)
- Cola a URL do ambiente (local, stage ou prod). A porta do dev local não é sempre 5173 — confira o
DEV_PORTdo seu workspace. - Browser abre em modo debug — toda tag que dispara aparece no painel
Numa brand com anaLayer, faça o preview do container certo — ver anaLayer.
dataLayer manual
DevTools Console:
window.dataLayer // array com todos os eventos pushados
window.dataLayer[window.dataLayer.length - 1] // último evento
window.anaLayer // container secundário (brands com anaLayer)
window.__gtagConsent // wrapper gtag do Consent Mode
Network tab
Procura por googletagmanager.com ou pelo host do Stape na coluna Domain. Cada evento dispara um request — Network mostra payload, status, latência.
Anti-patterns
- Reintroduzir gating de script por LCP / primeira interação / "delay pra Lighthouse". Já foi tentado e removido em 2026-06 porque descartava eventos e inflava score sintético. Se precisar tirar peso da janela de carregamento, use
marketingScriptsStrategy: "engagement", que adia só o download e preserva os eventos em fila. - Adicionar pixel novo direto via tag GTM quando o front já tem util pra ele (ex: Kwai, Meta, Taboola, Floodlight). Duplica tracking.
- Mudar
gtmIdem prod sem testar em staging. Tag mal configurada pode disparar evento errado e poluir dashboards. - Ligar
disableGtm: truenuma brand com Webtrends opt-in ativo. Webtrends carrega na mesma lane de marketing — desligar GTM não desliga Webtrends, mas os eventos que o WT injeta nodataLayercompartilhado deixam de ter container pra processá-los. - Ligar GTM primário e anaLayer com o MESMO container ID. Mesmo container carregado 2x → page_view duplicado.
- Assumir que o evento chegou ao container só porque foi pushado. Se a brand tem
enabledSpecificEvents, eventos fora da whitelist no-opam sem erro.