Zpět na blog
Analytics

Consent-Aware Analytics: Advanced Attribution Models

Tým GetCookiesOctober 17, 202514 min čtení
AttributionAnalyticsMarketingPrivacy

TLDR: A large share of EU users reject analytics cookies. Your attribution models are trained on the rest, who may behave differently. That's why your ROAS looks great but revenue targets get missed.

Read full summary

Move beyond basic consent rates to understand consent quality, timing effects, and revenue attribution. Learn to build consent-aware attribution models that accurately measure marketing performance in a privacy-first world. This guide covers data-driven attribution, incrementality testing, media mix modeling, conversion modeling with Google Consent Mode v2, and privacy-preserving measurement techniques with complete TypeScript implementations.

Summary by Claude AI

The €4.2 Million Attribution Mistake

Illustrative scenario: a composite example, not a specific reported case.

A European online retailer's marketing team was confident in their attribution model. Google Ads showed a 380% ROAS. Meta reported 420% ROAS. The CMO approved a €1.2 million increase in digital ad spend.

Six months later, revenue had grown 8%—not the projected 35%. The CFO demanded an explanation. The marketing team discovered the problem: 47% of their German and French customers rejected analytics cookies. Their attribution models were trained entirely on the consenting 53%.

Here's what they missed: privacy-conscious users who reject cookies tend to be higher-value customers. They research more, compare prices less, and have higher lifetime value. But they were invisible to the attribution model. The model over-credited low-funnel campaigns that reached easy-to-track users while ignoring the brand awareness that influenced the invisible majority.

How does consent affect marketing attribution?

When users deny consent for analytics cookies, traditional marketing attribution models (like last-click, first-click, or linear) become fundamentally broken. These models rely on tracking the complete user journey from first touchpoint to conversion—without cookies, you're flying blind.

The impact can be significant: when a large share of visitors reject analytics cookies, your marketing team may be making big budget decisions on data that covers only part of your customer base. Even worse, the users who accept tracking often have different behaviors than those who don't, creating systematic bias in your attribution data.

This comprehensive guide explores how to build consent-aware attribution systems that maintain measurement accuracy while respecting user privacy choices. We'll cover advanced attribution models, Google Consent Mode v2 integration, privacy-preserving measurement techniques, and practical TypeScript implementations you can deploy today.

Why Traditional Attribution Fails in a Consent-First World

Before diving into solutions, let's understand exactly why traditional attribution breaks down when consent enters the picture.

The Consent Gap Problem

Traditional attribution assumes you can track every touchpoint in the customer journey:

// What traditional attribution expects
interface IdealUserJourney {
  touchpoints: Array<{
    channel: string;
    timestamp: Date;
    campaign: string;
    userId: string;  // Same user tracked across all touchpoints
  }>;
  conversion: {
    value: number;
    timestamp: Date;
    userId: string;  // Same user, linked to journey
  };
}

// What you actually get with consent
interface RealityUserJourney {
  // User visits via Google Ad - consented to tracking
  touchpoint1?: { channel: 'paid_search'; userId: 'abc123' };

  // Same user visits via email - different session, declined cookies
  touchpoint2?: { channel: 'email'; userId: undefined };  // Lost!

  // Same user converts - consented this time
  conversion: { value: 500; userId: 'xyz789' };  // Different ID!
}

The result? Your attribution model might credit the Google Ad with 100% of the conversion (last identifiable touch), completely missing the email campaign that actually drove the decision.

Consent Bias in Your Data

Users who accept cookies behave differently from those who decline:

interface ConsentBiasAnalysis {
  // Observed patterns from industry research
  consentingUsers: {
    averageSessionDuration: number;      // Typically 20-30% longer
    pagesPerSession: number;             // More pages viewed
    returnVisitRate: number;             // Higher return rates
    conversionRate: number;              // Often higher conversion
    demographicSkew: 'older' | 'mixed';  // Skews older in some markets
  };

  nonConsentingUsers: {
    averageSessionDuration: number;
    pagesPerSession: number;
    returnVisitRate: number;
    conversionRate: number;
    demographicSkew: 'younger' | 'privacy-conscious';
  };
}

// This means your "data-driven" decisions are based on
// a systematically biased sample of your actual customers

Building a Consent-Aware Attribution System

Let's build a comprehensive attribution system that accounts for consent status and models missing data accurately.

Core Attribution Engine

import { EventEmitter } from 'events';

interface TouchpointData {
  id: string;
  sessionId: string;
  channel: string;
  source: string;
  medium: string;
  campaign?: string;
  content?: string;
  keyword?: string;
  timestamp: Date;
  consentStatus: ConsentStatus;
  deviceFingerprint?: string;  // Privacy-safe device hints
  geoRegion: string;
}

interface ConsentStatus {
  analytics: boolean;
  marketing: boolean;
  timestamp: Date;
  method: 'explicit' | 'implicit' | 'modeled';
}

interface ConversionData {
  id: string;
  sessionId: string;
  value: number;
  currency: string;
  product?: string;
  category?: string;
  timestamp: Date;
  consentStatus: ConsentStatus;
}

interface AttributionResult {
  conversionId: string;
  totalValue: number;
  attributedTouchpoints: Array<{
    touchpointId: string;
    channel: string;
    credit: number;
    creditPercentage: number;
    confidence: number;
    modelType: string;
  }>;
  modeledData: {
    estimatedMissingTouchpoints: number;
    consentAdjustmentFactor: number;
    confidenceInterval: [number, number];
  };
  metadata: {
    model: string;
    computedAt: Date;
    dataCompleteness: number;
  };
}

class ConsentAwareAttributionEngine extends EventEmitter {
  private touchpointStore: Map<string, TouchpointData[]> = new Map();
  private conversionStore: Map<string, ConversionData> = new Map();
  private consentRatesByChannel: Map<string, number> = new Map();
  private channelPerformanceHistory: Map<string, ChannelMetrics> = new Map();

  constructor(
    private config: AttributionConfig
  ) {
    super();
    this.initializeHistoricalData();
  }

  private async initializeHistoricalData(): Promise<void> {
    // Load historical consent rates by channel
    // These inform our modeling of missing touchpoints
    const historicalData = await this.loadHistoricalConsentRates();

    for (const [channel, rate] of Object.entries(historicalData)) {
      this.consentRatesByChannel.set(channel, rate);
    }
  }

  async recordTouchpoint(data: TouchpointData): Promise<void> {
    const journeyId = await this.resolveJourneyId(data);

    const existing = this.touchpointStore.get(journeyId) || [];
    existing.push(data);
    this.touchpointStore.set(journeyId, existing);

    // Update consent rate statistics for this channel
    this.updateConsentRateStats(data.channel, data.consentStatus.analytics);

    this.emit('touchpoint:recorded', { journeyId, data });
  }

  async recordConversion(data: ConversionData): Promise<AttributionResult> {
    const journeyId = await this.resolveJourneyId(data);
    this.conversionStore.set(data.id, data);

    // Get known touchpoints for this journey
    const knownTouchpoints = this.touchpointStore.get(journeyId) || [];

    // Apply consent-aware attribution
    const result = await this.computeAttribution(
      journeyId,
      knownTouchpoints,
      data
    );

    this.emit('conversion:attributed', result);
    return result;
  }

  private async computeAttribution(
    journeyId: string,
    touchpoints: TouchpointData[],
    conversion: ConversionData
  ): Promise<AttributionResult> {
    // Step 1: Estimate missing touchpoints based on consent patterns
    const missingTouchpointEstimate = this.estimateMissingTouchpoints(
      touchpoints,
      conversion
    );

    // Step 2: Apply the appropriate attribution model
    const rawAttribution = await this.applyAttributionModel(
      touchpoints,
      conversion,
      this.config.model
    );

    // Step 3: Adjust for consent bias
    const adjustedAttribution = this.adjustForConsentBias(
      rawAttribution,
      missingTouchpointEstimate
    );

    // Step 4: Calculate confidence intervals
    const confidence = this.calculateConfidence(
      touchpoints.length,
      missingTouchpointEstimate.count,
      conversion.consentStatus
    );

    return {
      conversionId: conversion.id,
      totalValue: conversion.value,
      attributedTouchpoints: adjustedAttribution,
      modeledData: {
        estimatedMissingTouchpoints: missingTouchpointEstimate.count,
        consentAdjustmentFactor: missingTouchpointEstimate.adjustmentFactor,
        confidenceInterval: confidence.interval
      },
      metadata: {
        model: this.config.model,
        computedAt: new Date(),
        dataCompleteness: this.calculateDataCompleteness(
          touchpoints.length,
          missingTouchpointEstimate.count
        )
      }
    };
  }

  private estimateMissingTouchpoints(
    known: TouchpointData[],
    conversion: ConversionData
  ): { count: number; adjustmentFactor: number; byChannel: Map<string, number> } {
    // Calculate expected touchpoints based on:
    // 1. Historical average touchpoints per conversion
    // 2. Consent rates by channel
    // 3. Time between first known touch and conversion

    const avgTouchpointsPerConversion = this.config.historicalAvgTouchpoints || 4.5;
    const knownCount = known.length;

    // Calculate weighted consent rate based on known channels
    const channelsInJourney = new Set(known.map(t => t.channel));
    let weightedConsentRate = 0;
    let totalWeight = 0;

    for (const channel of channelsInJourney) {
      const rate = this.consentRatesByChannel.get(channel) || 0.5;
      const weight = this.channelPerformanceHistory.get(channel)?.touchpointFrequency || 1;
      weightedConsentRate += rate * weight;
      totalWeight += weight;
    }

    // Add estimate for channels not in journey (they might be missing due to no consent)
    const potentialMissingChannels = this.getPotentialMissingChannels(channelsInJourney);
    for (const channel of potentialMissingChannels) {
      const rate = this.consentRatesByChannel.get(channel) || 0.5;
      const weight = 0.5;  // Lower weight for speculative channels
      weightedConsentRate += rate * weight;
      totalWeight += weight;
    }

    weightedConsentRate = weightedConsentRate / totalWeight;

    // Estimate missing touchpoints
    const expectedTotal = avgTouchpointsPerConversion / weightedConsentRate;
    const estimatedMissing = Math.max(0, expectedTotal - knownCount);

    // Calculate adjustment factor for attribution values
    const adjustmentFactor = expectedTotal / Math.max(knownCount, 1);

    // Estimate missing touchpoints by channel
    const byChannel = new Map<string, number>();
    for (const channel of potentialMissingChannels) {
      const channelRate = this.consentRatesByChannel.get(channel) || 0.5;
      const channelFreq = this.channelPerformanceHistory.get(channel)?.touchpointFrequency || 0.1;
      byChannel.set(channel, estimatedMissing * channelFreq * (1 - channelRate));
    }

    return { count: estimatedMissing, adjustmentFactor, byChannel };
  }

  private async applyAttributionModel(
    touchpoints: TouchpointData[],
    conversion: ConversionData,
    model: string
  ): Promise<Array<{ touchpointId: string; channel: string; credit: number; creditPercentage: number; confidence: number; modelType: string }>> {
    switch (model) {
      case 'data-driven':
        return this.dataDrivenAttribution(touchpoints, conversion);
      case 'position-based':
        return this.positionBasedAttribution(touchpoints, conversion);
      case 'time-decay':
        return this.timeDecayAttribution(touchpoints, conversion);
      case 'linear':
        return this.linearAttribution(touchpoints, conversion);
      default:
        return this.dataDrivenAttribution(touchpoints, conversion);
    }
  }

  private async dataDrivenAttribution(
    touchpoints: TouchpointData[],
    conversion: ConversionData
  ): Promise<Array<{ touchpointId: string; channel: string; credit: number; creditPercentage: number; confidence: number; modelType: string }>> {
    // Data-driven attribution uses machine learning to assign credit
    // based on actual contribution to conversions

    if (touchpoints.length === 0) {
      return [];
    }

    // Get channel weights from our ML model
    const channelWeights = await this.getDataDrivenWeights(
      touchpoints.map(t => t.channel)
    );

    // Calculate position adjustments
    const positionMultipliers = touchpoints.map((_, index) => {
      // First and last touches get boosted
      if (index === 0) return 1.3;
      if (index === touchpoints.length - 1) return 1.2;
      return 1.0;
    });

    // Calculate time-decay adjustments
    const conversionTime = conversion.timestamp.getTime();
    const timeMultipliers = touchpoints.map(t => {
      const hoursSinceTouch = (conversionTime - t.timestamp.getTime()) / (1000 * 60 * 60);
      // Half-life of 7 days
      return Math.pow(0.5, hoursSinceTouch / 168);
    });

    // Combine all factors
    const rawScores = touchpoints.map((t, i) => ({
      touchpoint: t,
      score: (channelWeights.get(t.channel) || 1) *
             positionMultipliers[i] *
             timeMultipliers[i]
    }));

    const totalScore = rawScores.reduce((sum, r) => sum + r.score, 0);

    return rawScores.map(({ touchpoint, score }) => ({
      touchpointId: touchpoint.id,
      channel: touchpoint.channel,
      credit: (score / totalScore) * conversion.value,
      creditPercentage: (score / totalScore) * 100,
      confidence: this.calculateTouchpointConfidence(touchpoint),
      modelType: 'data-driven'
    }));
  }

  private timeDecayAttribution(
    touchpoints: TouchpointData[],
    conversion: ConversionData
  ): Array<{ touchpointId: string; channel: string; credit: number; creditPercentage: number; confidence: number; modelType: string }> {
    if (touchpoints.length === 0) return [];

    const conversionTime = conversion.timestamp.getTime();
    const halfLifeHours = this.config.timeDecayHalfLife || 168; // 7 days default

    const weights = touchpoints.map(t => {
      const hoursSinceTouch = (conversionTime - t.timestamp.getTime()) / (1000 * 60 * 60);
      return Math.pow(0.5, hoursSinceTouch / halfLifeHours);
    });

    const totalWeight = weights.reduce((sum, w) => sum + w, 0);

    return touchpoints.map((t, i) => ({
      touchpointId: t.id,
      channel: t.channel,
      credit: (weights[i] / totalWeight) * conversion.value,
      creditPercentage: (weights[i] / totalWeight) * 100,
      confidence: this.calculateTouchpointConfidence(t),
      modelType: 'time-decay'
    }));
  }

  private positionBasedAttribution(
    touchpoints: TouchpointData[],
    conversion: ConversionData
  ): Array<{ touchpointId: string; channel: string; credit: number; creditPercentage: number; confidence: number; modelType: string }> {
    if (touchpoints.length === 0) return [];

    const sorted = [...touchpoints].sort((a, b) =>
      a.timestamp.getTime() - b.timestamp.getTime()
    );

    // 40% to first, 40% to last, 20% distributed among middle
    const firstWeight = 0.4;
    const lastWeight = 0.4;
    const middleWeight = 0.2;

    return sorted.map((t, i) => {
      let weight: number;
      if (i === 0) {
        weight = firstWeight;
      } else if (i === sorted.length - 1) {
        weight = sorted.length === 1 ? 1 : lastWeight;
      } else {
        weight = middleWeight / (sorted.length - 2);
      }

      return {
        touchpointId: t.id,
        channel: t.channel,
        credit: weight * conversion.value,
        creditPercentage: weight * 100,
        confidence: this.calculateTouchpointConfidence(t),
        modelType: 'position-based'
      };
    });
  }

  private linearAttribution(
    touchpoints: TouchpointData[],
    conversion: ConversionData
  ): Array<{ touchpointId: string; channel: string; credit: number; creditPercentage: number; confidence: number; modelType: string }> {
    if (touchpoints.length === 0) return [];

    const equalWeight = 1 / touchpoints.length;

    return touchpoints.map(t => ({
      touchpointId: t.id,
      channel: t.channel,
      credit: equalWeight * conversion.value,
      creditPercentage: equalWeight * 100,
      confidence: this.calculateTouchpointConfidence(t),
      modelType: 'linear'
    }));
  }

  private adjustForConsentBias(
    rawAttribution: Array<{ touchpointId: string; channel: string; credit: number; creditPercentage: number; confidence: number; modelType: string }>,
    missingEstimate: { count: number; adjustmentFactor: number; byChannel: Map<string, number> }
  ): Array<{ touchpointId: string; channel: string; credit: number; creditPercentage: number; confidence: number; modelType: string }> {
    // Adjust credits based on estimated missing touchpoints
    // Channels with lower consent rates get boosted

    return rawAttribution.map(attr => {
      const channelConsentRate = this.consentRatesByChannel.get(attr.channel) || 0.5;

      // Channels with lower consent rates are likely underrepresented
      // Boost their attribution proportionally
      const boostFactor = 1 + ((1 - channelConsentRate) * 0.5);

      // Cap the boost to prevent runaway adjustments
      const cappedBoost = Math.min(boostFactor, 1.5);

      return {
        ...attr,
        credit: attr.credit * cappedBoost,
        creditPercentage: attr.creditPercentage * cappedBoost,
        confidence: attr.confidence * channelConsentRate  // Lower confidence for low-consent channels
      };
    });
  }

  private calculateConfidence(
    knownTouchpoints: number,
    estimatedMissing: number,
    conversionConsent: ConsentStatus
  ): { score: number; interval: [number, number] } {
    // Base confidence on data completeness
    const dataCompleteness = knownTouchpoints / (knownTouchpoints + estimatedMissing);

    // Adjust for consent quality
    const consentQuality = conversionConsent.method === 'explicit' ? 1.0 :
                          conversionConsent.method === 'implicit' ? 0.8 : 0.6;

    const score = dataCompleteness * consentQuality;

    // Calculate confidence interval (95%)
    const standardError = Math.sqrt((1 - score) / Math.max(knownTouchpoints, 1));
    const interval: [number, number] = [
      Math.max(0, score - 1.96 * standardError),
      Math.min(1, score + 1.96 * standardError)
    ];

    return { score, interval };
  }

  private calculateTouchpointConfidence(touchpoint: TouchpointData): number {
    let confidence = 1.0;

    // Reduce confidence for modeled consent
    if (touchpoint.consentStatus.method === 'modeled') {
      confidence *= 0.7;
    }

    // Reduce confidence for implicit consent
    if (touchpoint.consentStatus.method === 'implicit') {
      confidence *= 0.85;
    }

    // Reduce confidence if analytics consent was denied
    if (!touchpoint.consentStatus.analytics) {
      confidence *= 0.5;
    }

    return confidence;
  }

  private async getDataDrivenWeights(channels: string[]): Promise<Map<string, number>> {
    // In production, this would call your ML model
    // Here we use historical performance data
    const weights = new Map<string, number>();

    for (const channel of channels) {
      const metrics = this.channelPerformanceHistory.get(channel);
      if (metrics) {
        // Weight based on conversion rate and average order value
        weights.set(channel, metrics.conversionRate * metrics.avgOrderValue);
      } else {
        weights.set(channel, 1);
      }
    }

    return weights;
  }

  private calculateDataCompleteness(known: number, estimated: number): number {
    return known / (known + estimated);
  }

  private async resolveJourneyId(data: TouchpointData | ConversionData): Promise<string> {
    // Implement your identity resolution logic
    // This might use first-party IDs, device fingerprinting, or probabilistic matching
    return data.sessionId;
  }

  private updateConsentRateStats(channel: string, consented: boolean): void {
    // Exponential moving average of consent rates
    const currentRate = this.consentRatesByChannel.get(channel) || 0.5;
    const alpha = 0.1;  // Smoothing factor
    const newRate = alpha * (consented ? 1 : 0) + (1 - alpha) * currentRate;
    this.consentRatesByChannel.set(channel, newRate);
  }

  private getPotentialMissingChannels(known: Set<string>): string[] {
    const allChannels = ['organic_search', 'paid_search', 'social', 'email', 'display', 'referral', 'direct'];
    return allChannels.filter(c => !known.has(c));
  }

  private async loadHistoricalConsentRates(): Promise<Record<string, number>> {
    // Load from database in production
    return {
      organic_search: 0.65,
      paid_search: 0.58,
      social: 0.45,
      email: 0.72,
      display: 0.35,
      referral: 0.60,
      direct: 0.70
    };
  }
}

interface AttributionConfig {
  model: 'data-driven' | 'position-based' | 'time-decay' | 'linear';
  historicalAvgTouchpoints?: number;
  timeDecayHalfLife?: number;
}

interface ChannelMetrics {
  touchpointFrequency: number;
  conversionRate: number;
  avgOrderValue: number;
}

Google Consent Mode v2 Integration

Google's Consent Mode v2 is essential for maintaining measurement in a consent-first world. It uses machine learning to model conversions for users who didn't consent to tracking.

Implementing Consent Mode v2

interface GoogleConsentState {
  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: GoogleConsentState;
  waitForUpdate: number;  // milliseconds
  urlPassthrough: boolean;
  adsDataRedaction: boolean;
  regions?: Array<{
    region: string[];
    state: Partial<GoogleConsentState>;
  }>;
}

class GoogleConsentModeManager {
  private currentState: GoogleConsentState;
  private initialized: boolean = false;

  constructor(private config: ConsentModeConfig) {
    this.currentState = config.defaultState;
  }

  initialize(): void {
    // Initialize gtag with consent defaults
    this.pushToDataLayer('consent', 'default', {
      ...this.config.defaultState,
      wait_for_update: this.config.waitForUpdate
    });

    // Set URL passthrough for better attribution without cookies
    if (this.config.urlPassthrough) {
      this.pushToDataLayer('set', 'url_passthrough', true);
    }

    // Enable ads data redaction when consent denied
    if (this.config.adsDataRedaction) {
      this.pushToDataLayer('set', 'ads_data_redaction', true);
    }

    // Apply regional defaults
    if (this.config.regions) {
      for (const regional of this.config.regions) {
        this.pushToDataLayer('consent', 'default', {
          ...regional.state,
          region: regional.region
        });
      }
    }

    this.initialized = true;
  }

  updateConsent(newState: Partial<GoogleConsentState>): void {
    if (!this.initialized) {
      console.warn('Consent Mode not initialized');
      return;
    }

    // Update internal state
    this.currentState = { ...this.currentState, ...newState };

    // Push update to Google
    this.pushToDataLayer('consent', 'update', newState);

    // Log for debugging and compliance
    this.logConsentUpdate(newState);
  }

  mapCMPConsentToGoogleConsent(cmpConsent: CMPConsentState): GoogleConsentState {
    // Map your CMP's consent categories to Google's consent types
    return {
      ad_storage: cmpConsent.advertising ? 'granted' : 'denied',
      ad_user_data: cmpConsent.advertising ? 'granted' : 'denied',
      ad_personalization: cmpConsent.advertising ? 'granted' : 'denied',
      analytics_storage: cmpConsent.analytics ? 'granted' : 'denied',
      functionality_storage: cmpConsent.functional ? 'granted' : 'denied',
      personalization_storage: cmpConsent.personalization ? 'granted' : 'denied',
      security_storage: 'granted'  // Usually always granted for security
    };
  }

  handleCMPConsentChange(cmpConsent: CMPConsentState): void {
    const googleConsent = this.mapCMPConsentToGoogleConsent(cmpConsent);
    this.updateConsent(googleConsent);
  }

  getConsentModeStatus(): ConsentModeStatus {
    return {
      initialized: this.initialized,
      currentState: this.currentState,
      isAdvancedMode: this.isAdvancedMode(),
      conversionModelingEligible: this.isConversionModelingEligible()
    };
  }

  private isAdvancedMode(): boolean {
    // Advanced mode: sends cookieless pings even when consent denied
    // This enables conversion modeling
    return this.config.defaultState.analytics_storage === 'denied' ||
           this.config.defaultState.ad_storage === 'denied';
  }

  private isConversionModelingEligible(): boolean {
    // Google requires minimum thresholds for conversion modeling:
    // - At least 1,000 daily users with ad_storage=granted
    // - At least 7 days of data
    // This is a placeholder - actual check requires Google's API
    return true;
  }

  private pushToDataLayer(...args: any[]): void {
    // @ts-ignore
    window.dataLayer = window.dataLayer || [];
    // @ts-ignore
    window.dataLayer.push(arguments);
  }

  private logConsentUpdate(state: Partial<GoogleConsentState>): void {
    console.log('[Consent Mode] Updated:', state, 'at', new Date().toISOString());
  }
}

interface CMPConsentState {
  necessary: boolean;
  functional: boolean;
  analytics: boolean;
  advertising: boolean;
  personalization: boolean;
}

interface ConsentModeStatus {
  initialized: boolean;
  currentState: GoogleConsentState;
  isAdvancedMode: boolean;
  conversionModelingEligible: boolean;
}

// Example implementation with your CMP
class ConsentModeIntegration {
  private googleConsentMode: GoogleConsentModeManager;

  constructor() {
    // Initialize with privacy-first defaults
    this.googleConsentMode = new GoogleConsentModeManager({
      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,
      adsDataRedaction: true,
      regions: [
        // California - stricter defaults
        {
          region: ['US-CA'],
          state: { ad_storage: 'denied', analytics_storage: 'denied' }
        },
        // Rest of US - can be more permissive
        {
          region: ['US'],
          state: { analytics_storage: 'granted' }
        }
      ]
    });
  }

  setup(): void {
    // Initialize before any Google tags load
    this.googleConsentMode.initialize();

    // Listen for CMP consent changes
    document.addEventListener('cmp:consent-update', (event: CustomEvent) => {
      this.googleConsentMode.handleCMPConsentChange(event.detail);
    });
  }
}

Media Mix Modeling for Privacy-First Attribution

Media Mix Modeling (MMM) provides aggregate-level attribution that doesn't rely on user-level tracking—perfect for a consent-constrained world.

Building a Simple MMM

import * as tf from '@tensorflow/tfjs';

interface MMMDataPoint {
  date: Date;
  sales: number;
  channels: {
    tvSpend: number;
    radioSpend: number;
    digitalSpend: number;
    printSpend: number;
    socialSpend: number;
    searchSpend: number;
    emailVolume: number;
  };
  externalFactors: {
    seasonalityIndex: number;
    competitorActivity: number;
    economicIndex: number;
    weatherIndex?: number;
  };
}

interface MMMResult {
  channelContributions: Map<string, number>;
  roi: Map<string, number>;
  saturationCurves: Map<string, SaturationCurve>;
  baselineRevenue: number;
  modelFit: {
    rSquared: number;
    mape: number;
    predictions: number[];
  };
}

interface SaturationCurve {
  halfSaturation: number;  // Spend level at 50% effectiveness
  maxEffect: number;       // Maximum incremental effect
  currentEfficiency: number;  // Current marginal efficiency
}

class MediaMixModel {
  private model: tf.LayersModel | null = null;
  private channelNames: string[] = [];
  private trainedCoefficients: Map<string, number> = new Map();

  async buildAndTrainModel(data: MMMDataPoint[]): Promise<MMMResult> {
    // Prepare features
    const { features, labels, channelNames } = this.prepareData(data);
    this.channelNames = channelNames;

    // Apply adstock transformation (carryover effect)
    const adstockedFeatures = this.applyAdstock(features, channelNames);

    // Apply saturation transformation
    const saturatedFeatures = this.applySaturation(adstockedFeatures, channelNames);

    // Build regression model
    this.model = this.buildModel(saturatedFeatures[0].length);

    // Train
    const featureTensor = tf.tensor2d(saturatedFeatures);
    const labelTensor = tf.tensor2d(labels.map(l => [l]));

    await this.model.fit(featureTensor, labelTensor, {
      epochs: 500,
      batchSize: 32,
      validationSplit: 0.2,
      callbacks: {
        onEpochEnd: (epoch, logs) => {
          if (epoch % 100 === 0) {
            console.log(`Epoch ${epoch}: loss = ${logs?.loss?.toFixed(4)}`);
          }
        }
      }
    });

    // Extract results
    return this.extractResults(data, saturatedFeatures, labels);
  }

  private prepareData(data: MMMDataPoint[]): {
    features: number[][];
    labels: number[];
    channelNames: string[];
  } {
    const channelNames = Object.keys(data[0].channels);
    const externalNames = Object.keys(data[0].externalFactors);

    const features = data.map(d => [
      ...Object.values(d.channels),
      ...Object.values(d.externalFactors)
    ]);

    const labels = data.map(d => d.sales);

    return { features, labels, channelNames: [...channelNames, ...externalNames] };
  }

  private applyAdstock(
    features: number[][],
    channelNames: string[]
  ): number[][] {
    // Adstock models the carryover effect of advertising
    // Ad effects don't disappear immediately - they decay over time

    const decayRates: Record<string, number> = {
      tvSpend: 0.7,      // TV has long carryover
      radioSpend: 0.5,
      digitalSpend: 0.3,  // Digital decays faster
      printSpend: 0.6,
      socialSpend: 0.3,
      searchSpend: 0.2,   // Search intent is immediate
      emailVolume: 0.4,
      seasonalityIndex: 0,
      competitorActivity: 0,
      economicIndex: 0,
      weatherIndex: 0
    };

    const adstocked = features.map((row, t) => {
      return row.map((value, i) => {
        const channelName = channelNames[i];
        const decay = decayRates[channelName] || 0;

        if (decay === 0 || t === 0) return value;

        // Geometric adstock: current + decay * previous_adstock
        let adstockValue = value;
        for (let lag = 1; lag <= Math.min(t, 8); lag++) {
          adstockValue += Math.pow(decay, lag) * features[t - lag][i];
        }
        return adstockValue;
      });
    });

    return adstocked;
  }

  private applySaturation(
    features: number[][],
    channelNames: string[]
  ): number[][] {
    // Saturation models diminishing returns
    // More spend → less incremental effect

    // Hill function parameters (would be learned in production)
    const halfSaturation: Record<string, number> = {
      tvSpend: 500000,
      radioSpend: 100000,
      digitalSpend: 200000,
      printSpend: 150000,
      socialSpend: 100000,
      searchSpend: 150000,
      emailVolume: 50000
    };

    return features.map(row => {
      return row.map((value, i) => {
        const channelName = channelNames[i];
        const k = halfSaturation[channelName];

        if (!k) return value;  // No saturation for non-channel features

        // Hill function: x / (x + k)
        return value / (value + k);
      });
    });
  }

  private buildModel(inputDim: number): tf.LayersModel {
    const model = tf.sequential();

    // Simple linear regression with regularization
    model.add(tf.layers.dense({
      units: 1,
      inputShape: [inputDim],
      kernelRegularizer: tf.regularizers.l2({ l2: 0.01 }),
      useBias: true
    }));

    model.compile({
      optimizer: tf.train.adam(0.01),
      loss: 'meanSquaredError',
      metrics: ['mae']
    });

    return model;
  }

  private async extractResults(
    originalData: MMMDataPoint[],
    features: number[][],
    actuals: number[]
  ): Promise<MMMResult> {
    if (!this.model) throw new Error('Model not trained');

    // Get model coefficients
    const weights = this.model.getWeights()[0];
    const bias = this.model.getWeights()[1];
    const coefficients = await weights.array() as number[][];
    const baselineValue = (await bias.array() as number[])[0];

    // Calculate channel contributions
    const contributions = new Map<string, number>();
    const roi = new Map<string, number>();

    for (let i = 0; i < this.channelNames.length; i++) {
      const channelName = this.channelNames[i];
      const coefficient = coefficients[i][0];

      // Sum contribution across all time periods
      const totalContribution = features.reduce((sum, row) => {
        return sum + row[i] * coefficient;
      }, 0);

      contributions.set(channelName, totalContribution);

      // Calculate ROI (for spend channels)
      if (channelName.includes('Spend')) {
        const totalSpend = originalData.reduce((sum, d) => {
          return sum + (d.channels as any)[channelName];
        }, 0);
        roi.set(channelName, totalContribution / totalSpend);
      }
    }

    // Calculate predictions and model fit
    const predictions = await this.predict(features);
    const rSquared = this.calculateRSquared(actuals, predictions);
    const mape = this.calculateMAPE(actuals, predictions);

    // Calculate saturation curves
    const saturationCurves = this.calculateSaturationCurves(originalData);

    return {
      channelContributions: contributions,
      roi,
      saturationCurves,
      baselineRevenue: baselineValue,
      modelFit: {
        rSquared,
        mape,
        predictions
      }
    };
  }

  private async predict(features: number[][]): Promise<number[]> {
    if (!this.model) throw new Error('Model not trained');

    const featureTensor = tf.tensor2d(features);
    const predictions = this.model.predict(featureTensor) as tf.Tensor;
    const predArray = await predictions.array() as number[][];

    return predArray.map(p => p[0]);
  }

  private calculateRSquared(actuals: number[], predictions: number[]): number {
    const mean = actuals.reduce((a, b) => a + b, 0) / actuals.length;
    const ssTotal = actuals.reduce((sum, y) => sum + Math.pow(y - mean, 2), 0);
    const ssResidual = actuals.reduce((sum, y, i) =>
      sum + Math.pow(y - predictions[i], 2), 0
    );

    return 1 - (ssResidual / ssTotal);
  }

  private calculateMAPE(actuals: number[], predictions: number[]): number {
    const ape = actuals.map((actual, i) =>
      Math.abs((actual - predictions[i]) / actual)
    );
    return (ape.reduce((a, b) => a + b, 0) / ape.length) * 100;
  }

  private calculateSaturationCurves(data: MMMDataPoint[]): Map<string, SaturationCurve> {
    const curves = new Map<string, SaturationCurve>();

    // Simplified saturation curve estimation
    const channels = ['tvSpend', 'digitalSpend', 'socialSpend', 'searchSpend'];

    for (const channel of channels) {
      const spends = data.map(d => (d.channels as any)[channel]);
      const maxSpend = Math.max(...spends);
      const avgSpend = spends.reduce((a, b) => a + b, 0) / spends.length;

      curves.set(channel, {
        halfSaturation: maxSpend * 0.3,  // Simplified
        maxEffect: maxSpend * 2,
        currentEfficiency: avgSpend < maxSpend * 0.3 ? 0.8 : 0.4
      });
    }

    return curves;
  }

  generateOptimizationRecommendations(
    result: MMMResult,
    budget: number
  ): BudgetOptimization {
    const recommendations: BudgetOptimization = {
      currentAllocation: new Map(),
      recommendedAllocation: new Map(),
      expectedLift: 0,
      insights: []
    };

    // Find underinvested and overinvested channels
    for (const [channel, roiValue] of result.roi.entries()) {
      const curve = result.saturationCurves.get(channel);

      if (curve && curve.currentEfficiency > 0.6) {
        recommendations.insights.push({
          channel,
          type: 'underinvested',
          message: `${channel} shows high marginal efficiency (${(curve.currentEfficiency * 100).toFixed(0)}%). Consider increasing spend.`,
          recommendedChange: '+20%'
        });
      } else if (curve && curve.currentEfficiency < 0.3) {
        recommendations.insights.push({
          channel,
          type: 'overinvested',
          message: `${channel} shows diminishing returns (${(curve.currentEfficiency * 100).toFixed(0)}% efficiency). Consider reallocating budget.`,
          recommendedChange: '-15%'
        });
      }
    }

    return recommendations;
  }
}

interface BudgetOptimization {
  currentAllocation: Map<string, number>;
  recommendedAllocation: Map<string, number>;
  expectedLift: number;
  insights: Array<{
    channel: string;
    type: 'underinvested' | 'overinvested' | 'optimal';
    message: string;
    recommendedChange: string;
  }>;
}

Incrementality Testing Framework

Incrementality testing measures the true causal impact of your marketing by comparing test and control groups—no cookies required.

interface IncrementalityTestConfig {
  testName: string;
  channel: string;
  testDuration: number;  // days
  controlGroupSize: number;  // percentage
  minimumDetectableEffect: number;  // percentage lift
  confidenceLevel: number;  // e.g., 0.95
}

interface IncrementalityTestResult {
  testName: string;
  status: 'running' | 'complete' | 'inconclusive';
  metrics: {
    testGroupConversions: number;
    controlGroupConversions: number;
    testGroupSize: number;
    controlGroupSize: number;
    incrementalLift: number;
    incrementalLiftPercentage: number;
    statisticalSignificance: number;
    confidenceInterval: [number, number];
  };
  recommendation: string;
}

class IncrementalityTestingEngine {
  private activeTests: Map<string, IncrementalityTest> = new Map();

  async createGeoTest(config: IncrementalityTestConfig): Promise<IncrementalityTest> {
    // Geo-based incrementality test
    // Split by geographic regions, not individual users

    const test = new GeoIncrementalityTest({
      ...config,
      testRegions: await this.selectTestRegions(config.controlGroupSize),
      controlRegions: await this.selectControlRegions(config.controlGroupSize)
    });

    this.activeTests.set(config.testName, test);
    return test;
  }

  async createTimeSplitTest(config: IncrementalityTestConfig): Promise<IncrementalityTest> {
    // Time-based incrementality test
    // Compare same region with/without marketing exposure

    const test = new TimeSplitIncrementalityTest({
      ...config,
      baselinePeriod: this.calculateBaselinePeriod(config.testDuration),
      testPeriod: this.calculateTestPeriod(config.testDuration)
    });

    this.activeTests.set(config.testName, test);
    return test;
  }

  async createConversionLiftTest(config: IncrementalityTestConfig): Promise<IncrementalityTest> {
    // Platform-native conversion lift test
    // Works with Meta, Google, TikTok conversion lift APIs

    const test = new PlatformConversionLiftTest(config);
    this.activeTests.set(config.testName, test);
    return test;
  }

  private async selectTestRegions(targetPercentage: number): Promise<string[]> {
    // Select statistically similar regions for test group
    // Based on historical conversion patterns, demographics, seasonality
    return ['region_1', 'region_3', 'region_5'];  // Placeholder
  }

  private async selectControlRegions(targetPercentage: number): Promise<string[]> {
    // Select matched control regions
    return ['region_2', 'region_4', 'region_6'];  // Placeholder
  }

  private calculateBaselinePeriod(testDays: number): { start: Date; end: Date } {
    const end = new Date();
    end.setDate(end.getDate() - testDays);
    const start = new Date(end);
    start.setDate(start.getDate() - testDays);
    return { start, end };
  }

  private calculateTestPeriod(testDays: number): { start: Date; end: Date } {
    const start = new Date();
    const end = new Date();
    end.setDate(end.getDate() + testDays);
    return { start, end };
  }
}

abstract class IncrementalityTest {
  protected results: IncrementalityTestResult | null = null;

  abstract start(): Promise<void>;
  abstract stop(): Promise<IncrementalityTestResult>;
  abstract getIntermediateResults(): IncrementalityTestResult;

  protected calculateStatisticalSignificance(
    testConversions: number,
    testSize: number,
    controlConversions: number,
    controlSize: number
  ): number {
    // Two-proportion z-test
    const p1 = testConversions / testSize;
    const p2 = controlConversions / controlSize;
    const pooledP = (testConversions + controlConversions) / (testSize + controlSize);

    const standardError = Math.sqrt(
      pooledP * (1 - pooledP) * (1/testSize + 1/controlSize)
    );

    const zScore = (p1 - p2) / standardError;

    // Convert to p-value (two-tailed)
    const pValue = 2 * (1 - this.normalCDF(Math.abs(zScore)));

    return 1 - pValue;  // Return significance level
  }

  protected calculateConfidenceInterval(
    testConversions: number,
    testSize: number,
    controlConversions: number,
    controlSize: number,
    confidenceLevel: number
  ): [number, number] {
    const p1 = testConversions / testSize;
    const p2 = controlConversions / controlSize;
    const diff = p1 - p2;

    const standardError = Math.sqrt(
      (p1 * (1 - p1) / testSize) + (p2 * (1 - p2) / controlSize)
    );

    const zValue = this.getZValue(confidenceLevel);

    return [
      diff - zValue * standardError,
      diff + zValue * standardError
    ];
  }

  protected calculateMinimumSampleSize(
    baselineConversionRate: number,
    minimumDetectableEffect: number,
    confidenceLevel: number,
    power: number = 0.8
  ): number {
    const alpha = 1 - confidenceLevel;
    const beta = 1 - power;

    const zAlpha = this.getZValue(1 - alpha/2);
    const zBeta = this.getZValue(power);

    const p1 = baselineConversionRate;
    const p2 = baselineConversionRate * (1 + minimumDetectableEffect);
    const pooledP = (p1 + p2) / 2;

    const n = Math.pow(
      zAlpha * Math.sqrt(2 * pooledP * (1 - pooledP)) +
      zBeta * Math.sqrt(p1 * (1 - p1) + p2 * (1 - p2)),
      2
    ) / Math.pow(p2 - p1, 2);

    return Math.ceil(n);
  }

  private normalCDF(x: number): number {
    const a1 =  0.254829592;
    const a2 = -0.284496736;
    const a3 =  1.421413741;
    const a4 = -1.453152027;
    const a5 =  1.061405429;
    const p  =  0.3275911;

    const sign = x < 0 ? -1 : 1;
    x = Math.abs(x) / Math.sqrt(2);

    const t = 1.0 / (1.0 + p * x);
    const y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);

    return 0.5 * (1.0 + sign * y);
  }

  private getZValue(confidenceLevel: number): number {
    // Common z-values
    const zTable: Record<number, number> = {
      0.90: 1.645,
      0.95: 1.96,
      0.99: 2.576
    };
    return zTable[confidenceLevel] || 1.96;
  }
}

class GeoIncrementalityTest extends IncrementalityTest {
  constructor(private config: IncrementalityTestConfig & {
    testRegions: string[];
    controlRegions: string[];
  }) {
    super();
  }

  async start(): Promise<void> {
    // Activate marketing in test regions only
    console.log(`Starting geo test: ${this.config.testName}`);
    console.log(`Test regions: ${this.config.testRegions.join(', ')}`);
    console.log(`Control regions: ${this.config.controlRegions.join(', ')}`);
  }

  async stop(): Promise<IncrementalityTestResult> {
    // Collect final results
    return this.getIntermediateResults();
  }

  getIntermediateResults(): IncrementalityTestResult {
    // In production, query your analytics system
    const mockResults = {
      testGroupConversions: 1250,
      controlGroupConversions: 1050,
      testGroupSize: 50000,
      controlGroupSize: 50000
    };

    const incrementalLift = mockResults.testGroupConversions - mockResults.controlGroupConversions;
    const incrementalLiftPercentage = (incrementalLift / mockResults.controlGroupConversions) * 100;

    const significance = this.calculateStatisticalSignificance(
      mockResults.testGroupConversions,
      mockResults.testGroupSize,
      mockResults.controlGroupConversions,
      mockResults.controlGroupSize
    );

    const confidenceInterval = this.calculateConfidenceInterval(
      mockResults.testGroupConversions,
      mockResults.testGroupSize,
      mockResults.controlGroupConversions,
      mockResults.controlGroupSize,
      this.config.confidenceLevel
    );

    return {
      testName: this.config.testName,
      status: significance >= this.config.confidenceLevel ? 'complete' : 'running',
      metrics: {
        ...mockResults,
        incrementalLift,
        incrementalLiftPercentage,
        statisticalSignificance: significance,
        confidenceInterval
      },
      recommendation: significance >= this.config.confidenceLevel
        ? `${this.config.channel} shows ${incrementalLiftPercentage.toFixed(1)}% incremental lift with ${(significance * 100).toFixed(0)}% confidence.`
        : 'Continue test - not yet statistically significant.'
    };
  }
}

class TimeSplitIncrementalityTest extends IncrementalityTest {
  constructor(private config: IncrementalityTestConfig & {
    baselinePeriod: { start: Date; end: Date };
    testPeriod: { start: Date; end: Date };
  }) {
    super();
  }

  async start(): Promise<void> {
    console.log(`Starting time-split test: ${this.config.testName}`);
  }

  async stop(): Promise<IncrementalityTestResult> {
    return this.getIntermediateResults();
  }

  getIntermediateResults(): IncrementalityTestResult {
    // Compare baseline period (no marketing) to test period (with marketing)
    // Control for seasonality and trends
    return {
      testName: this.config.testName,
      status: 'running',
      metrics: {
        testGroupConversions: 0,
        controlGroupConversions: 0,
        testGroupSize: 0,
        controlGroupSize: 0,
        incrementalLift: 0,
        incrementalLiftPercentage: 0,
        statisticalSignificance: 0,
        confidenceInterval: [0, 0]
      },
      recommendation: 'Test in progress'
    };
  }
}

class PlatformConversionLiftTest extends IncrementalityTest {
  constructor(private config: IncrementalityTestConfig) {
    super();
  }

  async start(): Promise<void> {
    // Integrate with platform APIs (Meta, Google, etc.)
    console.log(`Starting platform conversion lift test: ${this.config.testName}`);
  }

  async stop(): Promise<IncrementalityTestResult> {
    return this.getIntermediateResults();
  }

  getIntermediateResults(): IncrementalityTestResult {
    return {
      testName: this.config.testName,
      status: 'running',
      metrics: {
        testGroupConversions: 0,
        controlGroupConversions: 0,
        testGroupSize: 0,
        controlGroupSize: 0,
        incrementalLift: 0,
        incrementalLiftPercentage: 0,
        statisticalSignificance: 0,
        confidenceInterval: [0, 0]
      },
      recommendation: 'Test in progress'
    };
  }
}

Privacy-Preserving Measurement Techniques

Beyond modeling and aggregation, several advanced techniques enable measurement while preserving individual privacy.

Differential Privacy Implementation

interface DifferentialPrivacyConfig {
  epsilon: number;  // Privacy budget (lower = more private)
  delta: number;    // Probability of privacy breach
  sensitivity: number;  // Maximum influence of single record
}

class DifferentialPrivacyEngine {
  constructor(private config: DifferentialPrivacyConfig) {}

  addLaplaceNoise(trueValue: number): number {
    // Laplace mechanism for numeric queries
    const scale = this.config.sensitivity / this.config.epsilon;
    const noise = this.sampleLaplace(scale);
    return trueValue + noise;
  }

  addGaussianNoise(trueValue: number): number {
    // Gaussian mechanism (for (ε,δ)-differential privacy)
    const sigma = this.config.sensitivity * Math.sqrt(2 * Math.log(1.25 / this.config.delta)) / this.config.epsilon;
    const noise = this.sampleGaussian(0, sigma);
    return trueValue + noise;
  }

  randomizedResponse(trueBit: boolean, probability: number = 0.75): boolean {
    // Randomized response for binary data
    // With probability p, return true answer; otherwise, flip a coin
    if (Math.random() < probability) {
      return trueBit;
    }
    return Math.random() < 0.5;
  }

  aggregateWithPrivacy<T extends Record<string, number>>(
    records: T[],
    aggregations: Array<keyof T>
  ): Record<keyof T, number> {
    const result: Record<string, number> = {};

    for (const field of aggregations) {
      const trueSum = records.reduce((sum, r) => sum + (r[field] as number), 0);
      result[field as string] = this.addLaplaceNoise(trueSum);
    }

    return result as Record<keyof T, number>;
  }

  private sampleLaplace(scale: number): number {
    const u = Math.random() - 0.5;
    return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
  }

  private sampleGaussian(mean: number, stdDev: number): number {
    // Box-Muller transform
    const u1 = Math.random();
    const u2 = Math.random();
    const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
    return mean + stdDev * z;
  }
}

// Example: Privacy-preserving conversion reporting
class PrivateConversionReporter {
  private dpEngine: DifferentialPrivacyEngine;

  constructor() {
    this.dpEngine = new DifferentialPrivacyEngine({
      epsilon: 1.0,  // Moderate privacy
      delta: 1e-5,
      sensitivity: 1  // One user can only contribute one conversion
    });
  }

  reportConversionsByChannel(
    conversions: Array<{ channel: string; value: number }>
  ): Map<string, { count: number; value: number }> {
    // Group by channel
    const byChannel = new Map<string, { count: number; totalValue: number }>();

    for (const conv of conversions) {
      const existing = byChannel.get(conv.channel) || { count: 0, totalValue: 0 };
      byChannel.set(conv.channel, {
        count: existing.count + 1,
        totalValue: existing.totalValue + conv.value
      });
    }

    // Add noise to each channel's metrics
    const privateResults = new Map<string, { count: number; value: number }>();

    for (const [channel, metrics] of byChannel.entries()) {
      privateResults.set(channel, {
        count: Math.max(0, Math.round(this.dpEngine.addLaplaceNoise(metrics.count))),
        value: Math.max(0, this.dpEngine.addLaplaceNoise(metrics.totalValue))
      });
    }

    return privateResults;
  }
}

K-Anonymity for Attribution Reports

interface AnonymityConfig {
  kThreshold: number;  // Minimum group size
  suppressionValue: string;  // Value for suppressed cells
}

class KAnonymityEngine {
  constructor(private config: AnonymityConfig) {}

  anonymizeAttributionReport(
    data: AttributionReportRow[]
  ): AttributionReportRow[] {
    // Group data and check k-anonymity
    const groups = this.groupData(data);

    return data.map(row => {
      const groupKey = this.getGroupKey(row);
      const groupSize = groups.get(groupKey) || 0;

      if (groupSize < this.config.kThreshold) {
        // Suppress small groups
        return this.suppressRow(row);
      }

      return row;
    });
  }

  private groupData(data: AttributionReportRow[]): Map<string, number> {
    const groups = new Map<string, number>();

    for (const row of data) {
      const key = this.getGroupKey(row);
      groups.set(key, (groups.get(key) || 0) + 1);
    }

    return groups;
  }

  private getGroupKey(row: AttributionReportRow): string {
    // Define quasi-identifiers that could identify users
    return `${row.channel}|${row.region}|${row.deviceCategory}`;
  }

  private suppressRow(row: AttributionReportRow): AttributionReportRow {
    return {
      ...row,
      conversions: this.config.suppressionValue as any,
      revenue: this.config.suppressionValue as any
    };
  }

  generalizeForPrivacy(
    data: AttributionReportRow[],
    hierarchies: GeneralizationHierarchies
  ): AttributionReportRow[] {
    // Generalize quasi-identifiers until k-anonymity is achieved
    let currentData = [...data];
    let level = 0;

    while (!this.meetsKAnonymity(currentData) && level < 5) {
      currentData = this.applyGeneralization(currentData, hierarchies, level);
      level++;
    }

    return currentData;
  }

  private meetsKAnonymity(data: AttributionReportRow[]): boolean {
    const groups = this.groupData(data);
    return Array.from(groups.values()).every(size => size >= this.config.kThreshold);
  }

  private applyGeneralization(
    data: AttributionReportRow[],
    hierarchies: GeneralizationHierarchies,
    level: number
  ): AttributionReportRow[] {
    return data.map(row => ({
      ...row,
      region: hierarchies.region[level]?.(row.region) || row.region,
      deviceCategory: hierarchies.device[level]?.(row.deviceCategory) || row.deviceCategory
    }));
  }
}

interface AttributionReportRow {
  channel: string;
  region: string;
  deviceCategory: string;
  conversions: number;
  revenue: number;
}

interface GeneralizationHierarchies {
  region: Array<(value: string) => string>;
  device: Array<(value: string) => string>;
}

// Example hierarchies
const exampleHierarchies: GeneralizationHierarchies = {
  region: [
    // Level 0: City -> State
    (city) => cityToStateMap[city] || city,
    // Level 1: State -> Country
    (state) => stateToCountryMap[state] || state,
    // Level 2: Country -> Continent
    (country) => countryToContinentMap[country] || country,
    // Level 3: All -> *
    () => '*'
  ],
  device: [
    // Level 0: Specific model -> Category
    (model) => modelToCategoryMap[model] || model,
    // Level 1: Category -> *
    () => '*'
  ]
};

const cityToStateMap: Record<string, string> = {
  'San Francisco': 'California',
  'Los Angeles': 'California',
  'New York': 'New York',
  'Chicago': 'Illinois'
};

const stateToCountryMap: Record<string, string> = {
  'California': 'United States',
  'New York': 'United States',
  'Illinois': 'United States'
};

const countryToContinentMap: Record<string, string> = {
  'United States': 'North America',
  'Canada': 'North America',
  'Germany': 'Europe'
};

const modelToCategoryMap: Record<string, string> = {
  'iPhone 15': 'Mobile',
  'Pixel 8': 'Mobile',
  'iPad Pro': 'Tablet',
  'MacBook': 'Desktop'
};

Building a Consent Analytics Dashboard

Bring all these concepts together in a unified dashboard that provides actionable insights while respecting privacy.

interface ConsentAnalyticsDashboard {
  overview: OverviewMetrics;
  channelPerformance: ChannelPerformance[];
  consentImpact: ConsentImpactAnalysis;
  recommendations: Recommendation[];
}

interface OverviewMetrics {
  totalConversions: number;
  attributedConversions: number;
  modeledConversions: number;
  overallConfidence: number;
  consentRate: number;
  dataCompleteness: number;
}

interface ChannelPerformance {
  channel: string;
  attributedRevenue: number;
  modeledRevenue: number;
  confidence: number;
  consentRate: number;
  roi: number;
  incrementalLift?: number;
}

interface ConsentImpactAnalysis {
  revenueAtRisk: number;  // Revenue from low-consent channels
  measurementGap: number;  // Percentage of unmeasured activity
  biasRisk: 'low' | 'medium' | 'high';
  recommendations: string[];
}

class ConsentAnalyticsDashboardBuilder {
  constructor(
    private attributionEngine: ConsentAwareAttributionEngine,
    private mmmModel: MediaMixModel,
    private incrementalityEngine: IncrementalityTestingEngine,
    private privacyEngine: DifferentialPrivacyEngine
  ) {}

  async buildDashboard(
    dateRange: { start: Date; end: Date }
  ): Promise<ConsentAnalyticsDashboard> {
    // Gather data from all sources
    const attributionData = await this.getAttributionData(dateRange);
    const mmmResults = await this.getMMMResults(dateRange);
    const incrementalityResults = await this.getIncrementalityResults();

    // Build overview
    const overview = this.buildOverview(attributionData);

    // Build channel performance (with privacy)
    const channelPerformance = this.buildChannelPerformance(
      attributionData,
      mmmResults,
      incrementalityResults
    );

    // Analyze consent impact
    const consentImpact = this.analyzeConsentImpact(attributionData);

    // Generate recommendations
    const recommendations = this.generateRecommendations(
      overview,
      channelPerformance,
      consentImpact
    );

    return {
      overview,
      channelPerformance,
      consentImpact,
      recommendations
    };
  }

  private buildOverview(data: AttributionData): OverviewMetrics {
    const totalConversions = data.conversions.length;
    const attributedConversions = data.conversions.filter(c => c.hasAttribution).length;
    const modeledConversions = totalConversions - attributedConversions;

    const avgConfidence = data.conversions.reduce((sum, c) =>
      sum + c.confidence, 0
    ) / totalConversions;

    const consentedSessions = data.sessions.filter(s => s.consented).length;
    const consentRate = consentedSessions / data.sessions.length;

    const dataCompleteness = attributedConversions / totalConversions;

    return {
      totalConversions,
      attributedConversions,
      modeledConversions,
      overallConfidence: avgConfidence,
      consentRate,
      dataCompleteness
    };
  }

  private buildChannelPerformance(
    attribution: AttributionData,
    mmm: MMMResult,
    incrementality: Map<string, IncrementalityTestResult>
  ): ChannelPerformance[] {
    const channels = new Set<string>();

    // Collect all channels
    for (const conv of attribution.conversions) {
      for (const tp of conv.touchpoints) {
        channels.add(tp.channel);
      }
    }

    return Array.from(channels).map(channel => {
      const channelConversions = attribution.conversions.filter(c =>
        c.touchpoints.some(tp => tp.channel === channel)
      );

      const attributedRevenue = channelConversions.reduce((sum, c) => {
        const channelCredit = c.attribution.find(a => a.channel === channel);
        return sum + (channelCredit?.credit || 0);
      }, 0);

      const mmmContribution = mmm.channelContributions.get(channel) || 0;
      const mmmROI = mmm.roi.get(channel) || 0;

      const incrementalityResult = incrementality.get(channel);

      const consentedTouchpoints = attribution.touchpoints.filter(
        tp => tp.channel === channel && tp.consented
      ).length;
      const totalTouchpoints = attribution.touchpoints.filter(
        tp => tp.channel === channel
      ).length;

      return {
        channel,
        attributedRevenue,
        modeledRevenue: mmmContribution,
        confidence: this.calculateChannelConfidence(channelConversions),
        consentRate: totalTouchpoints > 0 ? consentedTouchpoints / totalTouchpoints : 0,
        roi: mmmROI,
        incrementalLift: incrementalityResult?.metrics.incrementalLiftPercentage
      };
    });
  }

  private analyzeConsentImpact(data: AttributionData): ConsentImpactAnalysis {
    // Calculate revenue at risk from low-consent channels
    const lowConsentChannels = this.identifyLowConsentChannels(data);
    const revenueAtRisk = lowConsentChannels.reduce((sum, channel) => {
      return sum + this.getChannelRevenue(data, channel);
    }, 0);

    // Calculate measurement gap
    const unconsentedSessions = data.sessions.filter(s => !s.consented).length;
    const measurementGap = unconsentedSessions / data.sessions.length;

    // Assess bias risk
    const biasRisk = this.assessBiasRisk(data);

    // Generate recommendations
    const recommendations: string[] = [];

    if (measurementGap > 0.4) {
      recommendations.push(
        'Consider implementing server-side tracking for critical conversions'
      );
    }

    if (biasRisk === 'high') {
      recommendations.push(
        'High consent bias detected. Recommend running incrementality tests to validate attribution'
      );
    }

    if (lowConsentChannels.length > 0) {
      recommendations.push(
        `${lowConsentChannels.join(', ')} have low consent rates. Consider privacy-first alternatives`
      );
    }

    return {
      revenueAtRisk,
      measurementGap: measurementGap * 100,
      biasRisk,
      recommendations
    };
  }

  private generateRecommendations(
    overview: OverviewMetrics,
    channels: ChannelPerformance[],
    impact: ConsentImpactAnalysis
  ): Recommendation[] {
    const recommendations: Recommendation[] = [];

    // Data completeness recommendations
    if (overview.dataCompleteness < 0.6) {
      recommendations.push({
        priority: 'high',
        category: 'measurement',
        title: 'Improve data completeness',
        description: 'Less than 60% of conversions have full attribution. Consider implementing Google Consent Mode v2 for conversion modeling.',
        expectedImpact: '+20-30% visibility into conversion paths'
      });
    }

    // Channel-specific recommendations
    for (const channel of channels) {
      if (channel.consentRate < 0.4 && channel.attributedRevenue > 10000) {
        recommendations.push({
          priority: 'medium',
          category: 'channel',
          title: `Validate ${channel.channel} attribution`,
          description: `${channel.channel} has low consent rate (${(channel.consentRate * 100).toFixed(0)}%) but significant attributed revenue. Run incrementality test to validate.`,
          expectedImpact: 'Accurate ROI measurement for budget allocation'
        });
      }

      if (channel.incrementalLift !== undefined && channel.incrementalLift < 5) {
        recommendations.push({
          priority: 'high',
          category: 'budget',
          title: `Review ${channel.channel} spend`,
          description: `Incrementality testing shows only ${channel.incrementalLift.toFixed(1)}% lift. Consider reallocating budget.`,
          expectedImpact: 'Improved marketing efficiency'
        });
      }
    }

    // Privacy compliance recommendations
    if (overview.consentRate < 0.5) {
      recommendations.push({
        priority: 'medium',
        category: 'privacy',
        title: 'Optimize consent experience',
        description: 'Consent rate below 50%. Review consent UX and consider testing different messaging.',
        expectedImpact: '+10-15% consent rate improvement'
      });
    }

    return recommendations;
  }

  private calculateChannelConfidence(conversions: ConversionData[]): number {
    if (conversions.length === 0) return 0;
    return conversions.reduce((sum, c) => sum + c.confidence, 0) / conversions.length;
  }

  private identifyLowConsentChannels(data: AttributionData): string[] {
    const channelConsent = new Map<string, { consented: number; total: number }>();

    for (const tp of data.touchpoints) {
      const existing = channelConsent.get(tp.channel) || { consented: 0, total: 0 };
      channelConsent.set(tp.channel, {
        consented: existing.consented + (tp.consented ? 1 : 0),
        total: existing.total + 1
      });
    }

    return Array.from(channelConsent.entries())
      .filter(([_, stats]) => stats.consented / stats.total < 0.4)
      .map(([channel]) => channel);
  }

  private getChannelRevenue(data: AttributionData, channel: string): number {
    return data.conversions.reduce((sum, c) => {
      const channelCredit = c.attribution.find(a => a.channel === channel);
      return sum + (channelCredit?.credit || 0);
    }, 0);
  }

  private assessBiasRisk(data: AttributionData): 'low' | 'medium' | 'high' {
    // Compare behavior of consenting vs non-consenting users
    const consentedSessions = data.sessions.filter(s => s.consented);
    const unconsentedSessions = data.sessions.filter(s => !s.consented);

    if (unconsentedSessions.length < 100) return 'low';  // Not enough data

    const consentedConvRate = consentedSessions.filter(s => s.converted).length / consentedSessions.length;
    const unconsentedConvRate = unconsentedSessions.filter(s => s.converted).length / unconsentedSessions.length;

    const ratioDiff = Math.abs(consentedConvRate - unconsentedConvRate) / unconsentedConvRate;

    if (ratioDiff > 0.3) return 'high';
    if (ratioDiff > 0.15) return 'medium';
    return 'low';
  }

  private async getAttributionData(dateRange: { start: Date; end: Date }): Promise<AttributionData> {
    // Fetch from your data warehouse
    return {
      conversions: [],
      touchpoints: [],
      sessions: []
    };
  }

  private async getMMMResults(dateRange: { start: Date; end: Date }): Promise<MMMResult> {
    // Get cached MMM results or run new model
    return {
      channelContributions: new Map(),
      roi: new Map(),
      saturationCurves: new Map(),
      baselineRevenue: 0,
      modelFit: { rSquared: 0, mape: 0, predictions: [] }
    };
  }

  private async getIncrementalityResults(): Promise<Map<string, IncrementalityTestResult>> {
    return new Map();
  }
}

interface AttributionData {
  conversions: Array<ConversionData & {
    hasAttribution: boolean;
    confidence: number;
    touchpoints: TouchpointData[];
    attribution: Array<{ channel: string; credit: number }>;
  }>;
  touchpoints: Array<TouchpointData & { consented: boolean }>;
  sessions: Array<{ id: string; consented: boolean; converted: boolean }>;
}

interface Recommendation {
  priority: 'high' | 'medium' | 'low';
  category: 'measurement' | 'channel' | 'budget' | 'privacy';
  title: string;
  description: string;
  expectedImpact: string;
}

FAQ

What's the difference between Consent Mode v1 and v2?

Consent Mode v2 adds two new consent signals: ad_user_data and ad_personalization. These are required for using customer match audiences and enhanced conversions in the EEA. V2 also has improved conversion modeling algorithms that can fill more data gaps when users decline consent.

How accurate is conversion modeling?

Google reports that Consent Mode v2 conversion modeling recovers approximately 70% of conversions that would otherwise be lost to consent denial. However, this varies by industry, geography, and your specific consent rates. Always validate with incrementality testing.

Can I use MMM and MTA together?

Yes! This is actually the recommended approach. MTA (Multi-Touch Attribution) provides granular, near-real-time insights for optimization, while MMM provides the "ground truth" validation and captures offline and non-digital channels. Use MTA for tactical decisions and MMM for strategic budget allocation.

What privacy budget should I use for differential privacy?

Epsilon values between 0.1 and 1.0 are considered "strong" privacy protection. For marketing analytics where individual-level data isn't sensitive, epsilon of 1-3 is common. For sensitive data like healthcare, use epsilon < 1. Lower epsilon = more privacy but more noise in results.

How do I handle Safari's ITP and Firefox's ETP?

Both browsers block third-party cookies and limit first-party cookie lifespans. Focus on: (1) server-side tracking for conversion measurement, (2) first-party data collection with explicit consent, (3) Google's Privacy Sandbox APIs when available, and (4) probabilistic attribution for Safari traffic.

Your Privacy-First Measurement Roadmap

The death of third-party cookies doesn't mean the death of marketing measurement. By implementing consent-aware attribution, leveraging conversion modeling, running incrementality tests, and adopting privacy-preserving techniques, you can maintain accurate measurement while building user trust.

Key actions to take today:

  1. Implement Google Consent Mode v2 if you use Google Analytics or Google Ads
  2. Audit your consent rates by channel and identify measurement gaps
  3. Run your first incrementality test on your highest-spend channel
  4. Build a simple MMM to validate your digital attribution
  5. Start collecting first-party data with clear value exchanges

The future of marketing measurement is privacy-first, and the companies that adapt now will have a significant competitive advantage.

G

Tým GetCookies

Napsal a udržuje tým GetCookies ve společnosti Getia AS v Oslu v Norsku. Tento článek poskytuje obecné informace o souhlasu s cookies a není právním poradenstvím.

Naskenujte svůj web zdarma

Bezplatný plán zobrazuje banner na jednom webu a nabízí tři skeny cookies měsíčně. Bez karty, bez obchodních hovorů.