Pular para o conteúdo principal

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

CanalCampos de PII enviadosFormato
GTM (dataLayer)email, phone, first_name, last_name, dobSHA-256
anaLayer (container secundário)email_hash, phone_hashSHA-256
AppsFlyeruser_email, phone, first_name, last_name, city, region, countrytexto puro
cookie_trackingPII é 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):

  1. No login / troca de conta, o useAnalyticsBootstrap chama refreshGtmIdentityHashes(user).
  2. O cache é limpo antes do refresh (clearGtmIdentityHashes()), pra não vazar o hash da conta anterior num disparo que ocorra no meio da troca.
  3. gtmUserParams lê 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):

CampoNormalização
emailtrim + lowercase
phoneE.164 sem + (DDI + nacional, só dígitos)
first_name / last_nametrim + lowercase
dobsó dígitos (YYYYMMDD)
user_id_hashSHA-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:

  1. Bridge nativawindow.twa.getPostMessageService() recebe JSON.stringify({ event: "logAppsflyerEvent", ...payload }) e o APK loga via SDK embarcado. Usada quando está em app mode e a versão do APK é >= postMessageMinVersion.
  2. HTTPPOST /api/tracking/appsflyer (rota interna do front), que encaminha pro s2sUrl da 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.
  • isAppMode nã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.

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

  1. Não peça "o e-mail em texto no dataLayer". Não existe e não vai existir por esse caminho — o gtmUserParams hasheia incondicionalmente.
  2. Não hasheie de novo no GTM. O valor já é SHA-256. Hash duplo derruba o match a zero.
  3. Não confie em PII no primeiro push pós-login. O cache pode ainda estar vazio.
  4. Enhanced Conversions / CAPI funcionam com hash — é o formato que essas APIs esperam.
  5. Ao propor tag nova que consome PII, diga de qual canal. GTM e AppsFlyer entregam formatos diferentes para os mesmos campos.