Building a Consent Management Platform
Building a Consent Management Platform
A robust consent management platform (CMP) forms the foundation of compliant cookie usage. While commercial CMPs exist, understanding how to build one illuminates the technical requirements and enables better integration with custom applications. The CMP must handle consent collection, storage, enforcement, and documentation while providing seamless user experience.
// Comprehensive Cookie Consent Management Platform
class CookieConsentManager {
constructor(config = {}) {
this.config = {
cookieName: 'cookie_consent',
cookieDomain: window.location.hostname,
cookieExpiry: 365,
defaultRegion: 'US',
geoIpEndpoint: '/api/geoip',
consentEndpoint: '/api/privacy/consent',
cookieCategories: {
necessary: {
name: 'Necessary',
description: 'Essential for website functionality',
required: true,
cookies: ['session_id', 'csrf_token', 'auth_token']
},
analytics: {
name: 'Analytics',
description: 'Help us understand how visitors interact with our website',
required: false,
cookies: ['_ga', '_gid', '_gat', 'amplitude_id'],
scripts: ['google-analytics', 'amplitude']
},
marketing: {
name: 'Marketing',
description: 'Used to deliver personalized advertisements',
required: false,
cookies: ['_fbp', 'fr', 'ads_session'],
scripts: ['facebook-pixel', 'google-ads']
},
functional: {
name: 'Functional',
description: 'Enable enhanced functionality and personalization',
required: false,
cookies: ['language', 'theme', 'timezone'],
localStorage: ['user_preferences', 'saved_searches']
}
},
...config
};
this.consent = null;
this.region = null;
this.initialized = false;
}
// Initialize consent management
async init() {
// Detect user region for appropriate consent flow
this.region = await this.detectRegion();
// Load existing consent
this.consent = this.loadConsent();
// Apply consent or show banner
if (this.consent && !this.consentExpired()) {
await this.applyConsent();
} else {
await this.showConsentBanner();
}
// Block scripts until consent is given
this.interceptScriptLoading();
// Monitor for consent changes
this.setupConsentMonitoring();
this.initialized = true;
}
// Detect user region for appropriate consent flow
async detectRegion() {
try {
// Check for cached region
const cachedRegion = sessionStorage.getItem('user_region');
if (cachedRegion) return cachedRegion;
// Call GeoIP service
const response = await fetch(this.config.geoIpEndpoint);
const data = await response.json();
// Map country to privacy region
const region = this.mapCountryToRegion(data.country);
sessionStorage.setItem('user_region', region);
return region;
} catch (error) {
console.error('Failed to detect region:', error);
return this.config.defaultRegion;
}
}
// Map country codes to privacy regions
mapCountryToRegion(country) {
const regions = {
EU: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE',
'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT',
'RO', 'SK', 'SI', 'ES', 'SE', 'GB'],
CA: ['US-CA'], // California
BR: ['BR'], // Brazil (LGPD)
US: ['US'] // Other US states
};
for (const [region, countries] of Object.entries(regions)) {
if (countries.includes(country)) return region;
}
return 'OTHER';
}
// Show appropriate consent banner based on region
async showConsentBanner() {
const bannerConfig = this.getRegionBannerConfig();
const banner = this.createBanner(bannerConfig);
// Add to page
document.body.appendChild(banner);
// Animate entrance
requestAnimationFrame(() => {
banner.classList.add('visible');
});
// Focus management for accessibility
const firstButton = banner.querySelector('button');
if (firstButton) firstButton.focus();
// Prevent page interaction until consent decision
if (bannerConfig.blockingRequired) {
this.createBlockingOverlay();
}
}
// Get region-specific banner configuration
getRegionBannerConfig() {
const configs = {
EU: {
type: 'opt-in',
blockingRequired: true,
showRejectAll: true,
defaultSelections: {
necessary: true,
analytics: false,
marketing: false,
functional: false
},
text: {
title: 'We value your privacy',
description: 'We use cookies to enhance your browsing experience, analyze traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies.',
acceptAll: 'Accept All',
rejectAll: 'Reject All',
customize: 'Customize',
save: 'Save Preferences'
}
},
CA: {
type: 'opt-out',
blockingRequired: false,
showRejectAll: false,
defaultSelections: {
necessary: true,
analytics: true,
marketing: true,
functional: true
},
text: {
title: 'Cookie Notice',
description: 'We use cookies and similar technologies. You can manage your preferences or opt-out of data sales.',
acceptAll: 'OK',
customize: 'Manage Preferences',
optOut: 'Do Not Sell My Personal Information'
}
}
};
return configs[this.region] || configs.US;
}
// Create consent banner DOM
createBanner(config) {
const banner = document.createElement('div');
banner.className = 'cookie-consent-banner';
banner.setAttribute('role', 'dialog');
banner.setAttribute('aria-label', 'Cookie consent');
banner.setAttribute('aria-modal', 'true');
banner.innerHTML = `
<div class="consent-content">
<h2 id="consent-title">${config.text.title}</h2>
<p id="consent-description">${config.text.description}</p>
<div class="consent-options" id="consent-options" style="display: none;">
${Object.entries(this.config.cookieCategories).map(([key, category]) => `
<label class="consent-option ${category.required ? 'required' : ''}">
<input type="checkbox"
name="consent_${key}"
value="${key}"
${category.required ? 'checked disabled' : ''}
${config.defaultSelections[key] ? 'checked' : ''}
aria-describedby="desc_${key}">
<span class="option-name">${category.name}</span>
<span class="option-description" id="desc_${key}">${category.description}</span>
</label>
`).join('')}
</div>
<div class="consent-actions">
<button class="btn-primary" onclick="cookieConsent.acceptAll()">
${config.text.acceptAll}
</button>
${config.showRejectAll ? `
<button class="btn-secondary" onclick="cookieConsent.rejectAll()">
${config.text.rejectAll}
</button>
` : ''}
<button class="btn-text" onclick="cookieConsent.showOptions()">
${config.text.customize}
</button>
${config.text.optOut ? `
<a href="/privacy/do-not-sell" class="opt-out-link">
${config.text.optOut}
</a>
` : ''}
</div>
</div>
`;
return banner;
}
// Handle accept all action
async acceptAll() {
const consent = {};
Object.keys(this.config.cookieCategories).forEach(category => {
consent[category] = true;
});
await this.saveConsent(consent);
this.closeBanner();
}
// Handle reject all action
async rejectAll() {
const consent = {};
Object.keys(this.config.cookieCategories).forEach(category => {
consent[category] = this.config.cookieCategories[category].required;
});
await this.saveConsent(consent);
this.closeBanner();
}
// Show detailed options
showOptions() {
const optionsDiv = document.getElementById('consent-options');
const actionsDiv = document.querySelector('.consent-actions');
optionsDiv.style.display = 'block';
// Update buttons
actionsDiv.innerHTML = `
<button class="btn-primary" onclick="cookieConsent.savePreferences()">
Save Preferences
</button>
<button class="btn-secondary" onclick="cookieConsent.hideOptions()">
Cancel
</button>
`;
}
// Save user preferences
async savePreferences() {
const consent = {};
const checkboxes = document.querySelectorAll('.consent-options input[type="checkbox"]');
checkboxes.forEach(checkbox => {
const category = checkbox.value;
consent[category] = checkbox.checked;
});
await this.saveConsent(consent);
this.closeBanner();
}
// Save consent with all required metadata
async saveConsent(consent) {
const consentRecord = {
id: this.generateConsentId(),
timestamp: new Date().toISOString(),
consent: consent,
region: this.region,
consentType: this.getRegionBannerConfig().type,
cookieVersion: '1.0',
source: 'banner',
ipHash: await this.hashIP(),
userAgent: navigator.userAgent
};
// Save to cookie
this.setCookie(this.config.cookieName, JSON.stringify(consentRecord), {
expires: this.config.cookieExpiry,
domain: this.config.cookieDomain,
sameSite: 'Lax',
secure: true
});
// Save to server for compliance records
try {
await fetch(this.config.consentEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(consentRecord)
});
} catch (error) {
console.error('Failed to save consent to server:', error);
}
// Apply consent immediately
this.consent = consentRecord;
await this.applyConsent();
}
// Apply consent by controlling cookies and scripts
async applyConsent() {
// Remove non-consented cookies
this.removeNonConsentedCookies();
// Enable/disable scripts based on consent
this.controlScripts();
// Update third-party services
this.updateThirdPartyServices();
// Dispatch consent event for other components
window.dispatchEvent(new CustomEvent('cookieConsentUpdated', {
detail: this.consent
}));
}
// Remove cookies for non-consented categories
removeNonConsentedCookies() {
const allCookies = this.getAllCookies();
Object.entries(this.config.cookieCategories).forEach(([category, config]) => {
if (!this.hasConsent(category) && !config.required) {
config.cookies?.forEach(cookieName => {
// Remove exact matches and pattern matches
allCookies.forEach(cookie => {
if (cookie.name === cookieName || cookie.name.startsWith(cookieName)) {
this.deleteCookie(cookie.name);
}
});
});
// Clear localStorage items
config.localStorage?.forEach(key => {
localStorage.removeItem(key);
});
}
});
}
// Control script loading based on consent
controlScripts() {
// Enable consented scripts
document.querySelectorAll('script[data-consent-category]').forEach(script => {
const category = script.getAttribute('data-consent-category');
if (this.hasConsent(category)) {
// Create new script element to trigger execution
const newScript = document.createElement('script');
Array.from(script.attributes).forEach(attr => {
if (attr.name !== 'type') {
newScript.setAttribute(attr.name, attr.value);
}
});
newScript.textContent = script.textContent;
script.parentNode.replaceChild(newScript, script);
}
});
}
// Intercept dynamic script loading
interceptScriptLoading() {
const originalAppend = Element.prototype.appendChild;
const self = this;
Element.prototype.appendChild = function(element) {
if (element.tagName === 'SCRIPT' && element.src) {
const category = self.getScriptCategory(element.src);
if (category && !self.hasConsent(category)) {
console.log(`Blocked script loading: ${element.src} (requires ${category} consent)`);
return element;
}
}
return originalAppend.call(this, element);
};
}
// Determine script category from URL
getScriptCategory(url) {
const scriptMappings = {
'google-analytics.com': 'analytics',
'googletagmanager.com': 'analytics',
'facebook.com': 'marketing',
'doubleclick.net': 'marketing',
'amplitude.com': 'analytics'
};
for (const [domain, category] of Object.entries(scriptMappings)) {
if (url.includes(domain)) {
return category;
}
}
return null;
}
// Check if category has consent
hasConsent(category) {
if (!this.consent) return false;
return this.consent.consent[category] === true;
}
// Update third-party services based on consent
updateThirdPartyServices() {
// Google Analytics
if (typeof gtag !== 'undefined') {
if (this.hasConsent('analytics')) {
gtag('consent', 'update', {
'analytics_storage': 'granted'
});
} else {
gtag('consent', 'update', {
'analytics_storage': 'denied'
});
}
}
// Facebook Pixel
if (typeof fbq !== 'undefined') {
if (!this.hasConsent('marketing')) {
fbq('dataProcessingOptions', ['LDU'], 1, 1000);
} else {
fbq('dataProcessingOptions', []);
}
}
}
// Cookie utility functions
setCookie(name, value, options = {}) {
let cookie = `${name}=${encodeURIComponent(value)}`;
if (options.expires) {
const date = new Date();
date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
cookie += `; expires=${date.toUTCString()}`;
}
if (options.domain) {
cookie += `; domain=${options.domain}`;
}
cookie += `; path=${options.path || '/'}`;
if (options.secure) {
cookie += '; secure';
}
if (options.sameSite) {
cookie += `; samesite=${options.sameSite}`;
}
document.cookie = cookie;
}
getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return decodeURIComponent(parts.pop().split(';').shift());
}
return null;
}
deleteCookie(name) {
this.setCookie(name, '', { expires: -1 });
// Try deleting with different domain variations
this.setCookie(name, '', { expires: -1, domain: `.${window.location.hostname}` });
this.setCookie(name, '', { expires: -1, domain: window.location.hostname });
}
getAllCookies() {
return document.cookie.split(';').map(cookie => {
const [name, value] = cookie.trim().split('=');
return { name, value: decodeURIComponent(value || '') };
}).filter(cookie => cookie.name);
}
}
// Initialize consent management
const cookieConsent = new CookieConsentManager();
document.addEventListener('DOMContentLoaded', () => cookieConsent.init());