Pular para o conteúdo principal

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çãoComportamento
PersonalizarExpande 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_consent vá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_consent válido existe (já decidiu)
  • Brand desativou cookieConsentPopup no 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.

CategoriaPode desligar no painel?Cookies divulgados hoje
NecessáriosNãocookies_consent, jwt_token, __cf_bm, _cfuvid
PerformanceSim_ga, _ga_*, _gid, _clck, _clsk
FuncionalidadeSimcookie_app_mode, current_lang, topbar_closed
Outros / marketingSimcookie_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 o lastclick (Clever) são escritos pela plataforma mas não aparecem no COOKIE_TABLE.
  • AMP_* (Amplitude) é divulgado no painel, mas não foi localizada nenhuma integração Amplitude no front-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.

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:

  1. a brand não está em modo cosmético (cookieConsentCosmeticOnly !== true); e
  2. existe cookie cookies_consent de 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.

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

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 init matching
  • Eventos disparam mas em modo "no cookies"

Configuração por brand

Dois controles, em lugares diferentes:

ControleOnde viveO que faz
brand.features.cookieConsentPopupbackoffice / BFFtrue → banner renderiza na primeira visita; false → banner não renderiza
featuresConfig.cookieConsentCosmeticOnlyfront, 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

  1. Mudar cookieConsentPopup ou cookieConsentCosmeticOnly em 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.
  2. Não deduza o estado de consent de uma brand a partir de outra. cookieConsentCosmeticOnly varia por brand e é override file-replacement (sem deep-merge) — ver a tabela em Modo Cosmético.
  3. Os 7 sinais precisam ser sempre emitidos explicitamente, inclusive security_storage. Omitir sinal gera warning no GTM.
  4. 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 o update all-denied no boot. Tag que dependa de cookie bloqueado falha em silêncio.
  5. Ao adicionar campo novo em features.ts, propague para todos os overrides. Override é substituição de arquivo inteiro — brand sem o campo recebe undefined, não o default.
  6. Não trate esta página como política publicada até o responsável por LGPD confirmar o comportamento descrito (ver aviso no topo).