Pular para o conteúdo principal

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",
};
CampoTipoPra quê serve
containerIdstring | nullContainer GTM secundário. Sem fallback API — sempre local. null = brand sem container
enabledboolean?Master switch. Permite manter o containerId declarado mas desligado. Default false
dataLayerNamestring?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)

BrandContainer IDenabled
7k-bet-brGTM-59XBN5T7true
betpontobet-bet-brGTM-N3X5XTD3true
cl-bet7k-comGTM-MPMFX38Wtrue
donald-bet-brGTM-5D9C7445true

Todas as outras brands herdam o default do base (desligado).

Como carrega

O <AnaLayerInitializer /> (montado uma vez no DefaultLayout):

  1. No-op dentro do app (TWA/WebView — ver "Modo app" abaixo), quando enabled é falso, quando containerId é null, ou quando a brand liga o kill switch featuresConfig.thirdPartyScriptsDisabled.
  2. Prime síncrono — injeta os defaults do Google Consent Mode v2 no anaLayer antes de qualquer evento do app, pra que o consent default fique à frente do backlog na fila. Comportamento igual ao do initGTM.
  3. Carrega https://www.googletagmanager.com/gtm.js?id=<containerId>&l=<dataLayerName> — é o &l= que faz o container ler de window.anaLayer em vez de window.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 via scheduleThirdParty.
  • "engagement" (hoje só 7k-bet-br) — o consent + a fila nascem síncronos no mount e o gtm.js só baixa na primeira interação real OU no deadline marketingScriptsMaxDelayMs.

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.

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óprio33 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:

FaseEventos
Navegação / conteúdopage_view, select_content, search, modal_viewed, form_error
Authlogin, sign_up
Depósitobegin_checkout, add_payment_info, deposit_pix_generated, deposit_pix_copied, deposit_pix_failed, deposit_pix_expired, purchase, same_day_ftd, refund
Saquewithdrawal_started, withdrawal_requested, withdrawal_approved, withdrawal_paid, withdrawal_failed, withdrawal_cancelled
Ecommerce / promoview_item_list, select_item, view_promotion, select_promotion
Jogogame_opened, game_session_ended
KYCkyc_started, kyc_completed, kyc_approved, kyc_rejected
Bônuscashback_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:

CampoValor
user_idAuthUser.id quando logado
user_id_hashSHA-256 (hex minúsculo) dos dígitos do CPF (document.number sem não-dígitos)
platformweb | web_mobile | app_mobile
profile_typevip | standard (default standard)
event_timestampISO 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_viewnã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 signupsignup_<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

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 GA4Vem de
promotion_idúltimo segmento do path de actionobrigatório
promotion_namealt do banner (HTML convertido pra texto puro)
creative_nameID 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

  1. Debugar window.dataLayer numa 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. Olhe window.anaLayer.
  2. Ligar enabled: true no analayer.ts mantendo o mesmo container ID como GTM primário (disableGtm: false). Carrega o mesmo container 2x → page_view duplicado. Ao ligar um, desligue o outro.
  3. Assumir que os nomes de evento do anaLayer são os do catálogo antigo. São catálogos diferentes (same_day_ftd vs pix_confirmado_ftd, withdrawal_requested vs saque). Trigger montado com o nome errado nunca dispara.
  4. Esperar email_hash / phone_hash em page_view. A lista de eventos que carregam identidade é fechada — ver acima.
  5. Omitir user_id no lugar de mandar null no logout. O valor anterior persiste no dataLayer e vaza pros eventos seguintes. O código já cuida disso; não "otimize" removendo a chave.