Zurück zum Blog
Technical

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

GetCookies-TeamNovember 14, 202514 Min. Lesezeit
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:

// 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<GA4DataRetentionConfig> {
    // 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<void> {
    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<DeletionResult> {
    // 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.

// 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<GoogleSignalsConfig> {
    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<void> {
    // 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<void> {
    // 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<string, any>;
  };
  fallbackTag: {
    type: string;
    settings: Record<string, any>;
  };
}

3. Granular Location and Device Data

GA4 allows you to control how much location and device information is collected:

// 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<string, any>;
}

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 covers this in depth, but here's the GA4-specific implementation:

// 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 `
<!-- Google Consent Mode v2 - Initial Configuration -->
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}

  // Set default consent state BEFORE loading any Google tags
  gtag('consent', 'default', {
    'ad_storage': 'denied',
    'ad_user_data': 'denied',
    'ad_personalization': 'denied',
    'analytics_storage': 'denied',
    'functionality_storage': 'granted',
    'personalization_storage': 'denied',
    'security_storage': 'granted',
    'wait_for_update': 500
  });

  // Regional defaults (EU defaults apply globally, US overrides for US visitors)
  gtag('consent', 'default', {
    'ad_storage': 'granted',
    'ad_user_data': 'granted',
    'ad_personalization': 'granted',
    'analytics_storage': 'granted',
    'region': ['US']
  });

  // California - stricter requirements
  gtag('consent', 'default', {
    'ad_user_data': 'denied',
    'ad_personalization': 'denied',
    'region': ['US-CA']
  });

  // Enable URL passthrough for attribution preservation
  gtag('set', 'url_passthrough', true);

  // Enable ads data redaction when consent denied
  gtag('set', 'ads_data_redaction', true);
</script>
`;
  }

  // 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

// 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<string, any>): Record<string, any> {
    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<string, any>;
  }>;
}

Complete Server-Side Implementation

// 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.

// 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<void> {
    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<void> {
    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<void> {
    // 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<void> {
    // 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

// 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<ComplianceCheckResult[]> {
  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.

Häufig gestellte Fragen

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

GetCookies-Team

Geschrieben und gepflegt vom GetCookies-Team der Getia AS in Oslo, Norwegen. Dieser Artikel ist eine allgemeine Information zur Cookie-Einwilligung und keine Rechtsberatung.

Scannen Sie Ihre eigene Website kostenlos

Im kostenlosen Plan läuft das Banner auf einer Website, dazu drei Cookie-Scans pro Monat. Keine Kreditkarte, kein Verkaufsgespräch.