anaLayer (GTM secundário)
O anaLayer é uma segunda camada de tagueamento GA4 — um container GTM próprio por brand, que lê de um dataLayer customizado (window.anaLayer) em vez do window.dataLayer usado pelo stack de analytics antigo. Os dois containers ficam totalmente isolados: nenhum evento do anaLayer aparece no dataLayer e vice-versa.
:::danger Na 7k o GTM primário está DESLIGADO — só o anaLayer roda
Em overrides/7k-bet-br/app/config/analytics/analytics.ts o disableGtm é true em produção (PROD_DISABLE_GTM_VALUE = true), e o analayer.ts da 7k está enabled: true. Ou seja: na 7k o container GTM-59XBN5T7 é carregado APENAS pelo anaLayer (l=anaLayer), e o caminho window.dataLayer não carrega container nenhum.
Consequência prática pra quem opera GTM: debugar window.dataLayer na 7k não mostra nada. Os eventos estão em window.anaLayer, e o catálogo é outro — nomes e payloads diferentes dos do dataLayer. Um nome do catálogo antigo (pix_confirmado_ftd, saque, second_deposit) não existe no anaLayer; consulte o inventário abaixo pra achar o evento correspondente antes de montar trigger.
Os dois nunca devem ficar ligados ao mesmo tempo com o mesmo container ID: seria o mesmo container carregado 2x (l=dataLayer + l=anaLayer), com page_view duplicado.
:::
Configuração por brand
// overrides/<brand>/app/config/analytics/analayer.ts
import type { AnaLayerConfig } from "~/types/analayer-config";
import { isDev } from "~/utils/env";
const DEV_CONTAINER_ID = null;
const PROD_CONTAINER_ID = "GTM-59XBN5T7";
export const anaLayerConfig: AnaLayerConfig = {
containerId: isDev() ? DEV_CONTAINER_ID : PROD_CONTAINER_ID,
enabled: true,
dataLayerName: "anaLayer",
};
| Campo | Tipo | Pra quê serve |
|---|---|---|
containerId | string | null | Container GTM secundário. Sem fallback API — sempre local. null = brand sem container |
enabled | boolean? | Master switch. Permite manter o containerId declarado mas desligado. Default false |
dataLayerName | string? | Nome do dataLayer customizado. Default "dataLayer" — tem que ser diferente disso pra manter o isolamento |
Gate único: só dispara quando enabled === true E containerId é non-null. Não existe checagem de brand em nenhum call site — o gate vive todo no pushAnaLayerEvent. Default do base: { containerId: null, enabled: false } (no-op).
Convenção containerId: null em dev: todas as brands ativas usam isDev() ? null : "GTM-...", espelhando o disableGtm do GTM primário. Em dev local o observer/tracker roda mas nada é enviado.
Brands ativas (4 hoje — lista viva em overrides/*/app/config/analytics/analayer.ts)
| Brand | Container ID | enabled |
|---|---|---|
7k-bet-br | GTM-59XBN5T7 | true |
betpontobet-bet-br | GTM-N3X5XTD3 | true |
cl-bet7k-com | GTM-MPMFX38W | true |
donald-bet-br | GTM-5D9C7445 | true |
Todas as outras brands herdam o default do base (desligado).
Como carrega
O <AnaLayerInitializer /> (montado uma vez no DefaultLayout):
- No-op dentro do app (TWA/WebView — ver "Modo app" abaixo), quando
enabledé falso, quandocontainerIdénull, ou quando a brand liga o kill switchfeaturesConfig.thirdPartyScriptsDisabled. - Prime síncrono — injeta os defaults do Google Consent Mode v2 no
anaLayerantes de qualquer evento do app, pra que o consent default fique à frente do backlog na fila. Comportamento igual ao doinitGTM. - Carrega
https://www.googletagmanager.com/gtm.js?id=<containerId>&l=<dataLayerName>— é o&l=que faz o container ler dewindow.anaLayerem vez dewindow.dataLayer.
O download respeita a marketingScriptsStrategy da brand, na mesma lane do GTM primário (ver GTM → Estratégia de carregamento):
"eager"(default) — baixa no mount viascheduleThirdParty."engagement"(hoje só7k-bet-br) — o consent + a fila nascem síncronos no mount e ogtm.jssó baixa na primeira interação real OU no deadlinemarketingScriptsMaxDelayMs.
Nenhum evento se perde no modo engagement: o pushAnaLayerEvent empurra no array incondicionalmente e o container processa o backlog em ordem quando carrega.
O fallback <noscript> do container secundário está desativado (comentado no código) — o ns.html estava sendo requisitado e bloqueando.
Consent Mode v2
O AnaLayerInitializer injeta os 7 sinais do GCM v2 no anaLayer antes do container carregar, espelhando exatamente a política do initGTM (o util compartilhado é hardcoded pro window.dataLayer, por isso o anaLayer tem a sua própria cópia). O rebase pra denied só acontece quando a brand não está em modo cosmético e o usuário recusou tudo numa visita anterior.
A política de default em si (granted vs denied) é a mesma do GTM primário e está descrita em Privacidade → Consent LGPD.
Eventos
O anaLayer tem catálogo próprio — 33 eventos hoje, definidos como union tipada em app/analytics/analayer/events.ts (AnaLayerEvent), que é a lista viva. Não são os mesmos nomes do catálogo do dataLayer. Consulte o arquivo pra assinatura exata de cada payload; o inventário atual:
| Fase | Eventos |
|---|---|
| Navegação / conteúdo | page_view, select_content, search, modal_viewed, form_error |
| Auth | login, sign_up |
| Depósito | begin_checkout, add_payment_info, deposit_pix_generated, deposit_pix_copied, deposit_pix_failed, deposit_pix_expired, purchase, same_day_ftd, refund |
| Saque | withdrawal_started, withdrawal_requested, withdrawal_approved, withdrawal_paid, withdrawal_failed, withdrawal_cancelled |
| Ecommerce / promo | view_item_list, select_item, view_promotion, select_promotion |
| Jogo | game_opened, game_session_ended |
| KYC | kyc_started, kyc_completed, kyc_approved, kyc_rejected |
| Bônus | cashback_credited |
Standard fields (em todo push)
Montados uma vez após a hidratação do auth (leitura da store reativa useAccountsStore, não do snapshot SSR) e mesclados em todo evento:
| Campo | Valor |
|---|---|
user_id | AuthUser.id quando logado |
user_id_hash | SHA-256 (hex minúsculo) dos dígitos do CPF (document.number sem não-dígitos) |
platform | web | web_mobile | app_mobile |
profile_type | vip | standard (default standard) |
event_timestamp | ISO 8601 UTC |
Regra dos índices (importa pra quem monta relatório): as chaves user_id / user_id_hash vão em todo push, mesmo undefined. O valor varia: real quando logado, undefined pra anônimo-desde-o-início, e null após logout. O motivo é que o dataLayer guarda o valor de um push pro outro na mesma página — omitir a chave não limpa, e o user_id de uma sessão logada vazaria pros eventos seguintes (o "usuário 0"). Só o null apaga no GA4.
days_since_signup não é standard field — é param exclusivo do page_view, com a mesma regra anti-vazamento.
email_hash / phone_hash — lista fechada
Os hashes de e-mail e telefone são event-scoped: só vão nos 15 eventos de EVENTS_WITH_IDENTITY_HASHES (login, sign_up, begin_checkout, add_payment_info, os 4 deposit_pix_*, purchase, same_day_ftd, refund, view_promotion, select_promotion, view_item_list, select_item). Qualquer outro evento — inclusive page_view — não envia e-mail/telefone.
Ambos são SHA-256 client-side (Web Crypto). E-mail é trim().toLowerCase() antes do hash; telefone é E.164 sem + (dígitos do DDI + dígitos nacionais). Nunca sai valor em claro.
event_id — âncora de dedup GA4
Todo evento que tem uma âncora de ID recebe um event_id determinístico no formato <event>_<âncora>, pra que o GA4 possa deduplicar o hit client contra o hit server-side (Measurement Protocol). As âncoras vêm de IDs ecoados do backend, nunca gerados no client. Eventos sem âncora (page_view, login, begin_checkout, …) não recebem event_id.
Exceção: sign_up ancora no user_id_hash dos standard fields e usa o prefixo signup → signup_<user_id_hash>.
Higiene de ecommerce
Quando o evento carrega um objeto ecommerce aninhado, o push manda { ecommerce: null } antes — pra que o container nunca faça merge de items[] sobrando de um evento anterior.
Modo app (TWA): anaLayer → AppsFlyer
Dentro do app (?app=true / ?is_twa=true / sessionStorage["cookie_app_mode"]) o anaLayer não vai pro GTM — nem o container é carregado, nem o push chega ao dataLayer. Em vez disso, os mesmos eventos são encaminhados pro AppsFlyer via SDK nativo (forwardAnaLayerToAppsFlyer):
- eventos cobertos pelo mapeamento do marketing → nome/params padrão do AppsFlyer (
af_*); - demais eventos → custom, com o nome do anaLayer.
O eventValue do AppsFlyer é um mapa plano, então o ecommerce aninhado é achatado (escalares + campos do 1º item de items[]).
Widgets que o anaLayer instrumenta
Popup do Smartico → modal_viewed
O Smartico entrega popups de campanha como um <iframe srcdoc> anexado ao <body>. Sendo documento isolado, o markup do popup não consegue pushar no anaLayer da página — a impressão tem que ser detectada de fora.
O <AnaLayerSmarticoPopup /> usa um único MutationObserver que cobre todo popup, qualquer que seja o conteúdo, observando dois seletores de engine: .__btgPromoHolder (o holder do popup srcdoc — o id varia, por isso a chave é a classe) e [data-ao-animaze-show] (shell inline do engine AdOfferio). Cada popup que entra no DOM dispara um modal_viewed com modal_category: "promo", deduplicado por nó via WeakSet (reflow de animação não conta duas vezes).
Em dev o observer roda e loga as detecções, pra validar seletor sem o push (que está desligado por containerId: null).
Banners → view_promotion / select_promotion
O ecommerce de promoção é derivado dos campos que o backoffice já manda em cada banner — sem campo novo no core nem dependência de backend:
| Campo GA4 | Vem de |
|---|---|
promotion_id | último segmento do path de action — obrigatório |
promotion_name | alt do banner (HTML convertido pra texto puro) |
creative_name | ID da imagem no Cloudflare Images |
item_category (em items[]) | primeiro segmento do path de action |
Banner sem promotion_id extraível não dispara evento — promoção sem identidade não é rastreável.
page_view
O <AnaLayerPageView /> dispara em mudança real de pathname, lendo page_location / page_path / page_title do documento vivo. Explicitamente não re-dispara em:
- abertura de modal (modais de auth/depósito vivem em store Zustand, não em rota);
- navegação que só muda search params na mesma rota (
?provider=x,?open=pix); - flip de auth (login/logout via modal).
logged_in é lido de forma não-reativa no momento do disparo, pra ficar fresco sem virar gatilho de re-disparo.
Como debuggar
window.anaLayer // array de eventos do container secundário
window.anaLayer[window.anaLayer.length - 1] // último evento
window.dataLayer // GTM primário — VAZIO de container na 7k
No GTM Preview Mode, abra o preview do container secundário (o ID da tabela acima), não o do backoffice. Na Network, os dois containers aparecem como googletagmanager.com/gtm.js — diferencie pelo id= e pelo l=anaLayer na query string.
Anti-patterns
- Debugar
window.dataLayernuma brand que roda anaLayer. Na 7k não há container no dataLayer — você vai ver um array vazio e concluir que o tracking está quebrado. Olhewindow.anaLayer. - Ligar
enabled: truenoanalayer.tsmantendo o mesmo container ID como GTM primário (disableGtm: false). Carrega o mesmo container 2x → page_view duplicado. Ao ligar um, desligue o outro. - Assumir que os nomes de evento do
anaLayersão os do catálogo antigo. São catálogos diferentes (same_day_ftdvspix_confirmado_ftd,withdrawal_requestedvssaque). Trigger montado com o nome errado nunca dispara. - Esperar
email_hash/phone_hashempage_view. A lista de eventos que carregam identidade é fechada — ver acima. - Omitir
user_idno lugar de mandarnullno logout. O valor anterior persiste no dataLayer e vaza pros eventos seguintes. O código já cuida disso; não "otimize" removendo a chave.