Pular para o conteúdo
JavaScript

GA4: ligando o Consent Mode ao banner de cookies

Paloma Macetko
Ilustracao de um unicornio diante de um portao de cristal com dois selos, um aceso e outro apagado, e uma coruja guardia segurando a chave

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:

orestbida/cookieconsentO banner de consentimento em JavaScript puro, licença MIT. Os exemplos deste artigo usam a versão 3.0.1.github.com

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:

SinalO que ele libera
analytics_storageOs cookies de análise. É o que permite reconhecer que é a mesma pessoa voltando, contar usuários e gerar os relatórios de comportamento.
ad_storageOs cookies ligados a publicidade.
ad_user_dataO envio dos dados do usuário ao Google com finalidade publicitária.
ad_personalizationUsar 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 bannerSinais do Google que ela libera
necessarynenhum — é readOnly: true, sempre ativa, e não tem sinal correspondente
analyticsanalytics_storage
marketingad_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ásicoModo avançado
O script do GA4 carrega…só depois do aceitesempre
Quem recusa gera requisição?nenhumaum ping sem cookie
Cookie _ga para quem recusanãonão
Estimativa de quem recusounão temtem

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âmetroO que significa
gcsO estado atual. Vem como G1 seguido de dois dígitos: o primeiro é ad_storage, o segundo é analytics_storage. 0 é negado, 1 é permitido.
gcdO 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á.

orestbida/cookieconsentA biblioteca do banner usada neste artigo, na versão 3.0.1. É de onde saem o run(), o onConsent, o onChange e o acceptedCategory().github.com

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