Tilbage til bloggen
Technical

GA4 Privacy Settings: Configuring Data Retention, Signals, and IP Masking

Sarah Chen, Privacy EngineerNovember 14, 202514 min læsning
GA4Google AnalyticsPrivacy SettingsGTM

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.

Ofte stillede spørgsmål

Does GA4 anonymize IP addresses?
Yes, unlike Universal Analytics, GA4 does not log or store individual IP addresses. It drops the last octet automatically.
S

Sarah Chen, Privacy Engineer

Skribent hos GetCookies, specialiseret i privatlivsoverholdelse, samtykkeadministration og optimering af digital markedsføring.

Klar til at forenkle cookiesamtykke?

GetCookies gør GDPR, CCPA og global privatlivsoverholdelse ubesværet. Kom i gang i dag.