TLDR: GA4 with default settings is not GDPR compliant. Google Signals enabled? Non-compliant. 14-month retention in EU? Risky. No Consent Mode? Fatal. This guide shows you which toggles to flip.
Read full summary
Complete guide to GA4 privacy configuration: consent mode integration, data retention settings, IP anonymization, user data deletion, and BigQuery export considerations. Optimize for both compliance and analytics utility.
*Summary by Claude AI*
---
title: "Google Analytics 4 Privacy Settings: Complete GDPR Compliance Guide 2025"
description: "Master GA4 privacy configuration for GDPR compliance. Learn data retention, Google Signals, IP anonymization, Server-Side GTM, and Consent Mode v2 implementation."
keywords: ["GA4 privacy settings", "google analytics gdpr", "GA4 data retention", "google signals privacy", "server-side gtm privacy", "GA4 consent mode", "google analytics compliance"]
author: "GetCookies Privacy Team"
date: "2025-01-15"
category: "Technical Guides"
featured: true
readingTime: "20 min read"
---
## The Default Settings That Got Banned in Austria
In 2022, the Austrian DPA (DSB) ruled that a website's use of Google Analytics violated GDPR—not because of what the site did, but because of what GA's *default settings* did. Google Signals was enabled. IP anonymization wasn't verified. User data flowed to US servers without adequate protection.
That ruling triggered a domino effect across Europe. The French CNIL followed. Then the Italian Garante. Suddenly, "we just use Google Analytics" wasn't a defense—it was an admission.
Since then, the EU-U.S. Data Privacy Framework has resolved the transfer issue. But the configuration problems remain. Is GA4 GDPR compliant?
The short answer: **It depends on your configuration.**
While the **EU-U.S. Data Privacy Framework (DPF)**, adopted in July 2023, has largely resolved the legal issues around *data transfers* to the US (superseding the concerns raised in 2022 by Austrian and French DPAs), using GA4 does **not** automatically make you compliant.
You must still:
1. **Obtain valid consent** before loading the script or setting cookies (ePrivacy Directive).
2. **Configure data retention** and minimization settings appropriately (GDPR).
3. **Respect user rights** (opt-out, deletion).
The responsibility for compliance lies with you as the data controller. Google provides the tools (like Consent Mode and data retention controls), but you must implement them correctly.
## Introduction: The Privacy Evolution of Google Analytics
When Universal Analytics sunset in 2023, Google Analytics 4 represented more than a feature update—it was a fundamental reimagining of web analytics for a privacy-first era. GA4 was built from the ground up to work in a world of consent requirements, cookie restrictions, and increasing regulatory scrutiny.
However, "privacy-ready" doesn't mean "privacy-compliant out of the box." While the DPF has eased the legal burden regarding international data transfers, the gap between GA4's capabilities and actual compliance is where many organizations stumble. Issues like "consent theater," improper Google Signals usage, and lack of IP anonymization verification can still land you in hot water.
This guide walks you through every privacy setting in GA4, explaining not just what each setting does, but how to configure it for different compliance regimes and business needs.
### The Regulatory Landscape for Google Analytics
| Regulation | Region | Key Requirements | GA4 Impact |
|------------|--------|------------------|------------|
| **GDPR** | EU/EEA | Consent before tracking, data minimization | Must block GA4 until consent |
| **CCPA/CPRA** | California | Opt-out rights, disclosure requirements | Can load but must honor opt-outs |
| **LGPD** | Brazil | Consent for non-essential processing | Similar to GDPR approach |
| **PIPL** | China | Consent + data localization | GA4 not recommended (use server-side) |
| **POPIA** | South Africa | Consent for direct marketing | Configure based on purpose |
## Deep Dive: GA4 Privacy Settings
### 1. Data Retention Configuration
Data retention is your first line of defense in data minimization. GA4 offers limited options compared to Universal Analytics:
```typescript
// ga4-data-retention-manager.ts
interface GA4DataRetentionConfig {
userAndEventData: '2 months' | '14 months';
resetOnNewActivity: boolean;
userIdentifierRetention: 'same_as_event_data' | 'custom';
}
// Impact analysis of retention settings
interface RetentionImpactAnalysis {
setting: '2 months' | '14 months';
complianceBenefit: string;
analyticsImpact: string;
recommendation: string;
}
const retentionAnalysis: RetentionImpactAnalysis[] = [
{
setting: '2 months',
complianceBenefit: 'Maximum data minimization, strongest GDPR position',
analyticsImpact: 'Cannot analyze user behavior beyond 2 months, limited cohort analysis, no year-over-year user comparisons',
recommendation: 'Best for EU-focused sites prioritizing compliance'
},
{
setting: '14 months',
complianceBenefit: 'Still reasonable retention, covers annual business cycles',
analyticsImpact: 'Full seasonal analysis, year-over-year comparisons possible, better LTV modeling',
recommendation: 'Suitable for most businesses with proper consent'
}
];
// TypeScript class for managing data retention
class GA4RetentionManager {
private propertyId: string;
private adminApiClient: any; // Google Analytics Admin API client
constructor(propertyId: string) {
this.propertyId = propertyId;
}
async getCurrentRetentionSettings(): Promise {
// Using Google Analytics Admin API
const response = await this.adminApiClient.properties.getDataRetentionSettings({
name: `properties/${this.propertyId}/dataRetentionSettings`
});
return {
userAndEventData: response.data.eventDataRetention === 'TWO_MONTHS'
? '2 months'
: '14 months',
resetOnNewActivity: response.data.resetUserDataOnNewActivity,
userIdentifierRetention: 'same_as_event_data'
};
}
async setOptimalRetentionForRegion(region: 'EU' | 'US' | 'GLOBAL'): Promise {
const settings = this.getRecommendedSettings(region);
await this.adminApiClient.properties.updateDataRetentionSettings({
name: `properties/${this.propertyId}/dataRetentionSettings`,
requestBody: {
eventDataRetention: settings.userAndEventData === '2 months'
? 'TWO_MONTHS'
: 'FOURTEEN_MONTHS',
resetUserDataOnNewActivity: settings.resetOnNewActivity
}
});
console.log(`Data retention updated for ${region}:`, settings);
}
private getRecommendedSettings(region: string): GA4DataRetentionConfig {
switch (region) {
case 'EU':
return {
userAndEventData: '2 months',
resetOnNewActivity: false, // More privacy-friendly
userIdentifierRetention: 'same_as_event_data'
};
case 'US':
return {
userAndEventData: '14 months',
resetOnNewActivity: true,
userIdentifierRetention: 'same_as_event_data'
};
default:
return {
userAndEventData: '14 months',
resetOnNewActivity: true,
userIdentifierRetention: 'same_as_event_data'
};
}
}
// Generate data deletion requests for DSAR compliance
async processDataDeletionRequest(userId: string): Promise {
// GA4 doesn't support individual user deletion via API
// You must use the User Explorer and manual deletion
// Or implement User-ID deletion through BigQuery export
return {
success: false,
method: 'manual',
instructions: [
'1. Go to GA4 Admin > Data Display > User Explorer',
`2. Search for user ID: ${userId}`,
'3. Click on the user row',
'4. Use "Request user data deletion" option',
'5. Confirm deletion (irreversible)',
'6. Document the deletion for compliance records'
],
automationNote: 'For automated DSAR compliance, implement User-ID mapping with BigQuery export and custom deletion workflows'
};
}
}
interface DeletionResult {
success: boolean;
method: 'api' | 'manual';
instructions?: string[];
automationNote?: string;
}
```
### 2. Google Signals: The Double-Edged Sword
Google Signals enables cross-device tracking and demographic reporting, but it's one of the highest-risk features from a privacy perspective.
```typescript
// google-signals-manager.ts
interface GoogleSignalsConfig {
enabled: boolean;
enabledRegions: string[];
excludedRegions: string[];
dataCollectionEnabled: boolean;
remarketing: boolean;
}
interface SignalsRiskAssessment {
feature: string;
riskLevel: 'low' | 'medium' | 'high' | 'critical';
gdprConcerns: string[];
mitigations: string[];
}
const signalsRiskAssessment: SignalsRiskAssessment[] = [
{
feature: 'Cross-device tracking',
riskLevel: 'high',
gdprConcerns: [
'Creates persistent identifier across devices',
'User may not understand tracking scope',
'Difficult to provide meaningful consent'
],
mitigations: [
'Disable for EU users',
'Clear disclosure in privacy policy',
'Explicit consent checkbox for cross-device'
]
},
{
feature: 'Demographics and Interests',
riskLevel: 'high',
gdprConcerns: [
'Infers sensitive categories (age, gender)',
'Based on Google account data',
'Data leaves your control'
],
mitigations: [
'Disable entirely for EU',
'Use first-party survey data instead',
'Implement server-side collection'
]
},
{
feature: 'Remarketing audiences',
riskLevel: 'critical',
gdprConcerns: [
'Shares user data with Google Ads',
'Third-party processing without clear basis',
'Complex data flow to document'
],
mitigations: [
'Never enable for EU without explicit consent',
'Use Customer Match with consent instead',
'Implement consent-based audience building'
]
}
];
class GoogleSignalsManager {
private propertyId: string;
private adminApiClient: any;
constructor(propertyId: string) {
this.propertyId = propertyId;
}
async getSignalsStatus(): Promise {
const response = await this.adminApiClient.properties.getGoogleSignalsSettings({
name: `properties/${this.propertyId}/googleSignalsSettings`
});
return {
enabled: response.data.state === 'GOOGLE_SIGNALS_ENABLED',
enabledRegions: response.data.consent?.enabledRegions || [],
excludedRegions: response.data.consent?.excludedRegions || [],
dataCollectionEnabled: true, // Tied to enabled state
remarketing: response.data.state === 'GOOGLE_SIGNALS_ENABLED'
};
}
async configureForGDPRCompliance(): Promise {
// Disable Google Signals entirely for strictest compliance
await this.adminApiClient.properties.updateGoogleSignalsSettings({
name: `properties/${this.propertyId}/googleSignalsSettings`,
requestBody: {
state: 'GOOGLE_SIGNALS_DISABLED'
}
});
console.log('Google Signals disabled for GDPR compliance');
}
async configureRegionalSignals(config: RegionalSignalsConfig): Promise {
// Unfortunately, GA4 doesn't support regional Signals at property level
// You must implement this at the tag level using GTM
throw new Error(
'Regional Signals configuration requires GTM implementation. ' +
'See configureGTMRegionalSignals() method.'
);
}
// GTM-based regional signals control
generateGTMRegionalConfig(): GTMConfiguration {
return {
triggerName: 'Google Signals - Non-EU Only',
triggerType: 'Custom Event',
conditions: [
{
variable: '{{User Region}}',
operator: 'does not match RegEx',
value: '^(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|NO|IS|LI|CH)$'
}
],
tag: {
type: 'GA4 Configuration',
settings: {
allow_google_signals: true,
allow_ad_personalization_signals: true
}
},
fallbackTag: {
type: 'GA4 Configuration',
settings: {
allow_google_signals: false,
allow_ad_personalization_signals: false
}
}
};
}
}
interface RegionalSignalsConfig {
euEnabled: boolean;
usEnabled: boolean;
otherEnabled: boolean;
}
interface GTMConfiguration {
triggerName: string;
triggerType: string;
conditions: Array<{
variable: string;
operator: string;
value: string;
}>;
tag: {
type: string;
settings: Record;
};
fallbackTag: {
type: string;
settings: Record;
};
}
```
### 3. Granular Location and Device Data
GA4 allows you to control how much location and device information is collected:
```typescript
// granular-data-control.ts
interface GranularDataSettings {
// Location granularity
collectCity: boolean;
collectRegion: boolean;
collectCountry: boolean;
// Device data
collectDeviceModel: boolean;
collectPlatformVersion: boolean;
collectScreenResolution: boolean;
collectBrowser: boolean;
collectBrowserVersion: boolean;
}
interface PrivacyImpact {
dataPoint: string;
identifiabilityRisk: 'low' | 'medium' | 'high';
analyticsValue: 'low' | 'medium' | 'high';
recommendation: string;
}
const granularDataPrivacyAnalysis: PrivacyImpact[] = [
{
dataPoint: 'City-level location',
identifiabilityRisk: 'medium',
analyticsValue: 'high',
recommendation: 'Disable for EU, enable elsewhere with consent'
},
{
dataPoint: 'Device model',
identifiabilityRisk: 'low',
analyticsValue: 'medium',
recommendation: 'Generally safe to collect for debugging'
},
{
dataPoint: 'Screen resolution',
identifiabilityRisk: 'medium',
analyticsValue: 'medium',
recommendation: 'Combined with other data can fingerprint - consider disabling'
},
{
dataPoint: 'Browser version',
identifiabilityRisk: 'medium',
analyticsValue: 'low',
recommendation: 'Can contribute to fingerprinting - disable for strict compliance'
}
];
class GranularDataController {
private measurementId: string;
constructor(measurementId: string) {
this.measurementId = measurementId;
}
// Configure gtag for privacy-respecting data collection
generatePrivacyConfig(level: 'strict' | 'moderate' | 'standard'): GtagConfig {
switch (level) {
case 'strict':
return {
// Minimal data collection for EU
send_page_view: true,
cookie_flags: 'SameSite=Strict;Secure',
client_storage: 'none',
anonymize_ip: true, // Redundant in GA4 but explicit
allow_google_signals: false,
allow_ad_personalization_signals: false,
restricted_data_processing: true,
// Disable granular data
redact_device_info: true,
// Disable user properties that could identify
user_properties: {}
};
case 'moderate':
return {
send_page_view: true,
cookie_flags: 'SameSite=Lax;Secure',
anonymize_ip: true,
allow_google_signals: false,
allow_ad_personalization_signals: false,
restricted_data_processing: false
};
case 'standard':
return {
send_page_view: true,
cookie_flags: 'SameSite=Lax;Secure',
anonymize_ip: true,
allow_google_signals: true,
allow_ad_personalization_signals: true
};
}
}
// Implement in GTM
generateGTMVariableConfig(): GTMVariableConfig[] {
return [
{
name: 'GA4 Config - Strict Privacy',
type: 'Google Analytics: GA4 Configuration',
measurementId: this.measurementId,
fieldsToSet: [
{ name: 'allow_google_signals', value: 'false' },
{ name: 'allow_ad_personalization_signals', value: 'false' },
{ name: 'restricted_data_processing', value: 'true' }
],
trigger: 'Consent - Analytics Granted - EU'
},
{
name: 'GA4 Config - Standard',
type: 'Google Analytics: GA4 Configuration',
measurementId: this.measurementId,
fieldsToSet: [
{ name: 'allow_google_signals', value: 'true' },
{ name: 'allow_ad_personalization_signals', value: 'true' }
],
trigger: 'Consent - Analytics Granted - Non-EU'
}
];
}
}
interface GtagConfig {
send_page_view: boolean;
cookie_flags?: string;
client_storage?: 'none' | 'cookie';
anonymize_ip?: boolean;
allow_google_signals?: boolean;
allow_ad_personalization_signals?: boolean;
restricted_data_processing?: boolean;
redact_device_info?: boolean;
user_properties?: Record;
}
interface GTMVariableConfig {
name: string;
type: string;
measurementId: string;
fieldsToSet: Array<{ name: string; value: string }>;
trigger: string;
}
```
### 4. Consent Mode v2 Integration
Consent Mode is critical for maintaining some analytics capability while respecting user choices. Our [Consent Mode Guide](/blog/google-consent-mode-v2-implementation-guide-2025) covers this in depth, but here's the GA4-specific implementation:
```typescript
// ga4-consent-mode-integration.ts
interface ConsentModeState {
ad_storage: 'granted' | 'denied';
ad_user_data: 'granted' | 'denied';
ad_personalization: 'granted' | 'denied';
analytics_storage: 'granted' | 'denied';
functionality_storage?: 'granted' | 'denied';
personalization_storage?: 'granted' | 'denied';
security_storage?: 'granted' | 'denied';
}
interface ConsentModeConfig {
defaultState: ConsentModeState;
waitForUpdate: number; // milliseconds
urlPassthrough: boolean;
adsDataRedaction: boolean;
regions: RegionConsentConfig[];
}
interface RegionConsentConfig {
region: string[];
defaultState: ConsentModeState;
}
class GA4ConsentModeManager {
private config: ConsentModeConfig;
private currentState: ConsentModeState;
constructor() {
this.config = this.getDefaultConfig();
this.currentState = this.config.defaultState;
}
private getDefaultConfig(): ConsentModeConfig {
return {
defaultState: {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
functionality_storage: 'granted',
personalization_storage: 'denied',
security_storage: 'granted'
},
waitForUpdate: 500,
urlPassthrough: true, // Allow URL parameters through for attribution
adsDataRedaction: true, // Redact ad-click identifiers when denied
regions: [
{
region: ['US'],
defaultState: {
ad_storage: 'granted',
ad_user_data: 'granted',
ad_personalization: 'granted',
analytics_storage: 'granted'
}
},
{
region: ['US-CA'], // California - stricter
defaultState: {
ad_storage: 'granted',
ad_user_data: 'denied', // Requires explicit consent
ad_personalization: 'denied',
analytics_storage: 'granted'
}
}
]
};
}
// Generate the initial consent mode script
generateInitialScript(): string {
return `
`;
}
// Update consent based on user choice
updateConsent(userConsent: UserConsentChoice): void {
const newState: ConsentModeState = {
ad_storage: userConsent.marketing ? 'granted' : 'denied',
ad_user_data: userConsent.marketing ? 'granted' : 'denied',
ad_personalization: userConsent.personalization ? 'granted' : 'denied',
analytics_storage: userConsent.analytics ? 'granted' : 'denied',
functionality_storage: userConsent.functional ? 'granted' : 'denied',
personalization_storage: userConsent.personalization ? 'granted' : 'denied'
};
// Push consent update
if (typeof gtag === 'function') {
gtag('consent', 'update', newState);
}
this.currentState = newState;
// Log for debugging
console.log('Consent Mode updated:', newState);
// Emit event for other systems
window.dispatchEvent(new CustomEvent('consentModeUpdate', {
detail: newState
}));
}
// Check what data GA4 is currently collecting
getCollectionStatus(): GA4CollectionStatus {
return {
cookiesAllowed: this.currentState.analytics_storage === 'granted',
adTrackingAllowed: this.currentState.ad_storage === 'granted',
modelingEnabled: this.currentState.analytics_storage === 'denied',
dataRedaction: this.currentState.ad_storage === 'denied',
estimatedDataLoss: this.calculateDataLoss()
};
}
private calculateDataLoss(): DataLossEstimate {
// When analytics_storage is denied, GA4 uses modeling
// Typical accuracy loss is 15-30% depending on consent rate
const analyticsGranted = this.currentState.analytics_storage === 'granted';
return {
conversionAccuracy: analyticsGranted ? '100%' : '70-85% (modeled)',
userCountAccuracy: analyticsGranted ? '100%' : '75-90% (modeled)',
attributionAccuracy: analyticsGranted ? '100%' : '60-80% (modeled)',
behaviorDataComplete: analyticsGranted
};
}
}
interface UserConsentChoice {
analytics: boolean;
marketing: boolean;
personalization: boolean;
functional: boolean;
}
interface GA4CollectionStatus {
cookiesAllowed: boolean;
adTrackingAllowed: boolean;
modelingEnabled: boolean;
dataRedaction: boolean;
estimatedDataLoss: DataLossEstimate;
}
interface DataLossEstimate {
conversionAccuracy: string;
userCountAccuracy: string;
attributionAccuracy: string;
behaviorDataComplete: boolean;
}
```
## Server-Side GTM: Maximum Privacy Control
For maximum privacy control, proxy GA4 traffic through Server-Side GTM. This allows you to strip PII (Personally Identifiable Information) and user-agent strings *before* the data is sent to Google's servers.
### Server-Side GTM Architecture
```typescript
// server-side-gtm-privacy.ts
interface ServerSideGTMConfig {
containerUrl: string; // Your sGTM endpoint
firstPartyDomain: string;
dataRedaction: DataRedactionConfig;
ipHandling: IPHandlingConfig;
userAgentHandling: UserAgentHandlingConfig;
}
interface DataRedactionConfig {
redactPII: boolean;
piiPatterns: RegExp[];
hashUserIds: boolean;
hashingAlgorithm: 'sha256' | 'md5';
salt: string;
}
interface IPHandlingConfig {
removeLastOctet: boolean;
fullAnonymization: boolean;
countryOnly: boolean;
}
interface UserAgentHandlingConfig {
fullRedaction: boolean;
reduceToEssentials: boolean;
clientHintsOnly: boolean;
}
class ServerSideGTMManager {
private config: ServerSideGTMConfig;
constructor(config: ServerSideGTMConfig) {
this.config = config;
}
// Generate client-side GA4 config for server-side proxy
generateClientConfig(): ClientSideConfig {
return {
// Point to your server-side container
transportUrl: this.config.containerUrl,
firstPartyCollection: true,
// Configure first-party cookies
cookieConfig: {
domain: this.config.firstPartyDomain,
prefix: '_ga', // Keep standard prefix
flags: 'SameSite=Lax;Secure',
update: true
}
};
}
// Server-side transformation template (for GTM server container)
generateServerTemplate(): ServerTemplateConfig {
return {
templateName: 'Privacy-Enhanced GA4 Proxy',
// Request transformation
requestTransformations: [
{
name: 'Remove IP Address',
field: 'ip_override',
action: 'remove'
},
{
name: 'Anonymize User Agent',
field: 'user_agent',
action: 'transform',
transformation: (ua: string) => this.anonymizeUserAgent(ua)
},
{
name: 'Hash User ID',
field: 'user_id',
action: 'transform',
transformation: (uid: string) => this.hashIdentifier(uid)
},
{
name: 'Hash Client ID',
field: 'client_id',
action: 'transform',
transformation: (cid: string) => this.hashIdentifier(cid)
},
{
name: 'Redact PII from event parameters',
field: 'event_parameters',
action: 'transform',
transformation: (params: any) => this.redactPII(params)
}
],
// Data enrichment (server-side)
enrichments: [
{
name: 'Add processing timestamp',
field: 'server_timestamp',
value: () => new Date().toISOString()
},
{
name: 'Add consent state',
field: 'consent_state',
value: (request: any) => this.extractConsentState(request)
}
],
// Forwarding rules
forwardingRules: [
{
destination: 'Google Analytics',
condition: (request: any) => request.consent?.analytics === 'granted',
config: {
measurementId: 'G-XXXXXXXX'
}
},
{
destination: 'BigQuery (Raw)',
condition: () => true, // Always forward to your own storage
config: {
projectId: 'your-project',
datasetId: 'analytics_raw',
tableId: 'events'
}
}
]
};
}
private anonymizeUserAgent(ua: string): string {
// Reduce to essential information only
const browserMatch = ua.match(/(Chrome|Firefox|Safari|Edge)\/[\d.]+/);
const osMatch = ua.match(/(Windows|Mac OS X|Linux|Android|iOS)[\s\d._]*/);
const browser = browserMatch ? browserMatch[0].split('/')[0] : 'Unknown';
const os = osMatch ? osMatch[1] : 'Unknown';
return `${browser}; ${os}`;
}
private hashIdentifier(id: string): string {
if (!id) return '';
// Use SHA-256 with salt for hashing
const saltedId = `${this.config.dataRedaction.salt}${id}`;
return this.sha256(saltedId);
}
private sha256(str: string): string {
// Server-side implementation would use crypto library
// This is a placeholder for the concept
const crypto = require('crypto');
return crypto.createHash('sha256').update(str).digest('hex');
}
private redactPII(params: Record): Record {
const redacted = { ...params };
const piiFields = ['email', 'phone', 'name', 'address', 'credit_card'];
for (const field of piiFields) {
if (redacted[field]) {
redacted[field] = '[REDACTED]';
}
}
// Check for PII patterns in string values
for (const [key, value] of Object.entries(redacted)) {
if (typeof value === 'string') {
redacted[key] = this.redactPIIFromString(value);
}
}
return redacted;
}
private redactPIIFromString(str: string): string {
let result = str;
// Email pattern
result = result.replace(
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
'[EMAIL_REDACTED]'
);
// Phone pattern (various formats)
result = result.replace(
/(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g,
'[PHONE_REDACTED]'
);
// Credit card pattern
result = result.replace(
/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
'[CC_REDACTED]'
);
return result;
}
private extractConsentState(request: any): ConsentModeState {
// Extract consent from request headers or cookies
return {
ad_storage: request.cookies?.consent_ads || 'denied',
ad_user_data: request.cookies?.consent_ads || 'denied',
ad_personalization: request.cookies?.consent_personalization || 'denied',
analytics_storage: request.cookies?.consent_analytics || 'denied'
};
}
}
interface ClientSideConfig {
transportUrl: string;
firstPartyCollection: boolean;
cookieConfig: {
domain: string;
prefix: string;
flags: string;
update: boolean;
};
}
interface ServerTemplateConfig {
templateName: string;
requestTransformations: Array<{
name: string;
field: string;
action: 'remove' | 'transform';
transformation?: (value: any) => any;
}>;
enrichments: Array<{
name: string;
field: string;
value: (request?: any) => any;
}>;
forwardingRules: Array<{
destination: string;
condition: (request: any) => boolean;
config: Record;
}>;
}
```
### Complete Server-Side Implementation
```typescript
// complete-sgtm-implementation.ts
interface SGTMDeploymentConfig {
cloudProvider: 'gcp' | 'aws' | 'cloudflare';
region: string;
scaling: ScalingConfig;
security: SecurityConfig;
monitoring: MonitoringConfig;
}
interface ScalingConfig {
minInstances: number;
maxInstances: number;
targetCPUUtilization: number;
}
interface SecurityConfig {
enableHTTPS: boolean;
customDomain: string;
corsOrigins: string[];
rateLimiting: {
enabled: boolean;
requestsPerMinute: number;
};
}
interface MonitoringConfig {
enableLogging: boolean;
alertOnErrors: boolean;
alertThreshold: number;
}
class SGTMDeployer {
// Generate GCP Cloud Run deployment config
generateGCPConfig(config: SGTMDeploymentConfig): GCPDeployment {
return {
service: {
name: 'gtm-server-side',
region: config.region,
template: {
containers: [{
image: 'gcr.io/cloud-tagging-10302018/gtm-cloud-image:stable',
ports: [{ containerPort: 8080 }],
env: [
{ name: 'CONTAINER_CONFIG', value: '${CONTAINER_CONFIG}' },
{ name: 'PREVIEW_SERVER_URL', value: '${PREVIEW_URL}' }
],
resources: {
limits: {
cpu: '1000m',
memory: '512Mi'
}
}
}],
scaling: {
minInstanceCount: config.scaling.minInstances,
maxInstanceCount: config.scaling.maxInstances
}
}
},
// Custom domain mapping
domainMapping: {
domain: config.security.customDomain,
certificateMode: 'AUTOMATIC'
},
// IAM for security
iamPolicy: {
bindings: [{
role: 'roles/run.invoker',
members: ['allUsers'] // Public endpoint
}]
}
};
}
// Generate Terraform for infrastructure as code
generateTerraform(config: SGTMDeploymentConfig): string {
return `
# Server-Side GTM - Terraform Configuration
# Generated for GDPR-compliant analytics
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
}
}
variable "project_id" {
description = "GCP Project ID"
type = string
}
variable "container_config" {
description = "GTM Container Config (base64 encoded)"
type = string
sensitive = true
}
variable "custom_domain" {
description = "Custom domain for sGTM"
type = string
default = "${config.security.customDomain}"
}
# Cloud Run Service
resource "google_cloud_run_service" "gtm_server" {
name = "gtm-server-side"
location = "${config.region}"
project = var.project_id
template {
spec {
containers {
image = "gcr.io/cloud-tagging-10302018/gtm-cloud-image:stable"
env {
name = "CONTAINER_CONFIG"
value = var.container_config
}
resources {
limits = {
cpu = "1000m"
memory = "512Mi"
}
}
}
container_concurrency = 80
timeout_seconds = 300
}
metadata {
annotations = {
"autoscaling.knative.dev/minScale" = "${config.scaling.minInstances}"
"autoscaling.knative.dev/maxScale" = "${config.scaling.maxInstances}"
}
}
}
traffic {
percent = 100
latest_revision = true
}
}
# Allow public access
resource "google_cloud_run_service_iam_member" "public_access" {
service = google_cloud_run_service.gtm_server.name
location = google_cloud_run_service.gtm_server.location
project = var.project_id
role = "roles/run.invoker"
member = "allUsers"
}
# Custom domain mapping
resource "google_cloud_run_domain_mapping" "custom_domain" {
location = google_cloud_run_service.gtm_server.location
name = var.custom_domain
project = var.project_id
metadata {
namespace = var.project_id
}
spec {
route_name = google_cloud_run_service.gtm_server.name
}
}
# Output the URLs
output "service_url" {
value = google_cloud_run_service.gtm_server.status[0].url
}
output "custom_domain_url" {
value = "https://\${var.custom_domain}"
}
`;
}
}
interface GCPDeployment {
service: {
name: string;
region: string;
template: {
containers: Array<{
image: string;
ports: Array<{ containerPort: number }>;
env: Array<{ name: string; value: string }>;
resources: {
limits: {
cpu: string;
memory: string;
};
};
}>;
scaling: {
minInstanceCount: number;
maxInstanceCount: number;
};
};
};
domainMapping: {
domain: string;
certificateMode: string;
};
iamPolicy: {
bindings: Array<{
role: string;
members: string[];
}>;
};
}
```
## BigQuery Export: Privacy-First Data Lake
For organizations that need full control over their analytics data, BigQuery export provides a way to store raw event data in your own infrastructure.
```typescript
// bigquery-privacy-export.ts
interface BigQueryExportConfig {
projectId: string;
datasetId: string;
dataRetentionDays: number;
piiHandling: PIIHandlingConfig;
accessControls: AccessControlConfig;
}
interface PIIHandlingConfig {
hashUserIds: boolean;
removeIPAddresses: boolean;
anonymizeDeviceIds: boolean;
redactUserAgents: boolean;
}
interface AccessControlConfig {
adminGroup: string;
analystGroup: string;
restrictPIIAccess: boolean;
auditLogging: boolean;
}
class BigQueryPrivacyManager {
private config: BigQueryExportConfig;
private bigquery: any; // BigQuery client
constructor(config: BigQueryExportConfig) {
this.config = config;
}
// Create privacy-compliant dataset schema
async createDataset(): Promise {
const dataset = this.bigquery.dataset(this.config.datasetId);
// Create dataset with default encryption
await dataset.create({
location: 'EU', // For GDPR data residency
defaultPartitionExpirationMs: this.config.dataRetentionDays * 24 * 60 * 60 * 1000,
defaultEncryptionConfiguration: {
kmsKeyName: 'projects/your-project/locations/eu/keyRings/analytics/cryptoKeys/ga4'
},
labels: {
'data-classification': 'confidential',
'gdpr-compliant': 'true'
}
});
// Create events table with privacy-friendly schema
await this.createEventsTable(dataset);
// Create user lookup table (pseudonymized)
await this.createUserTable(dataset);
// Set up access controls
await this.configureAccessControls(dataset);
}
private async createEventsTable(dataset: any): Promise {
const schema = [
{ name: 'event_date', type: 'DATE', mode: 'REQUIRED' },
{ name: 'event_timestamp', type: 'TIMESTAMP', mode: 'REQUIRED' },
{ name: 'event_name', type: 'STRING', mode: 'REQUIRED' },
{ name: 'event_params', type: 'RECORD', mode: 'REPEATED', fields: [
{ name: 'key', type: 'STRING' },
{ name: 'value', type: 'RECORD', fields: [
{ name: 'string_value', type: 'STRING' },
{ name: 'int_value', type: 'INTEGER' },
{ name: 'float_value', type: 'FLOAT' },
{ name: 'double_value', type: 'FLOAT' }
]}
]},
// Pseudonymized identifiers (hashed)
{ name: 'user_pseudo_id_hash', type: 'STRING', mode: 'REQUIRED',
description: 'SHA-256 hash of user_pseudo_id' },
{ name: 'user_id_hash', type: 'STRING',
description: 'SHA-256 hash of user_id if set' },
// Aggregated location (country only, no city)
{ name: 'geo_country', type: 'STRING' },
{ name: 'geo_continent', type: 'STRING' },
// Reduced device info
{ name: 'device_category', type: 'STRING' },
{ name: 'device_operating_system', type: 'STRING' },
{ name: 'device_browser', type: 'STRING' },
// Traffic source
{ name: 'traffic_source', type: 'RECORD', fields: [
{ name: 'source', type: 'STRING' },
{ name: 'medium', type: 'STRING' },
{ name: 'campaign', type: 'STRING' }
]},
// Consent state at time of event
{ name: 'consent_state', type: 'RECORD', fields: [
{ name: 'analytics', type: 'STRING' },
{ name: 'ads', type: 'STRING' },
{ name: 'personalization', type: 'STRING' }
]},
// Processing metadata
{ name: 'processing_timestamp', type: 'TIMESTAMP' },
{ name: 'data_source', type: 'STRING' }
];
await dataset.createTable('events', {
schema: schema,
timePartitioning: {
type: 'DAY',
field: 'event_date',
expirationMs: this.config.dataRetentionDays * 24 * 60 * 60 * 1000
},
clustering: {
fields: ['event_name', 'geo_country']
}
});
}
private async createUserTable(dataset: any): Promise {
// Separate table for user mapping (restricted access)
const schema = [
{ name: 'user_pseudo_id_hash', type: 'STRING', mode: 'REQUIRED' },
{ name: 'first_seen_date', type: 'DATE', mode: 'REQUIRED' },
{ name: 'last_seen_date', type: 'DATE' },
{ name: 'user_properties', type: 'RECORD', mode: 'REPEATED', fields: [
{ name: 'key', type: 'STRING' },
{ name: 'value', type: 'STRING' }
]},
{ name: 'consent_history', type: 'RECORD', mode: 'REPEATED', fields: [
{ name: 'timestamp', type: 'TIMESTAMP' },
{ name: 'analytics', type: 'STRING' },
{ name: 'ads', type: 'STRING' }
]},
// For DSAR processing
{ name: 'deletion_requested', type: 'BOOLEAN' },
{ name: 'deletion_request_date', type: 'TIMESTAMP' }
];
await dataset.createTable('users_pseudonymized', {
schema: schema
});
}
private async configureAccessControls(dataset: any): Promise {
// Set up column-level security for PII
const policy = {
bindings: [
{
role: 'roles/bigquery.dataViewer',
members: [`group:${this.config.accessControls.analystGroup}`],
condition: {
expression: `resource.type == "bigquery.googleapis.com/Table" &&
resource.name.contains("events")`,
title: 'Events table access only'
}
},
{
role: 'roles/bigquery.dataViewer',
members: [`group:${this.config.accessControls.adminGroup}`]
}
]
};
await dataset.setIamPolicy(policy);
}
// Generate view for analysts (no PII)
generateAnalystView(): string {
return `
-- Analyst-safe view (no direct identifiers)
CREATE OR REPLACE VIEW \`${this.config.projectId}.${this.config.datasetId}.events_analyst_view\`
AS
SELECT
event_date,
event_timestamp,
event_name,
event_params,
geo_country,
geo_continent,
device_category,
device_operating_system,
device_browser,
traffic_source,
-- Aggregate user_pseudo_id_hash for session analysis without exposing ID
FARM_FINGERPRINT(user_pseudo_id_hash) % 1000000 as user_bucket,
consent_state.analytics as analytics_consent
FROM
\`${this.config.projectId}.${this.config.datasetId}.events\`
WHERE
-- Only include data where analytics consent was granted
consent_state.analytics = 'granted'
-- Exclude any rows marked for deletion
AND user_pseudo_id_hash NOT IN (
SELECT user_pseudo_id_hash
FROM \`${this.config.projectId}.${this.config.datasetId}.users_pseudonymized\`
WHERE deletion_requested = TRUE
);
`;
}
// DSAR compliance: Generate deletion query
generateDeletionQuery(userPseudoIdHash: string): string {
return `
-- DSAR Deletion Request
-- User Hash: ${userPseudoIdHash}
-- Generated: ${new Date().toISOString()}
BEGIN TRANSACTION;
-- Mark user for deletion
UPDATE \`${this.config.projectId}.${this.config.datasetId}.users_pseudonymized\`
SET
deletion_requested = TRUE,
deletion_request_date = CURRENT_TIMESTAMP()
WHERE user_pseudo_id_hash = '${userPseudoIdHash}';
-- Delete user events
DELETE FROM \`${this.config.projectId}.${this.config.datasetId}.events\`
WHERE user_pseudo_id_hash = '${userPseudoIdHash}';
-- Log the deletion for audit
INSERT INTO \`${this.config.projectId}.${this.config.datasetId}.deletion_log\`
(user_pseudo_id_hash, deletion_timestamp, deletion_type, requested_by)
VALUES
('${userPseudoIdHash}', CURRENT_TIMESTAMP(), 'DSAR', 'automated');
COMMIT TRANSACTION;
`;
}
}
```
## Implementation Checklist
### GA4 Privacy Configuration Checklist
| Setting | GDPR (EU) | CCPA (CA) | Standard |
|---------|-----------|-----------|----------|
| Data Retention | 2 months | 14 months | 14 months |
| Google Signals | Disabled | Disabled | Enabled |
| Granular Location | Disabled | Enabled | Enabled |
| Consent Mode | Required | Required | Optional |
| Server-Side GTM | Recommended | Optional | Optional |
| IP Anonymization | Default (automatic) | Default | Default |
| User-ID | With consent only | Allowed | Allowed |
| BigQuery Export | To EU region | Any region | Any region |
### Pre-Launch Verification
```typescript
// ga4-compliance-checker.ts
interface ComplianceCheckResult {
category: string;
check: string;
status: 'pass' | 'fail' | 'warning';
details: string;
remediation?: string;
}
async function runGA4ComplianceChecks(
propertyId: string,
region: 'EU' | 'US' | 'GLOBAL'
): Promise {
const results: ComplianceCheckResult[] = [];
// Check 1: Data Retention
const retention = await checkDataRetention(propertyId);
results.push({
category: 'Data Minimization',
check: 'Data Retention Period',
status: region === 'EU' && retention !== '2 months' ? 'warning' : 'pass',
details: `Current retention: ${retention}`,
remediation: region === 'EU'
? 'Consider reducing to 2 months for stricter GDPR compliance'
: undefined
});
// Check 2: Google Signals
const signalsEnabled = await checkGoogleSignals(propertyId);
results.push({
category: 'Data Sharing',
check: 'Google Signals Status',
status: region === 'EU' && signalsEnabled ? 'fail' : 'pass',
details: `Google Signals: ${signalsEnabled ? 'Enabled' : 'Disabled'}`,
remediation: signalsEnabled && region === 'EU'
? 'Disable Google Signals for EU visitors'
: undefined
});
// Check 3: Consent Mode
const consentModeConfigured = await checkConsentMode(propertyId);
results.push({
category: 'Consent',
check: 'Consent Mode v2 Configuration',
status: !consentModeConfigured ? 'fail' : 'pass',
details: `Consent Mode: ${consentModeConfigured ? 'Configured' : 'Not configured'}`,
remediation: !consentModeConfigured
? 'Implement Consent Mode v2 for all Google tags'
: undefined
});
// Check 4: Enhanced Measurement
const enhancedMeasurement = await checkEnhancedMeasurement(propertyId);
results.push({
category: 'Data Collection',
check: 'Enhanced Measurement Settings',
status: 'pass', // Informational
details: `Enhanced Measurement features: ${JSON.stringify(enhancedMeasurement)}`,
remediation: 'Review each feature for privacy implications'
});
// Check 5: Data Streams
const dataStreams = await checkDataStreams(propertyId);
for (const stream of dataStreams) {
results.push({
category: 'Data Streams',
check: `Stream: ${stream.name}`,
status: stream.hasConsentConfig ? 'pass' : 'warning',
details: `Type: ${stream.type}, Consent configured: ${stream.hasConsentConfig}`,
remediation: !stream.hasConsentConfig
? 'Configure consent requirements for this data stream'
: undefined
});
}
return results;
}
// Generate compliance report
function generateComplianceReport(results: ComplianceCheckResult[]): string {
const passed = results.filter(r => r.status === 'pass').length;
const failed = results.filter(r => r.status === 'fail').length;
const warnings = results.filter(r => r.status === 'warning').length;
let report = `
# GA4 Privacy Compliance Report
Generated: ${new Date().toISOString()}
## Summary
- Passed: ${passed}
- Failed: ${failed}
- Warnings: ${warnings}
## Details
`;
for (const result of results) {
const icon = result.status === 'pass' ? '✅' :
result.status === 'fail' ? '❌' : '⚠️';
report += `
### ${icon} ${result.check}
**Category:** ${result.category}
**Status:** ${result.status.toUpperCase()}
**Details:** ${result.details}
${result.remediation ? `**Remediation:** ${result.remediation}` : ''}
`;
}
return report;
}
```
## Treat privacy as a GA4 feature
GA4 is powerful, but it requires active configuration to meet modern privacy laws. The default settings prioritize data volume, not data minimization. Regulators in multiple EU states have already questioned GA's transfers; you need to prove that your implementation honors consent choices, limits data retention, and routes traffic lawfully (for example via an EU-based server-side container paired with SCCs or the EU-U.S. Data Privacy Framework where applicable).
### Key Takeaways
1. **GA4 is not compliant out of the box** - You must configure it for your specific regulatory requirements
2. **Google Signals is high-risk** - Disable for EU visitors, consider disabling globally
3. **Consent Mode v2 is essential** - Required for any EU traffic, recommended everywhere
4. **Server-Side GTM provides maximum control** - Consider it for high-compliance environments
5. **BigQuery export enables data sovereignty** - Store data in your region under your control
6. **Regular audits are necessary** - Privacy settings can drift, especially after GA4 updates
### Next Steps
1. **Audit your current GA4 configuration** using the checklist above
2. **Implement Consent Mode v2** if you haven't already
3. **Disable Google Signals for EU traffic** at minimum
4. **Consider Server-Side GTM** for maximum privacy control
5. **Document your configuration** for compliance records
6. **Set up regular compliance checks** as GA4 evolves
By following this guide, you'll have a GA4 implementation that respects user privacy while still providing the insights you need to grow your business. Privacy and analytics aren't mutually exclusive—with the right configuration, you can have both.