Testes A/B de banners de consentimento para cookies

O desafio

Pretende testar diferentes designs ou mensagens dos banners de cookies para melhorar as taxas de consentimento, mantendo a conformidade com o RGPD.

O cenário

«Quero fazer um teste A/B ao nosso banner de cookies para ver se diferentes formulações afetam as taxas de aceitação. Mas estou preocupado com a conformidade com o RGPD se rastrear os utilizadores antes de eles darem o seu consentimento.»

Abordagem de testes A/B em conformidade

O princípio fundamental: pode testar banners de cookies SEM ferramentas de rastreamento, utilizando apenas cookies estritamente necessários.

Implementação

// Create variant assignment (strictly necessary cookie)
function assignVariant() {
    let variant = getCookie('banner_test_variant');

    if (!variant) {
        // Randomly assign variant
        variant = Math.random() < 0.5 ? 'A' : 'B';

        // Store in strictly necessary cookie (doesn't require consent)
        document.cookie = banner_test_variant=${variant}; path=/; max-age=2592000; SameSite=Strict;
    }

    return variant;
}

// Configuration for Variant A (Control)
const configA = {
    "notice_banner_type": "simple",
    "website_name": "My Website",
    // ... other config
};

// Configuration for Variant B (Test)
const configB = {
    "notice_banner_type": "headline",
    "website_name": "My Site", // Shorter name
    // ... other config
};

// Initialize based on variant
document.addEventListener('DOMContentLoaded', function () {
    const variant = assignVariant();
    const config = variant === 'A' ? configA : configB;

    // Add callback to track results (only after consent)
    config.callbacks = {
        "i_agree_button_clicked": () => {
            // User accepted - log this (now they've consented to tracking)
            logConsentEvent(variant, 'accepted');
        },
        "i_decline_button_clicked": () => {
            // User declined - still log the action
            logConsentEvent(variant, 'declined');
        }
    };

    cookieconsent.run(config);
});

function logConsentEvent(variant, action) {
    // Send to your analytics (user has now consented)
    if (typeof gtag !== 'undefined') {
        gtag('event', 'cookie_consent', {
            'variant': variant,
            'action': action
        });
    }

    // Or send to backend
    fetch('/api/log-consent', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
            variant: variant,
            action: action,
            timestamp: new Date().toISOString()
        })
    });
}

function getCookie(name) {
    const value = ; ${document.cookie};
    const parts = value.split(; ${name}=);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

Variáveis de teste

Tipo de banner

// Variant A: Simple banner
"notice_banner_type": "simple"

// Variant B: Headline banner
"notice_banner_type": "headline"

Esquema de cores

// Variant A: Light
"palette": "light"

// Variant B: Dark
"palette": "dark"

Texto do botão (através de CSS personalizado)

// Add CSS to change button text
const style = document.createElement('style');
style.textContent = variant === 'A'
    ? '.cc-nb-okagree::before { content: "Accept All"; }'
    : '.cc-nb-okagree::before { content: "I Agree"; }';
document.head.appendChild(style);

Medir impressões de consentimento

// Backend endpoint to analyze results
// GET /api/consent-analytics

{
    "variant_A": {
        "impressions": 1000,
        "accepted": 650,
        "declined": 250,
        "no_action": 100,
        "acceptance_rate": 0.65
    },
    "variant_B": {
        "impressions": 1000,
        "accepted": 720,
        "declined": 200,
        "no_action": 80,
        "acceptance_rate": 0.72
    }
}

Consentimento entre domínios

Problema

«Temos www.example.com para o nosso site principal e shop.example.com para a nossa loja. Os utilizadores recebem o banner de cookies em AMBOS os sites, mesmo depois de terem aceitado no primeiro. É irritante e estamos a perder conversões.»

Compreender os desafios entre domínios

Por predefinição, os cookies estão limitados ao domínio específico onde são definidos:

  • Cookie definido em www.example.com → Não acessível em shop.example.com
  • Cookie definido em shop.example.com → Não acessível em www.example.com

Utilize o parâmetro cookie_domain para partilhar o consentimento entre subdomínios:

cookieconsent.run({
    "consent_type": "express",
    "website_name": "My Website",
    "website_privacy_policy_url": "https://www.example.com/privacy",
    "cookie_domain": ".example.com", // Note the leading dot!
    "page_load_consent_levels": ["strictly-necessary"]
});

Importante! O ponto inicial (.example.com) torna o cookie acessível a:

  • example.com
  • www.example.com
  • shop.example.com
  • blog.example.com
  • Qualquer subdomínio de example.com

Um exemplo completo entre domínios

Em todos os domínios/subdomínios, utilize uma configuração idêntica:

<!-- On www.example.com -->
<script type="text/javascript" src="//www.termsfeed.com/public/cookie-consent/4.2.0/cookie-consent.js"></script>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function () {
    cookieconsent.run({
        "notice_banner_type": "headline",
        "consent_type": "express",
        "palette": "light",
        "language": "en",
        "website_name": "Example",
        "website_privacy_policy_url": "https://www.example.com/privacy",
        "open_preferences_center_selector": "#changePreferences",
        "page_load_consent_levels": ["strictly-necessary"],
        "cookie_domain": ".example.com", // Shared across all subdomains
        "cookie_secure": true // Recommended for production
    });
});
</script>
<!-- On shop.example.com - SAME configuration -->
<script type="text/javascript" src="//www.termsfeed.com/public/cookie-consent/4.2.0/cookie-consent.js"></script>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function () {
    cookieconsent.run({
        // EXACT SAME CONFIG as main domain
        "notice_banner_type": "headline",
        "consent_type": "express",
        "palette": "light",
        "language": "en",
        "website_name": "Example",
        "website_privacy_policy_url": "https://www.example.com/privacy",
        "open_preferences_center_selector": "#changePreferences",
        "page_load_consent_levels": ["strictly-necessary"],
        "cookie_domain": ".example.com", // Same domain!
        "cookie_secure": true
    });
});
</script>

Teste a configuração entre domínios

  1. Visite www.example.com
  2. Aceite os cookies
  3. Abra DevTools → Aplicação → Cookies
  4. Verifique se o domínio do cookie mostra .example.com (com ponto à esquerda)
  5. Navegue até shop.example.com
  6. Verifique se o banner NÃO aparece (consentimento já dado)

Problemas comuns entre domínios

Problema 1: O banner continua a aparecer nos subdomínios

Problema

«Defini cookie_domain como '.example.com', mas o banner continua a aparecer em todos os subdomínios.»

Causa

O navegador está a bloquear cookies de terceiros.

Solução

Certifique-se de que cookie_secure é verdadeiro para sites HTTPS.

// Make sure cookie_secure is true for HTTPS sites
"cookie_secure": true,

// Also verify in DevTools that cookie is actually being set
// Check: Application → Cookies → Look for cookie_consent_level
// Domain should show: .example.com (not www.example.com)

Problema 2: Consentimento diferente em subdomínios diferentes

Problema:

O utilizador dá o seu consentimento no domínio principal, mas as preferências diferem no subdomínio.

Causa:

As configurações não são idênticas entre domínios.

Solução:

Utilize um ficheiro de configuração ou variável partilhada.

// shared-config.js (include on all domains)
const SHARED_COOKIE_CONFIG = {
    "notice_banner_type": "headline",
    "consent_type": "express",
    "palette": "light",
    "language": "en",
    "website_name": "Example",
    "website_privacy_policy_url": "https://www.example.com/privacy",
    "open_preferences_center_selector": "#changePreferences",
    "page_load_consent_levels": ["strictly-necessary"],
    "cookie_domain": ".example.com",
    "cookie_secure": true
};

// Then on each domain:
cookieconsent.run(SHARED_COOKIE_CONFIG);

Multidomínio (TLDs diferentes)

Problema

Tem example.com E example.co.uk, ou seja, domínios completamente diferentes.

Solução

Os cookies NÃO PODEM ser partilhados entre diferentes domínios de nível superior devido à segurança do navegador.

Tem duas opções:

Opção 1: Aceitar vários banners.

  • O utilizador vê o banner em cada domínio de nível superior
  • O consentimento dado aplica-se individualmente a cada domínio, o consentimento não é partilhado
  • Esta é provavelmente a abordagem mais conforme

Opção 2: Utilizar uma sincronização de backend (para utilizadores com sessão iniciada)

// On example.com - user accepts
"callbacks": {
    "user_consent_saved": () => {
        if (isUserLoggedIn()) {
            // Sync to backend
            fetch('https://api.example.com/sync-consent', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': 'Bearer ' + getUserToken()
                },
                body: JSON.stringify({
                    consent: getConsentState(),
                    domains: ['example.com', 'example.co.uk']
                })
            });
        }
    }
}

// On example.co.uk - check backend first
async function checkBackendConsent() {
    if (isUserLoggedIn()) {
        const response = await fetch('https://api.example.com/get-consent', {
            headers: {'Authorization': 'Bearer ' + getUserToken()}
        });
        const data = await response.json();
        if (data.hasConsent) {
            // Set cookie locally
            setCookieConsent(data.consentLevel);
        }
    }
}

Recarregamento da página após o consentimento

Problema

«Depois de o utilizador aceitar os cookies, recarreguei a página com window.location.reload() para garantir que os scripts de rastreamento são carregados. Mas isto faz com que o Google Analytics perca a fonte de referência original e mostre todo o tráfego como "Direto".»

Isto acontece porque, quando recarrega a página:

  1. O navegador limpa as informações de referência
  2. O carregamento da nova página parece provir do mesmo domínio
  3. O Google Analytics classifica o tráfego como "Direto"
  4. Perde-se os dados de atribuição

Solução 1: Não recarregue (preferível)

Utilize callbacks para carregar scripts dinamicamente em vez de recarregar:

cookieconsent.run({
    "notice_banner_type": "headline",
    "consent_type": "express",
    "palette": "light",
    "language": "en",
    "website_name": "My Site",
    "website_privacy_policy_url": "https://mysite.com/privacy",
    "page_load_consent_levels": ["strictly-necessary"],

    "callbacks": {
        "scripts_specific_loaded": (level) => {
            // Scripts are NOW loaded - no reload needed!
            console.log("Scripts loaded for: " + level);

            if (level === 'tracking') {
                // Analytics is now active
                // Send initial page view if needed
                if (typeof gtag !== 'undefined') {
                    gtag('event', 'page_view', {
                        'page_location': window.location.href,
                        'page_referrer': document.referrer
                    });
                }
            }
        }
    },
    "callbacks_force": true
});

Solução 2: Preservar o referenciador ao recarregar a página

Se tiver de recarregar, preserve o referenciador:

cookieconsent.run({
    // ... config ...
    "callbacks": {
        "i_agree_button_clicked": () => {
            // Store referrer and source before reload
            sessionStorage.setItem('original_referrer', document.referrer);
            sessionStorage.setItem('landing_page', window.location.href);

            // Get URL parameters (utm_source, etc.)
            const urlParams = new URLSearchParams(window.location.search);
            if (urlParams.has('utm_source')) {
                sessionStorage.setItem('utm_source', urlParams.get('utm_source'));
                sessionStorage.setItem('utm_medium', urlParams.get('utm_medium'));
                sessionStorage.setItem('utm_campaign', urlParams.get('utm_campaign'));
            }

            // Now reload
            window.location.reload();
        }
    }
});

// After reload, restore attribution
document.addEventListener('DOMContentLoaded', function() {
    const savedReferrer = sessionStorage.getItem('original_referrer');
    const utmSource = sessionStorage.getItem('utm_source');

    if (savedReferrer && typeof gtag !== 'undefined') {
        // Send page view with preserved referrer
        gtag('event', 'page_view', {
            'page_location': window.location.href,
            'page_referrer': savedReferrer
        });

        // Send campaign data if available
        if (utmSource) {
            gtag('event', 'campaign_details', {
                'campaign_source': utmSource,
                'campaign_medium': sessionStorage.getItem('utm_medium'),
                'campaign_name': sessionStorage.getItem('utm_campaign')
            });
        }

        // Clear storage
        sessionStorage.removeItem('original_referrer');
        sessionStorage.removeItem('utm_source');
        // ... clear others
    }
});

Solução 3: Utilize o Modo de Consentimento do Google (prática recomendada)

Com o Modo de Consentimento V2, NÃO é necessário recarregar a página:

<!-- 1. Set default consent to denied -->
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
    'ad_storage': 'denied',
    'ad_user_data': 'denied',
    'ad_personalization': 'denied',
    'analytics_storage': 'denied'
});
</script>

<!-- 2. Load Google Analytics (runs in consent mode) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>

<!-- 3. Cookie Consent with callbacks -->
<script src="//www.termsfeed.com/public/cookie-consent/4.2.0/cookie-consent.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
    cookieconsent.run({
        "consent_type": "express",
        "website_name": "My Site",
        "page_load_consent_levels": ["strictly-necessary"],
        "callbacks": {
            "scripts_specific_loaded": (level) => {
                // Update consent mode - NO RELOAD!
                switch (level) {
                    case 'tracking':
                        gtag('consent', 'update', {
                            'analytics_storage': 'granted'
                        });
                        break;
                    case 'targeting':
                        gtag('consent', 'update', {
                            'ad_storage': 'granted',
                            'ad_user_data': 'granted',
                            'ad_personalization': 'granted'
                        });
                        break;
                }
            }
        },
        "callbacks_force": true
    });
});
  </script>


Conteúdo incorporado e iFrames

Problema

«Temos vídeos do YouTube incorporados no nosso site. Devemos bloquear os iframes até que os utilizadores aceitem os cookies? Como implementamos isto com o TermsFeed?»

Solução

A solução consiste em integrar um espaço reservado e utilizar o carregamento dinâmico.

Passo 1: Criar HTML de espaço reservado

<!-- Instead of directly embedding iframe -->
<div class="video-placeholder" data-video-id="dQw4w9WgXcQ" data-consent-type="targeting">
    <div class="placeholder-content">
        <img src="/images/video-thumbnail.jpg" alt="Video thumbnail">
        <div class="placeholder-overlay">
            <button class="accept-and-load" onclick="loadVideo(this)">
                 Accept cookies to watch this video
            </button>
            <p>This video is hosted by YouTube and requires marketing cookies.</p>
            <a href="#" onclick="openCookiePreferences(); return false;">
                Change cookie preferences
            </a>
        </div>
    </div>
</div>

<style>
.video-placeholder {
    position: relative;
    width: 100%;
    padding-bottom: 56.25%; // 16:9 aspect ratio
    background: #000;
}

.placeholder-content {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

.placeholder-content img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.placeholder-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.7);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: white;
    text-align: center;
    padding: 20px;
}

.accept-and-load {
    background: #ff0000;
    color: white;
    border: none;
    padding: 15px 30px;
    font-size: 16px;
    border-radius: 5px;
    cursor: pointer;
    margin-bottom: 15px;
}
</style>

Passo 2: Carregar o iFrame após o consentimento

// Check if user has already consented
function hasConsent(consentType) {
    const consentCookie = getCookie('cookie_consent_level');
    if (!consentCookie) return false;

    // Parse consent levels
    const levels = JSON.parse(decodeURIComponent(consentCookie));
    return levels.includes(consentType);
}

// Load video dynamically
function loadVideo(button) {
    const placeholder = button.closest('.video-placeholder');
    const videoId = placeholder.dataset.videoId;
    const consentType = placeholder.dataset.consentType;

    // Check if user has consent
    if (!hasConsent(consentType)) {
        // Request consent
        if (typeof cookieconsent !== 'undefined' && cookieconsent.openPreferencesCenter) {
            cookieconsent.openPreferencesCenter();
        } else {
            alert('Please accept marketing cookies to view this content');
        }
        return;
    }

    // Create iframe
    const iframe = document.createElement('iframe');
    iframe.src = https://www.youtube.com/embed/${videoId};
    iframe.width = '100%';
    iframe.height = '100%';
    iframe.frameBorder = '0';
    iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
    iframe.allowFullscreen = true;

    // Replace placeholder with iframe
    placeholder.innerHTML = '';
    placeholder.appendChild(iframe);
}

// Auto-load videos if consent already given
document.addEventListener('DOMContentLoaded', function() {
    document.querySelectorAll('.video-placeholder').forEach(placeholder => {
        const consentType = placeholder.dataset.consentType;
        if (hasConsent(consentType)) {
            // Auto-load - user already consented
            const videoId = placeholder.dataset.videoId;
            const iframe = document.createElement('iframe');
            iframe.src = https://www.youtube.com/embed/${videoId};
            iframe.width = '100%';
            iframe.height = '100%';
            iframe.frameBorder = '0';
            iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
            iframe.allowFullscreen = true;
            placeholder.innerHTML = '';
            placeholder.appendChild(iframe);
        }
    });
});

// Listen for consent updates
document.addEventListener('DOMContentLoaded', function() {
    cookieconsent.run({
        // ... config ...
        "callbacks": {
            "scripts_specific_loaded": (level) => {
                // When user accepts targeting cookies, load all pending videos
                if (level === 'targeting') {
                    document.querySelectorAll('.video-placeholder').forEach(placeholder => {
                        if (placeholder.dataset.consentType === 'targeting') {
                            loadVideo(placeholder.querySelector('.accept-and-load'));
                        }
                    });
                }
            }
        }
    });
});

function getCookie(name) {
    const value = ; ${document.cookie};
    const parts = value.split(; ${name}=);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

function openCookiePreferences() {
    if (typeof cookieconsent !== 'undefined' && cookieconsent.openPreferencesCenter) {
        cookieconsent.openPreferencesCenter();
    }
}

Exemplo para outras incorporações

Google Maps

<div class="map-placeholder" data-consent-type="functionality" data-location="New York, NY">
    <div class="placeholder-overlay">
        <p>️ Accept functional cookies to view map</p>
        <button onclick="loadMap(this)">Load Map</button>
    </div>
</div>

<script>
function loadMap(button) {
    const placeholder = button.closest('.map-placeholder');
    const location = placeholder.dataset.location;

    if (!hasConsent('functionality')) {
        cookieconsent.openPreferencesCenter();
        return;
    }

    const iframe = document.createElement('iframe');
    iframe.src = https://www.google.com/maps/embed/v1/place?key=YOUR_API_KEY&q=${encodeURIComponent(location)};
    iframe.width = '100%';
    iframe.height = '450';
    iframe.style.border = '0';
    iframe.allowFullscreen = true;
    iframe.loading = 'lazy';

    placeholder.innerHTML = '';
    placeholder.appendChild(iframe);
}
</script>

Incorporações de redes sociais (X, Instagram, Facebook)

<div class="social-placeholder" data-consent-type="targeting" data-platform="twitter" data-url="https://twitter.com/user/status/123">
    <div class="placeholder-overlay">
        <p> Accept marketing cookies to view this tweet</p>
        <button onclick="loadSocialEmbed(this)">Load Tweet</button>
    </div>
</div>

<script>
function loadSocialEmbed(button) {
    const placeholder = button.closest('.social-placeholder');
    const platform = placeholder.dataset.platform;
    const url = placeholder.dataset.url;

    if (!hasConsent('targeting')) {
        cookieconsent.openPreferencesCenter();
        return;
    }

    // Load platform SDK
    if (platform === 'twitter' && !window.twttr) {
        const script = document.createElement('script');
        script.src = 'https://platform.twitter.com/widgets.js';
        script.onload = () => {
            twttr.widgets.createTweet(
                url.split('/').pop(),
                placeholder,
                { theme: 'light' }
            );
        };
        document.head.appendChild(script);
    }
    // Add other platforms as needed
}
</script>