Consent LGPD + Google Consent Mode v2
Esta página descreve o comportamento que o código hoje implementa para o banner de cookies, as 4 categorias e a propagação de sinais do Google Consent Mode v2 (GCM v2).
:::danger Precisa de validação do responsável por LGPD antes de valer como política
O comportamento descrito abaixo mudou de forma substantiva ao longo de 2026: hoje os sinais de GCM
v2 sobem granted por default em todas as brands, o flag cookieConsentCosmeticOnly tem
default true no base, e o banner não tem botão de recusa no nível superior.
Esta página é uma descrição factual do código, não uma declaração de conformidade. O responsável por LGPD / DPO precisa confirmar que o comportamento observado é o pretendido antes de qualquer parte desta página ser tratada como política publicada. Enquanto isso não acontecer, não use este texto como base para resposta a titular, auditoria ou contrato. :::
O banner
Card flutuante ancorado no rodapé, centralizado na horizontal (mobile e desktop), sem backdrop
— a página continua utilizável atrás. Tem print:hidden e é escondido enquanto o drawer mobile
está aberto.
Duas ações no nível superior:
| Ação | Comportamento |
|---|---|
| Personalizar | Expande o painel com toggles individuais por categoria (CookieConsentDetails) |
| Aceitar todos | { necessary, performance, functionality, others } = all true |
Dentro do painel Personalizar, o botão de confirmação troca de rótulo: banner.acceptAll quando
nenhuma categoria opcional está ligada, banner.saveAndClose quando pelo menos uma está.
:::note Não existe botão "Recusar todos" no banner
O botão de recusa foi removido do banner por decisão de produto. O teste do componente trava esse
comportamento explicitamente:
expect(screen.queryByRole("button", { name: "banner.rejectAll" })).toBeNull()
(app/components/consent/__tests__/CookieConsentBanner.test.tsx).
O único caminho de recusa é Personalizar → desligar categorias → salvar — e, em brands com
cookieConsentCosmeticOnly: true, esse caminho também converge para aceitar tudo (ver
Modo Cosmético). Consequência prática: nessas brands não existe caminho de
recusa efetivo pela UI. Item para revisão do responsável por LGPD.
:::
Persistido em cookie cookies_consent com TTL de 365 dias
(CONSENT_COOKIE_MAX_AGE = 365 * 24 * 60 * 60). Depois disso o banner reaparece.
Quando o banner aparece
- Brand tem
brand.features.cookieConsentPopup === true(vem do backoffice/BFF) - User não tem cookie
cookies_consentválido (não decidiu ainda OU expirou)
O banner nasce visível no SSR quando a brand tem o popup habilitado — o <p> de descrição é o
elemento LCP da home mobile. Quem já consentiu é escondido antes do paint por um script inline
do root.tsx (atributo data-consent-given no <html> + CSS), e o estado React sincroniza na
hidratação.
Quando o banner é suprimido
cookies_consentválido existe (já decidiu)- Brand desativou
cookieConsentPopupno backoffice - Drawer/menu mobile está aberto (
mobileMenuOpen) - Impressão (
print:hidden)
:::caution Não há mais bypass de bot
O componente não tem gate de bot: shouldRender = visible && !mobileMenuOpen. Bots e auditorias
sintéticas (Lighthouse/PSI) veem o mesmo banner que um usuário real — consistente com a remoção
do branch de auditoria sintética do carregamento de scripts em 2026-06.
:::
O banner fica em z-[72], deliberadamente à frente da família de modais de auth
(z-[70]) — sem isso o bottom-sheet de cadastro no mobile cobria o banner. Fica abaixo dos
dropdowns internos de formulário (z-[110]/z-[120]) e dos overlays de sistema.
4 categorias de cookies
A lista que o usuário vê no painel Personalizar é o array COOKIE_TABLE em
app/utils/consent.ts — é essa a divulgação LGPD/GDPR de fato. Qualquer divergência entre esta
tabela e o array é discrepância de divulgação, não só gap de doc. Confira o array antes de
responder titular ou auditoria.
| Categoria | Pode desligar no painel? | Cookies divulgados hoje |
|---|---|---|
| Necessários | Não | cookies_consent, jwt_token, __cf_bm, _cfuvid |
| Performance | Sim | _ga, _ga_*, _gid, _clck, _clsk |
| Funcionalidade | Sim | cookie_app_mode, current_lang, topbar_closed |
| Outros / marketing | Sim | cookie_tracking, cookie_referrer, _fbp, ns-kwai-click-id, AMP_*, __smartico_ls_* |
:::warning Divergências conhecidas nessa divulgação — para revisão
- O cookie de remarketing da brand (
<rmkCookie>e<rmkCookie>_aud, ex:rmk7k/rmk7k_aud) e olastclick(Clever) são escritos pela plataforma mas não aparecem noCOOKIE_TABLE. AMP_*(Amplitude) é divulgado no painel, mas não foi localizada nenhuma integração Amplitude nofront-web-base. Pode ser resíduo da divulgação do legado Vue.
Nenhum dos dois é decisão de documentação — ambos exigem confirmação do responsável por LGPD, e a correção (nos dois sentidos) é mudança de código. :::
O estado inicial dos toggles no painel é { necessary: true, performance: false, functionality: false, others: false }. Isso é o estado da UI, não o estado dos sinais de GCM v2 — os sinais
sobem granted independentemente (ver a seção seguinte).
"Aceitar com só necessary toggled" → coerce pra Aceitar Tudo
Paridade legado: se user clica "Salvar" no painel "Personalizar" com apenas necessary ligado (todas outras desativadas), o sistema coerce pra Aceitar Tudo.
Razão: "só necessários" é equivalente a "Recusar" (mesma decisão), mas se user ativamente foi até "Personalizar" e clicou Salvar, é UX confusa retornar pra mesma decisão. Comportamento legado coerce — mantemos.
Google Consent Mode v2 (GCM v2)
GCM v2 é o protocolo do Google pra propagar consent pros tags GTM. Requer 7 sinais no dataLayer antes do GTM carregar.
Mapping: 4 categorias → 7 sinais GCM
necessary → security_storage (sempre granted, GCM exige explícito)
performance → analytics_storage
functionality → functionality_storage
+ personalization_storage
others → ad_storage
+ ad_user_data
+ ad_personalization
Diagrama do mapping
Defaults antes do GTM carregar — hoje é granted para toda brand
O módulo app/utils/metrics/consent.ts declara a política no próprio docstring:
"Policy: granted-by-default (inverted)."
injectConsentDefaults() sempre injeta os 7 sinais como granted, para todas as brands,
e sem wait_for_update:
// Injetado no dataLayer ANTES do GTM carregar — SEMPRE este shape
gtag('consent', 'default', {
analytics_storage: 'granted',
ad_storage: 'granted',
ad_user_data: 'granted',
ad_personalization: 'granted',
functionality_storage: 'granted',
personalization_storage: 'granted',
security_storage: 'granted',
});
Motivo registrado no código: o modelo anterior injetava denied + wait_for_update de 500ms, e
nessa janela GA4/Google Ads processavam hits em modo modelado/cookieless — as conversões
observadas caíam em relação ao app Nuxt legado, que não gateava nada.
O único ramo condicional é o inverso: applyDenied
Não existe mais um update que libera sinais no boot. Existe um update que os rebaixa:
// Só quando opts.applyDenied === true
gtag('consent', 'update', DENIED_SIGNALS);
applyDenied: true é passado pelos inicializadores (primeGTMQueue em metrics/gtm.ts,
initFloodlight em metrics/floodlight.ts, AnaLayerInitializer) sob duas condições
simultâneas:
- a brand não está em modo cosmético (
cookieConsentCosmeticOnly !== true); e - existe cookie
cookies_consentde visita anterior em que o usuário desligou todas as categorias opcionais (!performance && !functionality && !others).
Ou seja: o boot é sempre granted; a recusa registrada de uma visita anterior é reaplicada
imediatamente depois, e apenas nas brands não-cosméticas.
Update depois de uma escolha ao vivo
updateConsentSignals(prefs) continua sendo chamado pelos handlers de clique do banner, mapeando
as 4 categorias nos 7 sinais. É o caminho da escolha feita naquela visita.
Global de inspeção: window.__gtagConsent
O wrapper gtag usado para emitir os comandos de consent é guardado em
window.__gtagConsent (nome deliberadamente neutro). É por ali que se inspeciona/reemite consent
no console — ver Debug.
Modo "cosmetic-only" (cookieConsentCosmeticOnly)
featuresConfig.cookieConsentCosmeticOnly tem default true no base e controla se a recusa
do usuário tem efeito — não o estado default dos sinais. Detalhes, semântica atual e valores por
brand em Modo Cosmético.
Migração de cookie legado (padrão canônico de rename)
Duas migrações one-shot acontecem na leitura, em getConsentPreferences():
1. Rename do cookie. LEGACY_CONSENT_COOKIE_NAMES = ["cactusCookiesConsent"] é
lido uma vez, adotado e então deletado (setClientCookie(legacyName, "", 0)) — nunca escrito
de novo. É o que preserva o consent de quem já visitou, sem que o nome antigo (que carregava o
termo da plataforma) volte a existir no browser. É o exemplo canônico do padrão descrito em
Cookies → anti-pattern de rename.
2. Formato do valor. Versões anteriores usavam array (["necessary", "performance"]). O
parser detecta e converte para objeto, preservando as escolhas.
// Cookie legado (array)
["necessary", "performance"]
// Lido e convertido pra
{
necessary: true,
performance: true,
functionality: false,
others: false,
timestamp: 1731000000000
}
Sem necessidade de re-prompt — user mantém suas escolhas.
Como debuggar
Cookie
DevTools → Cookies → cookies_consent:
{
"necessary": true,
"performance": true,
"functionality": false,
"others": true,
"timestamp": 1731000000000
}
GCM v2 signals
// Console
window.dataLayer.filter(e => e[0] === "consent")
window.__gtagConsent // wrapper gtag usado pelo módulo de consent
Mostra os comandos default e update. Em brand não-cosmética com recusa registrada em visita
anterior você deve ver dois comandos: default all-granted seguido de update all-denied.
Em brand cosmética, ou sem recusa registrada, só o default.
Em brands com anaLayer habilitado o mesmo par é injetado no dataLayer anaLayer antes do
segundo container carregar — inspecione window.anaLayer também.
Pixel comportamento limitado
Quando os sinais estão denied (caminho applyDenied ou recusa ao vivo), o Meta Pixel:
- Não escreve
_fbp,_fbc - Não dispara
initmatching - Eventos disparam mas em modo "no cookies"
Configuração por brand
Dois controles, em lugares diferentes:
| Controle | Onde vive | O que faz |
|---|---|---|
brand.features.cookieConsentPopup | backoffice / BFF | true → banner renderiza na primeira visita; false → banner não renderiza |
featuresConfig.cookieConsentCosmeticOnly | front, app/config/features/features.ts (+ overrides) | default true; controla se a recusa do usuário tem efeito — ver Modo Cosmético |
São independentes: desligar o popup não muda os sinais de GCM v2 (o default all-granted
continua sendo injetado quando o GTM/Floodlight/anaLayer inicializa).
Cuidados operacionais
- Mudar
cookieConsentPopupoucookieConsentCosmeticOnlyem produção é decisão conjunta. Envolve marketing e o responsável por LGPD, e o efeito é imediato no próximo deploy — registre a aprovação antes. - Não deduza o estado de consent de uma brand a partir de outra.
cookieConsentCosmeticOnlyvaria por brand e é override file-replacement (sem deep-merge) — ver a tabela em Modo Cosmético. - Os 7 sinais precisam ser sempre emitidos explicitamente, inclusive
security_storage. Omitir sinal gera warning no GTM. - Ao desenvolver tag GTM nova, teste também o estado
denied. Ele é alcançável: brand não-cosmética + recusa registrada em visita anterior dispara oupdateall-deniedno boot. Tag que dependa de cookie bloqueado falha em silêncio. - Ao adicionar campo novo em
features.ts, propague para todos os overrides. Override é substituição de arquivo inteiro — brand sem o campo recebeundefined, não o default. - Não trate esta página como política publicada até o responsável por LGPD confirmar o comportamento descrito (ver aviso no topo).