GA4: ligando o Consent Mode ao banner de cookies
Olá meus Unicórnios! 🦄✨
Sabe aquele site que tem o banner de cookies bonitinho, com "Aceitar", "Rejeitar" e "Customizar", e tem o Google Analytics instalado? 😊 Pois é. Eu fui mexer num desses semana dessas e descobri uma coisa que me deixou com cara de boba: as duas coisas estavam lá, funcionando perfeitamente — e não conversavam.
O banner guardava a escolha da pessoa num cookie. O GA4 carregava no rodapé e media tudo. Cada um no seu quadrado, feliz da vida. Só que o GA4 nunca ficava sabendo que alguém tinha clicado em "Rejeitar". Ele media igualzinho nos dois casos. 😳
Ou seja: o banner era decoração. E é exatamente esse buraco que o Consent Mode preenche. Vem comigo que hoje a gente liga os dois — e o código é bem mais curto do que você imagina.
Uma coisa importante antes de começar: o banner que eu uso aqui é o vanilla-cookieconsent, do Orest Bida — aquele mesmo de JavaScript puro, sem jQuery e sem dependência nenhuma, que muita gente já tem instalado sem saber o nome. É de lá que vêm os retornos de consentimento deste artigo:
Se você ainda não tem banner nenhum no site, instalar essa biblioteca do zero é assunto de outro artigo — aqui eu parto dela já funcionando e trato só de uma coisa: ligar o banner ao GA4. 😊
🕵️ Como descobrir se o seu site tem esse buraco
Antes de escrever qualquer linha, vale conferir se o problema é o seu
também. É rápido: abra o código-fonte da página e procure por
gtag. Numa implantação com esse buraco, você vai achar algo
assim no rodapé, e só isso:
<script async src="https://www.googletagmanager.com/gtag/js?id=G-EXEMPLO123"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-EXEMPLO123');
</script>
Esse é o trecho que o próprio Google manda copiar, e ele está certo
para quem não precisa de consentimento. O sinal de que falta o Consent
Mode é o que não está ali: nenhuma linha com
gtag("consent", ...) em lugar nenhum da página.
Depois procure o arquivo de configuração do banner — aquele que chama
o CookieConsent.run({...}). Numa implantação com o buraco,
ele tem as categorias declaradas e as traduções todas caprichadas, mais
ou menos assim:
CookieConsent.run({
categories: {
necessary: { readOnly: true },
analytics: {},
marketing: {}
},
language: {
default: "pt_BR",
translations: { pt_BR: { /* os textos do banner */ } }
}
});
Repare no que não tem aí: nenhum
onConsent, nenhum onChange, nenhuma menção a
gtag. O banner coleta a escolha lindamente, guarda no
cookie dele… e não conta para ninguém. Está confirmado: os dois vivem
no mesmo site sem se falar.
🚦 Os quatro sinais que o Google escuta
O Consent Mode não é uma biblioteca nem um plugin. É só uma
conversa: você usa a mesma função gtag() que
já existe e manda um comando consent dizendo o que pode e o
que não pode. O Google escuta quatro sinais.
Os nomes deles são da API do Google e ficam em inglês no código — não dá para traduzir, senão para de funcionar. Mas o que cada um significa, em português:
| Sinal | O que ele libera |
|---|---|
analytics_storage | Os cookies de análise. É o que permite reconhecer que é a mesma pessoa voltando, contar usuários e gerar os relatórios de comportamento. |
ad_storage | Os cookies ligados a publicidade. |
ad_user_data | O envio dos dados do usuário ao Google com finalidade publicitária. |
ad_personalization | Usar a pessoa em públicos personalizados e remarketing. |
Se o seu site só tem Analytics e nenhuma campanha de anúncios, a
tentação é declarar só o analytics_storage e ir embora.
Não faça isso: declare os quatro. Custa três linhas a mais, e o painel
do GA4 tem uma tela que confere sinal por sinal — os três de publicidade
aparecem lá do mesmo jeito, e ficam marcados como ausentes se você
não mandar nada.
Aliás, é essa tela que serve de conferência final. Ela fica em Administrador → Coleta e modificação de dados → Configurações de consentimento, mostra os indicadores separados em "análise de comportamento" e "publicidade", e diz se cada parâmetro está sendo recebido. Quando está tudo certo, ela dá um selo verde de Excelente e a frase "os indicadores de consentimento estão ativos neste fluxo". Vale olhar depois de publicar — mas repare que esse painel só conta o que chegou. Quem prova que a coisa funciona de verdade é o navegador, e a gente chega lá no fim. 🔍
⛔ O padrão tem de ser "negado" — e vir antes de tudo
Aqui mora a parte mais importante do artigo. Se você só for ler um pedaço, leia este. 🙏
O GA4 não vem negado de fábrica. Se você nunca declarar nada sobre consentimento, ele mede normalmente, grava o cookie e segue a vida — que é o comportamento certo para quem não precisa de banner. Então declarar o estado padrão como negado não é "reforçar" o que já era: é inverter o padrão.
E isso se faz com consent no modo default:
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
function definirConsentimentoPadrao() {
gtag("consent", "default", {
"analytics_storage": "denied",
"ad_storage": "denied",
"ad_user_data": "denied",
"ad_personalization": "denied",
// Segura a batida por 500 ms esperando o update do banner. Sem isso,
// quem aceita rapido ainda tem a primeira batida contada como negada.
"wait_for_update": 500
});
}
definirConsentimentoPadrao();
Agora o detalhe que estraga tudo se você errar: este bloco
precisa vir antes do script do GA4. Antes mesmo da tag
<script> que baixa o gtag/js.
Por quê? Porque o gtag("config", ...) é o momento em que
o GA4 decide o que fazer. Ele lê o estado de consentimento naquele
instante e dispara a primeira batida. Um default que chegue
depois disso não desfaz nada — a batida já saiu, e o cookie já
foi gravado.
E como isso não dá erro nenhum, é o tipo de coisa que passa despercebida para sempre. No site que eu estava mexendo, o problema era exatamente esse tipo de ordem: a tag do GA saía numa linha do rodapé, e o arquivo do banner era incluído quarenta linhas depois. Mesmo que o banner tivesse o código de consentimento — e não tinha — ele chegaria tarde demais.
Sobre o wait_for_update: ele resolve um caso específico e
chato. O banner costuma demorar uns milissegundos para aparecer, e uma
pessoa rápida no clique pode aceitar depois que a primeira
batida já foi embora como negada. Esses 500 ms pedem ao GA4 que segure a
batida um pouquinho, esperando uma possível atualização. É um número
folgado; não precisa exagerar.
✅ Avisando o Google no momento da escolha
Com o padrão negado, o site já está em conformidade — só que ele nunca vai medir ninguém, porque nada volta a ser permitido. 😅 Falta a outra metade: avisar o Google quando a pessoa escolher.
Isso é o update. Diferente do default, ele
roda no momento do clique, sem recarregar a página.
E aqui entra a única função da biblioteca que você precisa conhecer:
CookieConsent.acceptedCategory(). Ela recebe o nome de uma
categoria e devolve true ou false — é a ponte
entre o vocabulário do banner (analytics,
marketing) e o vocabulário do Google
(analytics_storage, ad_storage e companhia):
function atualizarConsentimento() {
var analiseAceita = CookieConsent.acceptedCategory("analytics");
var anunciosAceitos = CookieConsent.acceptedCategory("marketing");
var valorAnalise = "denied";
if (analiseAceita) {
valorAnalise = "granted";
}
var valorAnuncios = "denied";
if (anunciosAceitos) {
valorAnuncios = "granted";
}
gtag("consent", "update", {
"analytics_storage": valorAnalise,
"ad_storage": valorAnuncios,
"ad_user_data": valorAnuncios,
"ad_personalization": valorAnuncios
});
}
Repare que eu escrevi com if em vez de operador ternário.
É mais linha, sim, e é de propósito: quem estiver copiando isto às
onze da noite entende na primeira leitura o que está acontecendo. 🌙
O mapa entre as duas linguagens é este — duas categorias do banner alimentando quatro sinais do Google:
| Categoria do banner | Sinais do Google que ela libera |
|---|---|
necessary | nenhum — é readOnly: true, sempre ativa, e não tem sinal correspondente |
analytics | analytics_storage |
marketing | ad_storage, ad_user_data e ad_personalization |
E aí liga a função no banner, em dois lugares. Isso
mesmo, dois! 🤯 Os dois são retornos do próprio
CookieConsent.run({...}), cada um cobrindo uma situação
diferente — e esquecer um deles é o segundo erro mais comum depois da
ordem:
CookieConsent.run({
categories: {
necessary: { readOnly: true },
analytics: {},
marketing: {}
},
// Roda no momento da escolha e a cada carregamento com escolha ja salva.
onConsent: atualizarConsentimento,
// Roda quando a pessoa muda de ideia sem recarregar a pagina.
onChange: atualizarConsentimento,
// ...o resto da sua configuracao de textos continua igual
});
O onConsent cobre dois momentos: o clique inicial, e
todas as visitas seguintes. Essa segunda parte é a que
pega todo mundo, então vale devagar: quem já escolheu ontem
não vê o banner de novo. Nenhum botão é clicado, nenhum evento
de clique acontece — e mesmo assim o GA4 precisa ser avisado, porque
cada carregamento de página começa do zero com o
default negado lá em cima. É o onConsent que
faz esse aviso sozinho, lendo a escolha guardada no cookie.
Se você ligar o seu código só no clique dos botões, o consentimento vale para a primeira visita e some em todas as outras. A pessoa aceitou uma vez e vira "negado" para sempre — sem erro nenhum aparecendo em lugar nenhum. 😱
Já o onChange cobre quem abre "Customizar", desmarca
"Estatísticas" e salva. A pessoa mudou de ideia sem sair da página, e o
GA4 tem de ser avisado na hora — não no próximo F5.
Os dois recebem um objeto com a chave cookie, e o
onChange ainda recebe changedCategories com a
lista do que mudou. Eu não uso nenhum dos dois aqui de propósito: como a
atualizarConsentimento() pergunta o estado atual direto ao
acceptedCategory(), ela funciona igual nos dois lugares e
não precisa saber quem a chamou. Uma função só, ligada duas vezes. 🙌
🎛️ Modo básico ou avançado? A decisão que mais confunde
Essa é a dúvida que eu mais vejo, e a resposta depende do que você escolheu sem saber que estava escolhendo.
Do jeito que eu montei acima, o script do GA4 carrega sempre, mesmo para quem não consentiu. Isso é o modo avançado. Sem consentimento, o GA4 não grava cookie e não identifica ninguém — mas manda um ping anônimo, sem identificador. O Google usa esses pings para estimar por modelagem o que não pôde medir, e por isso os relatórios ficam menos furados.
O modo básico é o outro caminho: você segura o
carregamento do gtag/js até a pessoa aceitar. Quem recusa
não gera requisição nenhuma para o Google — zero. Em compensação, some
qualquer estimativa: quem recusou simplesmente não existe nos números.
| Modo básico | Modo avançado | |
|---|---|---|
| O script do GA4 carrega… | só depois do aceite | sempre |
| Quem recusa gera requisição? | nenhuma | um ping sem cookie |
Cookie _ga para quem recusa | não | não |
| Estimativa de quem recusou | não tem | tem |
Nos dois modos o cookie não é gravado sem consentimento — essa parte é igual, e é a que costuma pesar juridicamente. A diferença real é se sai ou não uma requisição anônima.
Se você quiser o modo básico, o caminho é tirar a tag do GA4 do HTML e
carregá-la dentro do onConsent, só quando
analytics_storage for permitido. Eu fiquei no avançado
porque ele resolve o problema com menos peça móvel: a ordem já é a certa,
e não tem carregamento condicional para dar errado.
📄 O arquivo inteiro, de cima a baixo
Juntando tudo, é uma página só. Nenhum framework, nenhum build,
nenhuma dependência além do gtag.js — que é o próprio
assunto — e do banner de consentimento que o seu site já tem.
Repare na ordem das três partes numeradas, que é o assunto do artigo
inteiro: o consent default está no <head>,
sozinho, antes de o banner sequer existir na página. A
biblioteca só entra na parte 3, lá no fim. Ela é a última a chegar e
ainda assim manda no resultado — porque o update dela
corrige um estado que já estava declarado como negado:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Consent Mode do GA4 com o vanilla-cookieconsent</title>
<link rel="stylesheet" href="/vendor/cookieconsent/dist/cookieconsent.css">
<!-- 1) A fila do gtag e o estado padrao NEGADO.
Este bloco tem de vir ANTES do script do GA4 la embaixo, e antes da
biblioteca do banner. Se ele vier depois, o GA4 ja mandou a primeira
batida como se tudo fosse permitido. -->
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
function definirConsentimentoPadrao() {
gtag("consent", "default", {
"analytics_storage": "denied",
"ad_storage": "denied",
"ad_user_data": "denied",
"ad_personalization": "denied",
// Segura a batida por 500 ms esperando o update do banner. Sem isso,
// quem aceita rapido ainda tem a primeira batida contada como negada.
"wait_for_update": 500
});
}
definirConsentimentoPadrao();
</script>
</head>
<body>
<h1>Consent Mode do GA4</h1>
<button type="button" data-cc="show-preferencesModal">Preferencias de cookies</button>
<!-- 2) O GA4. Carrega sempre (modo avancado): sem consentimento ele manda
ping sem cookie; com consentimento, vira medicao completa.
ID de exemplo, nao pertence a nenhuma propriedade real. -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-EXEMPLO123"></script>
<script>
gtag("js", new Date());
gtag("config", "G-EXEMPLO123");
</script>
<!-- 3) O banner, que so faz uma coisa nova: avisar o gtag da escolha. -->
<script type="module">
import "/vendor/cookieconsent/dist/cookieconsent.umd.js";
function atualizarConsentimento() {
var analiseAceita = CookieConsent.acceptedCategory("analytics");
var anunciosAceitos = CookieConsent.acceptedCategory("marketing");
var valorAnalise = "denied";
if (analiseAceita) {
valorAnalise = "granted";
}
var valorAnuncios = "denied";
if (anunciosAceitos) {
valorAnuncios = "granted";
}
gtag("consent", "update", {
"analytics_storage": valorAnalise,
"ad_storage": valorAnuncios,
"ad_user_data": valorAnuncios,
"ad_personalization": valorAnuncios
});
}
CookieConsent.run({
guiOptions: {
consentModal: { layout: "box inline", position: "bottom right", equalWeightButtons: true },
preferencesModal: { layout: "box", position: "right", equalWeightButtons: true }
},
categories: {
necessary: { readOnly: true },
analytics: {},
marketing: {}
},
// Roda na primeira escolha E a cada carregamento com escolha ja salva.
onConsent: atualizarConsentimento,
// Roda quando a pessoa muda de ideia sem recarregar a pagina.
onChange: atualizarConsentimento,
language: {
default: "pt_BR",
translations: {
pt_BR: {
consentModal: {
title: "Controle sua privacidade",
description: "Usamos cookies para melhorar a navegacao.",
acceptAllBtn: "Aceitar",
acceptNecessaryBtn: "Rejeitar",
showPreferencesBtn: "Customizar"
},
preferencesModal: {
title: "Preferencias",
acceptAllBtn: "Aceitar todos",
acceptNecessaryBtn: "Rejeitar todos",
savePreferencesBtn: "Salvar",
sections: [
{ title: "Necessarios", description: "Essenciais para o site funcionar.", linkedCategory: "necessary" },
{ title: "Estatisticas", description: "Medem o uso do site.", linkedCategory: "analytics" },
{ title: "Marketing", description: "Personalizam anuncios.", linkedCategory: "marketing" }
]
}
}
}
}
});
</script>
</body>
</html>
Uma observação sobre a parte 3: eu carrego a biblioteca com
<script type="module"> e um import, que é
como o projeto dela sugere. Se você preferir a tag src de
sempre, funciona igual — só lembre que aí precisa ser
duas tags, a da biblioteca antes e a da sua
configuração depois, senão o CookieConsent ainda não existe
quando o seu código roda.
🔍 Conferindo no navegador — onde a verdade aparece
Painel é bom, mas painel demora e só mostra o que chegou. Quem responde "isto está funcionando?" na hora é o seu próprio navegador. São três coisas para olhar, e nenhuma exige instalar nada. 🕵️♀️
Abra as ferramentas do desenvolvedor (F12), vá na aba
Rede e filtre por collect. Recarregue a
página sem clicar em nada no banner.
1. A requisição para /g/collect. No modo
avançado ela sai mesmo sem consentimento — é o ping anônimo. Se você tiver
optado pelo modo básico, não deve aparecer nada até o aceite.
2. Os parâmetros de consentimento dentro dela. Clique na requisição e olhe a query string. Dois parâmetros contam a história toda:
| Parâmetro | O que significa |
|---|---|
gcs | O estado atual. Vem como G1 seguido de dois dígitos: o primeiro é ad_storage, o segundo é analytics_storage. 0 é negado, 1 é permitido. |
gcd | O estado padrão declarado, com os quatro sinais. É ele que prova que o seu default chegou a tempo. |
Então, antes de qualquer clique, você quer ver
gcs=G100: os dois zeros dizendo que nada
foi consentido ainda. Se aparecer gcs=G111 numa página em
que ninguém clicou em nada, é o sinal vermelho — quase sempre o
default chegando depois do config.
3. O cookie _ga. Esse é o teste mais
direto de todos, e o que eu faria primeiro se tivesse trinta segundos.
Vá na aba Aplicativo (ou Armazenamento), procure
os cookies do seu domínio, e confira: antes do aceite,
não pode existir nenhum _ga. Se ele estiver
lá, o Consent Mode não está pegando, ponto final.
Depois clique em "Aceitar" e olhe de novo, sem recarregar. Você deve
ver, em sequência: uma requisição nova saindo com
gcs=G111, e o cookie _ga
aparecendo agora. Foi esse par — o G100 virando
G111 junto com o cookie nascendo — que me convenceu de que a
ligação estava mesmo feita. 🎉
E vale fazer o terceiro caminho, que é o que ninguém testa: clique em
"Rejeitar". O gcs continua G100, e o
_ga continua sem existir. Um "deu certo" mostra o código
funcionando; o "Rejeitar" mostra que você entendeu o problema.
Tem ainda um quarto caminho, e esse é o que separa quem ligou os dois
retornos de quem ligou só um: aceite e depois recarregue a
página. O banner não vai aparecer — você já escolheu, ele
respeita. Olhe o gcs mesmo assim. Ele precisa continuar
G111, mesmo sem ninguém ter clicado em nada nesse
carregamento. É o onConsent trabalhando calado. 🤫
Se nesse recarregamento o gcs voltar para
G100, o diagnóstico é quase sempre um só: o
update está pendurado no clique do botão em vez de estar no
onConsent. Funciona na primeira visita e falha em todas as
outras — que é justamente o tipo de bug que ninguém percebe, porque
ninguém testa a segunda visita.
🧹 Três detalhes que me morderam
Coisas pequenas que custaram tempo e que ninguém escreve:
Limpe o cookie do banner entre um teste e outro. O banner guarda a escolha e não aparece de novo. Você recarrega, não vê banner nenhum, acha que quebrou — e é só a sua escolha anterior sendo respeitada. A própria biblioteca tem um atalho para isso, que é o que eu deixo colado no console durante os testes:
// Apaga a escolha guardada e recarrega, para o banner voltar a aparecer.
CookieConsent.reset(true);
location.reload();
Apague também o _ga na aba Aplicativo,
senão o cookie da rodada anterior fica lá e confunde a leitura. 🧽
As categorias do banner são nomes seus, não do Google.
No meu exemplo elas se chamam analytics e
marketing porque foi assim que eu declarei no
categories do run(). Se no seu site elas se
chamam estatisticas e publicidade, é esse nome
que vai dentro do acceptedCategory(). Errar aqui devolve
false caladinho, e o consentimento nunca é concedido.
E tem uma variação desse erro que é ainda mais discreta: o
linkedCategory das seções do modal de preferências. É ele
que amarra a caixinha que a pessoa marca à categoria de verdade. Se duas
seções apontarem para a mesma categoria — copiar e colar a seção de
"Estatísticas" para fazer a de "Marketing" e esquecer de trocar essa
linha —, a pessoa vê duas chaves na tela e só uma funciona. As duas
mexem na mesma categoria, e a outra nunca é ligada. 😬
Uma categoria pode alimentar dois sinais. Repare que
no meu atualizarConsentimento() a categoria "marketing"
controla três sinais de uma vez (ad_storage,
ad_user_data e ad_personalization). Isso é
proposital e é o arranjo mais comum: para quem lê o banner, "aceitar
marketing" é uma decisão só. Não precisa de três caixinhas.
🎯 O que mudou, no fim das contas
O engraçado desse trabalho todo é que ele quase não tem código. São
duas chamadas de função: um default lá em cima dizendo
"não pode nada" e um update lá embaixo dizendo o que a
pessoa liberou. Mais duas linhas no run() do banner ligando
o onConsent e o onChange. O resto — o banner,
a tag do GA4, os textos — já estava tudo lá.
O que faltava era ninguém ter apresentado um ao outro. 😄 E, sinceramente, eu entendo por que isso passa batido: os dois lados funcionam perfeitamente sozinhos, ninguém reclama, o console fica limpo e o painel mostra número. É um daqueles bugs que só existem quando você para para perguntar "espera, o que acontece se eu clicar em Rejeitar?".
Se você cuida de algum site com banner de cookies, faz esse teste hoje:
abre o F12, clica em Rejeitar e procura o cookie
_ga. Leva trinta segundos e a resposta costuma ser
surpreendente. 🙈
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Leia também
Node.js: populando uma planilha no Google Sheets
Como escrever dados numa planilha do Google Sheets com Node.js, e as tres armadilhas que transformam dado certo em planilha errada.
Node.js: consultando CPF e CNPJ no SPC
Como consultar CPF e CNPJ no SPC Brasil com Node.js puro: envelope SOAP na mao, sem biblioteca, e as armadilhas do caminho.
Node.js: gerando o sitemap.xml de um site
Rastreando um site pelos links e montando o sitemap.xml com Node.js puro — e o bug de uma linha que fazia a resposta nunca chegar.