PII por canal — o que sai hasheado e o que sai em texto
Esta página responde a pergunta que aparece em toda discussão de tag nova: "posso mandar o e-mail do usuário?". A resposta depende do canal — o front trata PII de formas diferentes em cada um, e essa diferença precisa estar visível antes de qualquer decisão de tagging.
:::danger Precisa de validação do responsável por LGPD Esta página é uma descrição factual do que o código faz hoje, não uma declaração de conformidade nem um julgamento sobre o que é aceitável. Há uma assimetria documentada abaixo: dois canais ativos tratam os mesmos campos de PII de formas diferentes. O responsável por LGPD / DPO precisa avaliar essa assimetria e decidir o que é intencional antes de qualquer parte deste texto valer como política. :::
Resumo
| Canal | Campos de PII enviados | Formato |
|---|---|---|
GTM (dataLayer) | email, phone, first_name, last_name, dob | SHA-256 |
| anaLayer (container secundário) | email_hash, phone_hash | SHA-256 |
| AppsFlyer | user_email, phone, first_name, last_name, city, region, country | texto puro |
cookie_tracking | — | PII é bloqueada na captura |
GTM — hasheado, sob nomes de chave legados
gtmUserParams() (app/utils/gtm-user-params.ts) enriquece todo evento GTM com 7 chaves:
{
user_id: 12345, // cru — numérico, não é PII
btag_id: "AFIL1", // cru — vem de user.recommended_by
email: "<sha256>",
phone: "<sha256>",
first_name: "<sha256>",
last_name: "<sha256>",
dob: "<sha256>",
}
O detalhe que mais confunde: os nomes das chaves são os legados (email, não email_hash) —
decisão registrada de marketing, pra que as tags de GTM já configuradas continuassem lendo as
mesmas chaves. Só o valor mudou. Uma tag que espera texto nesses campos recebe hash e falha em
silêncio.
Não existem as chaves user_email / user_phone / user_first_name / user_last_name /
user_dob / affiliate_btag no dataLayer.
Como o hash é produzido
SHA-256 via Web Crypto é assíncrono, mas o gtmUserParams é síncrono e roda inline em ~20
disparos. A solução é um cache pré-computado (app/utils/gtm-identity-hash.ts):
- No login / troca de conta, o
useAnalyticsBootstrapchamarefreshGtmIdentityHashes(user). - O cache é limpo antes do refresh (
clearGtmIdentityHashes()), pra não vazar o hash da conta anterior num disparo que ocorra no meio da troca. gtmUserParamslê o cache de forma síncrona. Enquanto o refresh async não resolve, o cache está vazio — o primeiro push pode sair sem os campos de PII. Do seguinte em diante, preenchido.
Normalização antes do hash (paridade com anaLayer / Meta):
| Campo | Normalização |
|---|---|
email | trim + lowercase |
phone | E.164 sem + (DDI + nacional, só dígitos) |
first_name / last_name | trim + lowercase |
dob | só dígitos (YYYYMMDD) |
user_id_hash | SHA-256 do CPF (só dígitos), via hashCpf |
Normalização divergente do lado da plataforma de ads = taxa de match baixa. Se uma tag hasheia por conta própria, ela precisa usar exatamente essas regras — ou, melhor, não hashear de novo.
anaLayer — hasheado, com sufixo explícito
O container secundário usa email_hash / phone_hash (sufixo explícito, ao contrário do GTM
legado) e ancora dedupe em event_id. Detalhes em
Plataformas → anaLayer.
AppsFlyer — texto puro
O payload montado em app/hooks/useAppsFlyer.ts envia PII sem hash:
const payload: AppsFlyerEventPayload = {
user_id: String(user.id ?? ""),
user_email: user.email ?? "", // texto
phone: user.phone ?? "", // texto
first_name: user.first_name ?? "", // texto
last_name: user.last_name ?? "", // texto
city: ... , // texto
region: ... , // texto
country: user.country ?? "",
status, af_referrer, appsflyer_id, advertising_id, app_version,
};
O mesmo payload segue por duas rotas, e em nenhuma delas há hash:
- Bridge nativa —
window.twa.getPostMessageService()recebeJSON.stringify({ event: "logAppsflyerEvent", ...payload })e o APK loga via SDK embarcado. Usada quando está em app mode e a versão do APK é>= postMessageMinVersion. - HTTP —
POST /api/tracking/appsflyer(rota interna do front), que encaminha pros2sUrlda brand. É o fallback quando a bridge não está disponível ou falha.
:::warning A assimetria, dita explicitamente
Os campos email, phone, first_name e last_name saem do cliente hasheados pelo canal GTM
e em texto pelo canal AppsFlyer, para o mesmo usuário, na mesma sessão. city e region só
existem no canal AppsFlyer, e também em texto.
Está registrado aqui como fato observado. A avaliação — se é intencional, se um requisito do SDK do AppsFlyer justifica, se muda — é do responsável por LGPD, não desta doc. :::
Quem dispara o AppsFlyer não é só o app
O gate é enabled && isMobileUA:
const enabled = !!appsFlyerConfig.enabled;
const isMobileUA = /* detecção de UA de telefone */;
const ready = enabled && isMobileUA;
Duas consequências que costumam surpreender:
- Não é exclusivo de app mode. Usuário em browser mobile comum, sem
?app=true, também dispara — logo o payload em texto sai também no contexto web. isAppModenão faz parte do gate; ele só escolhe a rota (bridge vs HTTP).- Desktop e tablet não disparam por nenhuma das rotas (paridade com o filtro phone-only do legado).
Hoje só 7k-bet-br tem enabled: true — as outras 12 brands estão em false, então o canal está
inerte nelas. Ver Config map.
cookie_tracking — PII bloqueada na origem
Na captura de parâmetros de URL, cinco chaves nunca são persistidas, mesmo se vierem na URL
(NEVER_CAPTURE em app/utils/tracking.ts):
const NEVER_CAPTURE = new Set(["user_phone", "user_email", "user_name", "pixel", "currency"]);
Motivo real: macros de anúncio mal configuradas vazam PII na querystring (user_phone=__USER_PHONE__
e variantes). O bloqueio impede que isso chegue ao cookie e, dali, aos dashboards.
Consequências práticas pra quem monta tag
- Não peça "o e-mail em texto no dataLayer". Não existe e não vai existir por esse caminho — o
gtmUserParamshasheia incondicionalmente. - Não hasheie de novo no GTM. O valor já é SHA-256. Hash duplo derruba o match a zero.
- Não confie em PII no primeiro push pós-login. O cache pode ainda estar vazio.
- Enhanced Conversions / CAPI funcionam com hash — é o formato que essas APIs esperam.
- Ao propor tag nova que consome PII, diga de qual canal. GTM e AppsFlyer entregam formatos diferentes para os mesmos campos.