Pular para o conteúdo principal

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

LaneQuem usaQuando roda
Eager (default)GTM, Meta Pixel, Taboola, Kwai, Webtrends, FloodlightNo mount, imediatamente
Engagement (opt-in por brand)os mesmos acima, quando a brand ligaFilas primadas no mount; download na 1ª interação real OU no deadline
DeferredClarity, 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 deadline marketingScriptsMaxDelayMs, o que vier primeiro.

Nenhum evento é descartado no modo engagement. As filas nascem síncronas no mount, via prime:

PrimeCria
primeGTMQueuewindow.dataLayer + o consent default do GCM v2
primeFBPixelStubstub do fbq já com fbq('init')
primeKwaiQueuekwaiQueue

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 undefinednull — 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:

BrandgtmScriptUrl (prod)Container decodificado
betpontobet-bet-brhttps://stape.betpontobet.bet.br/7eqkozwmdmh.js?3qwt=aWQ9R1RNLVRaNVdXQk03&sort=descGTM-TZ5WWBM7
donald-bet-brhttps://stape.donald.bet.br/ecisgxpky.js?6p8e2gv8=aWQ9R1RNLTU2WldSTVhE&page=1GTM-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 dataLayer numa 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.

BrandgtmIdStape (gtmScriptUrl)disableGtm prodanaLayer
7k-bet-brpinado GTM-59XBN5T7nãotrueGTM-59XBN5T7
betpontobet-bet-brAPIGTM-TZ5WWBM7falseGTM-N3X5XTD3
donald-bet-brAPIGTM-56ZWRMXDfalseGTM-5D9C7445
cl-bet7k-comAPInãotrueGTM-MPMFX38W
ng-7k-betAPInãotrue
fi-7k-betAPInãotrue
pb-betAPInãotrue
rj-betAPInãotrue
demais (casateste-com, ph-state77-com, pt-state77-com, state77-com, x2b-bet)APInãofalse (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 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 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.gtmId no backoffice da brand (BFF). Próxima request lê o valor novo. Só funciona nas brands em gtmId: 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 SPA
  • sign_up — signup bem-sucedido
  • login — login bem-sucedido
  • pix_generated / astropayDeposit / creditCardDeposit / etc — quando user inicia depósito
  • pix_confirmado_ftd / pix_confirmado — quando depósito é confirmado (FTD ou rebill)
  • first_deposit, purchase — réplicas de FTD pra GA4 ecommerce
  • withdraw (event name saque) — saque solicitado
  • view_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.

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

  1. Acessa o container GTM (https://tagmanager.google.com)
  2. Clica em Preview (canto superior direito)
  3. Cola a URL do ambiente (local, stage ou prod). A porta do dev local não é sempre 5173 — confira o DEV_PORT do seu workspace.
  4. 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

  1. 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.
  2. Adicionar pixel novo direto via tag GTM quando o front já tem util pra ele (ex: Kwai, Meta, Taboola, Floodlight). Duplica tracking.
  3. Mudar gtmId em prod sem testar em staging. Tag mal configurada pode disparar evento errado e poluir dashboards.
  4. Ligar disableGtm: true numa 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 no dataLayer compartilhado deixam de ter container pra processá-los.
  5. Ligar GTM primário e anaLayer com o MESMO container ID. Mesmo container carregado 2x → page_view duplicado.
  6. Assumir que o evento chegou ao container só porque foi pushado. Se a brand tem enabledSpecificEvents, eventos fora da whitelist no-opam sem erro.