Featured image of post Advertising Attribution - A Comprehensive Guide to Attribution Architecture Implementation

Advertising Attribution - A Comprehensive Guide to Attribution Architecture Implementation

Recently, I refactored the advertising attribution service developed a year ago, making numerous improvements. This article summarizes the key points and architecture.

Explanation

  • Many concepts in this article reference insights from Sensors Data documentation.

Attribution Methods

  • Self-Attribution

    • Tracked by ad platforms. Some platforms receive app-open callbacks from users and perform attribution internally.
    • Others like Huawei store attribution data locally during app installation, retrievable during first launch.
  • Impression Attribution

    • Rarely used due to massive data volumes.
  • Click Attribution (Most Common)

    • Attributes conversions to the last ad click before conversion.

Attribution Models

  • Last-Touch Model (Most Common)

    • 100% credit to the final touchpoint before conversion.
  • First-Touch Model

    • 100% credit to the initial touchpoint that introduced the user.
  • Linear Model

    • Equal credit distribution across all touchpoints.
  • Time-Decay Model

    • Higher weight given to more recent interactions.
  • Position-Based Model

    • 40% credit to first/last touchpoints, 20% distributed among others.
  • Custom Weighted Model

    • Custom weights assigned to different channels.

Matching Methods

  • Exact Matching

    • Uses device identifiers: OAID, AndroidID, IMEI, MAC.
  • Fuzzy Matching

    • Uses IP addresses and User-Agent strings.

Architecture Implementation

Core Database Schema

-- Applications Table (apps)
id, appid, name, os, attribute_cycle_days, attribute_white_list(JSON)

-- Channels Table (channels)  
id, name, template_query

-- Tracking Links Table (links)
id, app_id, channel_id, channel_name, events(JSON), exp

-- Click Logs Table (click_logs, daily partitioned)
id, appid, ad_name, oaid, imei, android_id, mac, ip, ua, exp, callback, data(JSON), attributed_at

-- Attribution Logs & Callback Logs (custom logging)

Key Workflows

1. Ad Click Tracking (Endpoint)

// Unified request structure
class AdClickRequest {
    public $oaid;
    public $imei;
    public $imei_md5;
    public $android_md5; 
    public $ad_name;
    public $callback;
}

// Simplified endpoint logic
function clickLogs($id) {
    // 1. Parse request
    $req = new AdClickRequest(/* input params */);
    
    // 2. Validate tracking link
    $link = DB::table('links')->find($id);
    if (!$link) return response('FAIL', 400);
    
    // 3. Insert click log (async via queue)
    $log = ClickLog::create([
        'oaid' => $req->oaid,
        'imei' => $req->imei_md5,
        'events' => $link->events,
        'exp' => $link->exp
    ]);
    
    // 4. Cache identifiers for attribution window
    Redis::pipeline(function($pipe) use ($link, $req) {
        foreach (['oaid','imei_md5','android_md5'] as $field) {
            $key = "attr:{$link->app_id}_{$req->$field}";
            $pipe->setex($key, $link->exp*86400, "{$log->id}:{$log->table}");
        }
    });
    
    return response('OK');
}

2. First Launch Attribution

class AppReportRequest {
    public $device_key;
    public $oaid;
    public $imei;
    public $mac;
}

function appReport($appId) {
    // 1. Parse device data
    $req = new AppReportRequest(/* input */);
    
    // 2. Check cached click logs
    $matchedLog = null;
    foreach (['oaid','imei','mac'] as $field) {
        $key = "attr:{$appId}_{$req->$field}";
        $logInfo = Redis::get($key);
        if ($logInfo) {
            [$logId, $table] = explode(':', $logInfo);
            $matchedLog = DB::table($table)->find($logId);
            break;
        }
    }
    
    if (!$matchedLog) return response('No attribution', 404);
    
    // 3. Record attribution
    Redis::setex("attr_device:{$appId}_{$req->device_key}", 604800, $matchedLog->id);
    
    // 4. Cleanup & async callbacks
    Redis::del(array_keys($matchedKeys)); 
    Queue::dispatch(new ProcessAttribution($matchedLog));
    
    return response('OK');
}

3. Event Callbacks

function eventCallback($appId, $event, $deviceKey) {
    $logId = Redis::get("attr_device:{$appId}_{$deviceKey}");
    $log = ClickLog::find($logId);
    
    // Validate event against link configuration
    if (in_array($event, json_decode($log->events))) {
        // Execute channel-specific callback template
        Http::post($log->callback, [
            'event' => $event,
            'params' => $log->data
        ]);
    }
    
    return response('Processed');
}

Debugging Tools

1. Device Attribution Checker


Input device identifiers to simulate attribution matching.

2. Real-time Click Log Monitoring


Verify if click logs are received from ad platforms.

3. Attribution Whitelisting


Bypass attribution rules for testing specific devices.

4. Event Callback Testing


Manually trigger and verify postback events.

Client Implementation

// Unified attribution handler
class AttributionTracker {
  constructor() {
    this.storageKey = 'attribution_events';
  }

  track(event) {
    const sentEvents = this._getSentEvents();
    if (sentEvents[event]) return;

    api.post('/callback', { event }).then(() => {
      sentEvents[event] = true;
      this._saveEvents(sentEvents);
    });
  }

  attributeLaunch() {
    api.post('/launch').then(() => {
      this._clearSentEvents();
    });
  }

  _getSentEvents() {
    return JSON.parse(localStorage.getItem(this.storageKey)) || {};
  }

  _saveEvents(events) {
    localStorage.setItem(this.storageKey, JSON.stringify(events));
  }

  _clearSentEvents() {
    localStorage.removeItem(this.storageKey);
  }
}

This architecture provides:

  1. Modular Design: Separates click tracking, attribution matching, and callback execution.
  2. Scalable Storage: Uses partitioned tables and Redis for high-frequency operations.
  3. Flexible Matching: Supports both exact and fuzzy matching strategies.
  4. Debuggability: Comprehensive tools for platform verification and testing.