/**
* Netlify Function: GA4 Privacy Fires
* Consent-aware event processing with privacy controls
*
* Use Case: Fire GA4 events only when user consent is valid,
* apply data minimization based on consent level
*/
// netlify/functions/ga4-privacy-fire.js
const CONSENT_LEVELS = {
NONE: 0, // No tracking
ESSENTIAL: 1, // Anonymous aggregate only
FUNCTIONAL: 2, // Session tracking, no user ID
ANALYTICS: 3, // Full analytics with hashed IDs
MARKETING: 4 // Full tracking including ads
};
exports.handler = async (event, context) => {
// Only accept POST requests
if (event.httpMethod !== 'POST') {
return { statusCode: 405, body: 'Method Not Allowed' };
}
try {
const payload = JSON.parse(event.body);
const consentString = event.headers['x-consent-status'] || 'NONE';
const consentLevel = CONSENT_LEVELS[consentString] || 0;
// Get user region from headers (set by Netlify Edge)
const country = event.headers['x-country'] ||
event.headers['x-nf-client-connection-ip-country'] || 'US';
// Determine applicable privacy law
const privacyLaw = getApplicablePrivacyLaw(country);
// Check if we can fire this event
const eventType = payload.events?.[0]?.name || 'unknown';
const canFire = canFireEvent(eventType, consentLevel, privacyLaw);
if (!canFire.allowed) {
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: 'blocked',
reason: canFire.reason,
consent_required: canFire.requiredLevel,
current_consent: consentString
})
};
}
// Apply privacy transformations based on consent level
const transformedPayload = applyPrivacyTransforms(
payload,
consentLevel,
privacyLaw
);
// Add privacy metadata
transformedPayload.events[0].params = {
...transformedPayload.events[0].params,
consent_level: consentString,
privacy_law: privacyLaw,
data_minimized: consentLevel < CONSENT_LEVELS.MARKETING
};
// Forward to GA4
const ga4Response = await sendToGA4(transformedPayload);
// Log for audit (optional - use Netlify Blobs or external service)
await logPrivacyAudit({
event_type: eventType,
consent_level: consentString,
privacy_law: privacyLaw,
country,
timestamp: new Date().toISOString(),
allowed: true
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'X-Privacy-Law': privacyLaw,
'X-Consent-Applied': consentString
},
body: JSON.stringify({
status: 'fired',
event: eventType,
consent_level: consentString,
privacy_law: privacyLaw,
transformations_applied: getTransformationList(consentLevel)
})
};
} catch (error) {
console.error('Privacy fire error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Processing failed' })
};
}
};
/**
* Determine which privacy law applies
*/
function getApplicablePrivacyLaw(country) {
const laws = {
// GDPR countries
'AT': 'GDPR', 'BE': 'GDPR', 'BG': 'GDPR', 'HR': 'GDPR', 'CY': 'GDPR',
'CZ': 'GDPR', 'DK': 'GDPR', 'EE': 'GDPR', 'FI': 'GDPR', 'FR': 'GDPR',
'DE': 'GDPR', 'GR': 'GDPR', 'HU': 'GDPR', 'IE': 'GDPR', 'IT': 'GDPR',
'LV': 'GDPR', 'LT': 'GDPR', 'LU': 'GDPR', 'MT': 'GDPR', 'NL': 'GDPR',
'PL': 'GDPR', 'PT': 'GDPR', 'RO': 'GDPR', 'SK': 'GDPR', 'SI': 'GDPR',
'ES': 'GDPR', 'SE': 'GDPR', 'GB': 'UK-GDPR',
// US States
'US-CA': 'CCPA', 'US-VA': 'VCDPA', 'US-CO': 'CPA',
// Other
'BR': 'LGPD', 'CA': 'PIPEDA'
};
return laws[country] || 'NONE';
}
/**
* Check if event can fire based on consent
*/
function canFireEvent(eventType, consentLevel, privacyLaw) {
// Events that require marketing consent
const marketingEvents = ['view_promotion', 'select_promotion', 'ad_click'];
// Events that require analytics consent
const analyticsEvents = ['purchase', 'add_to_cart', 'begin_checkout', 'view_item'];
// Essential events (can fire with minimal consent)
const essentialEvents = ['page_view', 'scroll', 'first_visit'];
// GDPR/UK-GDPR requires explicit consent for non-essential
if (['GDPR', 'UK-GDPR'].includes(privacyLaw)) {
if (marketingEvents.includes(eventType) && consentLevel < CONSENT_LEVELS.MARKETING) {
return { allowed: false, reason: 'Marketing consent required', requiredLevel: 'MARKETING' };
}
if (analyticsEvents.includes(eventType) && consentLevel < CONSENT_LEVELS.ANALYTICS) {
return { allowed: false, reason: 'Analytics consent required', requiredLevel: 'ANALYTICS' };
}
}
// CCPA allows opt-out model (can fire unless explicitly opted out)
if (privacyLaw === 'CCPA' && consentLevel === CONSENT_LEVELS.NONE) {
if (marketingEvents.includes(eventType)) {
return { allowed: false, reason: 'User opted out of sale', requiredLevel: 'FUNCTIONAL' };
}
}
return { allowed: true };
}
/**
* Apply privacy transformations based on consent
*/
function applyPrivacyTransforms(payload, consentLevel, privacyLaw) {
const transformed = JSON.parse(JSON.stringify(payload));
// Level 1 (Essential): Remove all identifiers
if (consentLevel <= CONSENT_LEVELS.ESSENTIAL) {
delete transformed.user_id;
transformed.client_id = 'anonymous';
if (transformed.events?.[0]?.params) {
delete transformed.events[0].params.user_properties;
}
}
// Level 2 (Functional): Hash client_id, no user_id
else if (consentLevel === CONSENT_LEVELS.FUNCTIONAL) {
delete transformed.user_id;
if (transformed.client_id) {
transformed.client_id = hashValue(transformed.client_id);
}
}
// Level 3 (Analytics): Hash all IDs
else if (consentLevel === CONSENT_LEVELS.ANALYTICS) {
if (transformed.user_id) {
transformed.user_id = hashValue(transformed.user_id);
}
if (transformed.client_id) {
transformed.client_id = hashValue(transformed.client_id);
}
}
// Level 4 (Marketing): Full data, no transformations
return transformed;
}
/**
* Hash sensitive values
*/
function hashValue(value) {
let hash = 5381;
for (let i = 0; i < value.length; i++) {
hash = ((hash << 5) + hash) + value.charCodeAt(i);
}
return 'h_' + Math.abs(hash).toString(36);
}
/**
* Get list of transformations applied
*/
function getTransformationList(consentLevel) {
const transforms = [];
if (consentLevel <= CONSENT_LEVELS.ESSENTIAL) {
transforms.push('anonymized_ids', 'removed_user_properties');
} else if (consentLevel === CONSENT_LEVELS.FUNCTIONAL) {
transforms.push('hashed_client_id', 'removed_user_id');
} else if (consentLevel === CONSENT_LEVELS.ANALYTICS) {
transforms.push('hashed_all_ids');
}
return transforms;
}
/**
* Send to GA4 Measurement Protocol
*/
async function sendToGA4(payload) {
const fetch = (await import('node-fetch')).default;
return fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${process.env.GA4_MEASUREMENT_ID}&api_secret=${process.env.GA4_API_SECRET}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}
);
}
/**
* Log for privacy audit trail
*/
async function logPrivacyAudit(auditData) {
// Implement with Netlify Blobs, external DB, or logging service
console.log('Privacy Audit:', JSON.stringify(auditData));
}
/**
* Client-side usage:
*/
// fetch('/.netlify/functions/ga4-privacy-fire', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'X-Consent-Status': 'ANALYTICS' // or 'NONE', 'ESSENTIAL', 'FUNCTIONAL', 'MARKETING'
// },
// body: JSON.stringify({
// client_id: 'GA1.1.123456789',
// events: [{ name: 'purchase', params: { value: 99.99 } }]
// })
// });