Skip to content

Meta Ads Attribution Enhancement — Ad Creatives & Custom Conversions #377

@mustafaneguib

Description

@mustafaneguib

Meta Ads Attribution Enhancement — Ad Creatives & Custom Conversions Implementation Plan

Date: April 25, 2026
Status: Planning Phase
Priority: P1 - HIGH (Unlocks Full Attribution ROI Tracking)
Estimated Timeline: 2-3 weeks (3 phases)


📋 Executive Summary

The Problem

Current Meta Ads integration provides basic campaign metrics (Campaigns, Ad Sets, Ads, Insights) but cannot prove marketing ROI to executives because:

  1. 🔴 Missing Creative-Level Attribution: Can see "Campaign A spent $5,000" but NOT "Video Creative A drove 3.2x more conversions than Image Creative B"
  2. 🔴 Missing Conversion Event Tracking: Can see impressions/clicks but NOT actual business outcomes (purchases, sign-ups, leads)
  3. 🔴 Attribution Models Incomplete: Platform has 5 attribution models (first-touch, last-touch, linear, time-decay, U-shaped) but they can't calculate ROI without conversion data

Current vs. Desired State

CURRENT (LIMITED):
┌──────────────────────┐
│ Meta Ads Integration │
└──────────────────────┘
         │
         ├─ Campaigns ✅
         ├─ Ad Sets ✅
         ├─ Ads ✅
         └─ Insights (impressions, clicks, spend) ✅
         
    ❌ NO creative-level analysis
    ❌ NO conversion event tracking
    ❌ Attribution models can't calculate ROI
    ❌ CMOs can't answer "Which creative drove revenue?"
    
DESIRED (FULL ATTRIBUTION):
┌──────────────────────┐
│ Meta Ads Integration │
└──────────────────────┘
         │
         ├─ Campaigns ✅
         ├─ Ad Sets ✅
         ├─ Ads ✅
         ├─ Insights ✅
         ├─ Ad Creatives ⭐ NEW (images, videos, copy, CTAs)
         └─ Custom Conversions ⭐ NEW (purchases, sign-ups, add-to-cart)
         
    ✅ Creative-level ROI: "Video A → $12K revenue at 4.2 ROAS"
    ✅ Conversion tracking: "Campaign X drove 47 purchases"
    ✅ Full attribution: First-touch/last-touch with conversion values
    ✅ Executive reporting: Prove marketing ROI with hard numbers

Business Impact

  • 🎯 Aligns with Core Value Prop: "Finally Prove Marketing ROI to Your CEO" (landing page promise)
  • 📊 Unlocks Attribution Engine: Enables accurate ROI calculation for all 5 attribution models
  • 💰 Revenue Impact: Marketers can optimize creative performance → higher ROAS → customer retention
  • 🏆 Competitive Advantage: Most BI tools (Tableau, Power BI) don't have native creative-level Meta attribution

✅ What's Already Implemented

Backend Infrastructure ✅

  • MetaAdsService: HTTP client for Meta Marketing API v22.0 with retry logic
  • MetaAdsDriver: Syncs 4 tables (campaigns, ad sets, ads, insights) to dra_meta_ads schema
  • MetaOAuthService: OAuth 2.0 flow with token refresh
  • SyncHistoryService: Tracks sync status, duration, records synced/failed
  • TableMetadataService: Physical table naming (ds{id}_{hash8}) and metadata storage
  • Rate Limiting: expensiveOperationsLimiter (30 req/15min) on sync endpoints

Frontend Integration ✅

  • meta-ads.vue: OAuth connection page with account selection + report type selector
  • useMetaAds composable: API call helpers for sync/status
  • Data Sources Pages: Already show Meta Ads in list/detail views with sync controls

Database Schema ✅

  • Schema: dra_meta_ads
  • Existing Tables: campaigns, adsets, ads, insights
  • Metadata Tracking: All tables registered in dra_table_metadata

API Permissions (Current) ⚠️

'ads_read',              // Read ad account data
'business_management',   // Access Business Manager accounts

❌ Critical Gaps (What's Missing)

Gap 1: Ad Creatives Table

  • ❌ No storage for creative assets (image URLs, video IDs, ad copy, headlines, CTAs)
  • ❌ Can't analyze creative performance separately from ad performance
  • ❌ Missing link between ads and their creative specifications

Gap 2: Custom Conversions Table

  • ❌ No conversion event tracking (purchases, sign-ups, add-to-cart events)
  • ❌ Attribution models can't calculate actual ROI without conversion values
  • ❌ Can only measure "clicks" not business outcomes

Gap 3: API Permissions

  • ❌ Need ads_management permission for both tables (requires App Review)

Gap 4: Frontend Configuration

  • ❌ Connect page doesn't offer "Ad Creatives" and "Custom Conversions" as selectable report types
  • ❌ No UI to display creative-level insights

🎯 Proposed Solution: 3-Phase Implementation

Phase Overview

PHASE 1: Backend Infrastructure (Week 1)
       ↓
    - Update OAuth scopes (+ads_management)
    - Add TypeScript interfaces
    - Implement MetaAdsService methods (getCreatives, getCustomConversions)
    - Add MetaAdsDriver sync methods
    - Create database tables
    - Register new report types
       ↓
PHASE 2: Frontend Integration (Week 2)
       ↓
    - Update connect page with new report type options
    - Add creative/conversion display to data source detail pages
    - Update sync configuration storage
       ↓
PHASE 3: Meta App Review & Testing (Week 3)
       ↓
    - Submit ads_management permission for review
    - Test with real ad accounts
    - Validate attribution calculations
    - Documentation updates

📦 Phase 1: Backend Infrastructure (Week 1)

1.1 Update OAuth Permissions

File: backend/src/services/MetaOAuthService.ts

Change:

// BEFORE
public static getMetaAdsScopes(): string[] {
    return [
        'ads_read',
        'business_management',
    ];
}

// AFTER
public static getMetaAdsScopes(): string[] {
    return [
        'ads_read',              // Read ad account data (campaigns, ads, insights)
        'business_management',   // Access Business Manager accounts
        'ads_management',        // Required for custom conversions, creatives (read-only)
    ];
}

Impact: All new OAuth connections will request the expanded permission set. Existing users will need to re-authenticate.


1.2 Add TypeScript Interfaces

File: backend/src/types/IMetaAds.ts

Add:

/**
 * Meta Ad Creative
 * Represents the creative content (images, videos, copy) used in ads
 */
export interface IMetaAdCreative {
    id: string;                          // Creative ID (e.g., "23849876543210")
    name?: string;                       // Creative name
    title?: string;                      // Ad title/headline
    body?: string;                       // Ad body text
    call_to_action_type?: string;        // CTA button type (LEARN_MORE, SHOP_NOW, etc.)
    link_url?: string;                   // Destination URL
    image_url?: string;                  // Thumbnail image URL
    video_id?: string;                   // Video asset ID
    asset_feed_spec?: any;               // Dynamic product ads feed spec (JSONB)
    object_story_spec?: any;             // Post/story spec (JSONB)
    status?: string;                     // ACTIVE, PAUSED, DELETED, etc.
    effective_object_story_id?: string;  // Published post ID
}

/**
 * Meta Custom Conversion
 * Represents custom conversion events tracked via Meta Pixel
 */
export interface IMetaCustomConversion {
    id: string;                          // Custom conversion ID
    name: string;                        // Conversion name (e.g., "Purchase", "Add to Cart")
    rule: string;                        // URL match rule (JSONB in DB)
    event_source_type?: string;          // PIXEL, APP, OFFLINE
    pixel_id?: string;                   // Associated Meta Pixel ID
    custom_event_type?: string;          // PURCHASE, LEAD, COMPLETE_REGISTRATION, etc.
    default_conversion_value?: number;   // Default value for conversions
    description?: string;                // Admin description
    creation_time?: string;              // ISO timestamp
    last_fired_time?: string;            // Last time conversion fired
    is_archived?: boolean;               // Archive status
}

/**
 * Meta Creative Insights
 * Performance metrics for a specific creative (optional enhancement)
 */
export interface IMetaCreativeInsights {
    creative_id: string;
    date_start: string;
    date_stop: string;
    impressions?: number;
    clicks?: number;
    spend?: number;
    conversions?: number;
    conversion_value?: number;
    ctr?: number;                        // Click-through rate
    cpc?: number;                        // Cost per click
    cpm?: number;                        // Cost per thousand impressions
    roas?: number;                       // Return on ad spend
}

1.3 Implement MetaAdsService Methods

File: backend/src/services/MetaAdsService.ts

Add (after existing methods, around line 400):

/**
 * Get ad creatives for an ad account
 */
public async getCreatives(
    adAccountId: string,
    accessToken: string,
    params?: { limit?: number }
): Promise<IMetaAdCreative[]> {
    const fields = [
        'id',
        'name',
        'title',
        'body',
        'call_to_action_type',
        'link_url',
        'image_url',
        'video_id',
        'asset_feed_spec',
        'object_story_spec',
        'status',
        'effective_object_story_id',
    ].join(',');
    
    let url = `${MetaAdsService.BASE_URL}/${MetaAdsService.API_VERSION}/${adAccountId}/adcreatives?fields=${fields}`;
    
    if (params?.limit) {
        url += `&limit=${params.limit}`;
    }
    
    console.log(`[Meta Ads] Fetching ad creatives for ${adAccountId}`);
    
    try {
        const creatives: IMetaAdCreative[] = [];
        let nextUrl: string | undefined = url;
        
        // Handle pagination
        while (nextUrl) {
            const response = await this.makeAPIRequest<IMetaAPIResponse<IMetaAdCreative>>(nextUrl, accessToken);
            creatives.push(...response.data);
            
            nextUrl = response.paging?.next;
            
            if (nextUrl) {
                console.log(`   - Fetching next page (${creatives.length} creatives so far)...`);
            }
        }
        
        console.log(`✅ [Meta Ads] Fetched ${creatives.length} ad creatives`);
        return creatives;
    } catch (error: any) {
        console.error('❌ [Meta Ads] Failed to fetch ad creatives:', error);
        throw new Error(`Failed to fetch ad creatives: ${error.message}`);
    }
}

/**
 * Get custom conversions for an ad account
 */
public async getCustomConversions(
    adAccountId: string,
    accessToken: string
): Promise<IMetaCustomConversion[]> {
    const fields = [
        'id',
        'name',
        'rule',
        'event_source_type',
        'pixel_id',
        'custom_event_type',
        'default_conversion_value',
        'description',
        'creation_time',
        'last_fired_time',
        'is_archived',
    ].join(',');
    
    const url = `${MetaAdsService.BASE_URL}/${MetaAdsService.API_VERSION}/${adAccountId}/customconversions?fields=${fields}`;
    
    console.log(`[Meta Ads] Fetching custom conversions for ${adAccountId}`);
    
    try {
        const conversions: IMetaCustomConversion[] = [];
        let nextUrl: string | undefined = url;
        
        while (nextUrl) {
            const response = await this.makeAPIRequest<IMetaAPIResponse<IMetaCustomConversion>>(nextUrl, accessToken);
            conversions.push(...response.data);
            
            nextUrl = response.paging?.next;
            
            if (nextUrl) {
                console.log(`   - Fetching next page (${conversions.length} conversions so far)...`);
            }
        }
        
        console.log(`✅ [Meta Ads] Fetched ${conversions.length} custom conversions`);
        return conversions;
    } catch (error: any) {
        console.error('❌ [Meta Ads] Failed to fetch custom conversions:', error);
        throw new Error(`Failed to fetch custom conversions: ${error.message}`);
    }
}

/**
 * Get creative-level insights (optional - for advanced attribution)
 * Requires date range and returns performance by creative
 */
public async getCreativeInsights(
    adAccountId: string,
    accessToken: string,
    params: {
        startDate: string;
        endDate: string;
        creativeIds?: string[];
    }
): Promise<IMetaCreativeInsights[]> {
    const fields = [
        'creative_id',
        'date_start',
        'date_stop',
        'impressions',
        'clicks',
        'spend',
        'conversions',
        'conversion_value',
        'ctr',
        'cpc',
        'cpm',
    ].join(',');
    
    const queryParams = new URLSearchParams({
        fields: fields,
        time_range: JSON.stringify({
            since: params.startDate,
            until: params.endDate,
        }),
        level: 'ad',  // Creative insights come from ad-level breakdown
        breakdowns: 'creative_id',
    });
    
    const url = `${MetaAdsService.BASE_URL}/${MetaAdsService.API_VERSION}/${adAccountId}/insights?${queryParams.toString()}`;
    
    console.log(`[Meta Ads] Fetching creative insights for ${adAccountId}`);
    
    try {
        const insights: IMetaCreativeInsights[] = [];
        let nextUrl: string | undefined = url;
        
        while (nextUrl) {
            const response = await this.makeAPIRequest<IMetaAPIResponse<IMetaCreativeInsights>>(nextUrl, accessToken);
            insights.push(...response.data);
            
            nextUrl = response.paging?.next;
        }
        
        console.log(`✅ [Meta Ads] Fetched ${insights.length} creative insight records`);
        return insights;
    } catch (error: any) {
        console.error('❌ [Meta Ads] Failed to fetch creative insights:', error);
        throw new Error(`Failed to fetch creative insights: ${error.message}`);
    }
}

1.4 Add MetaAdsDriver Sync Methods

File: backend/src/drivers/MetaAdsDriver.ts

Update syncEntityType() method (around line 175):

private async syncEntityType(
    manager: any,
    schemaName: string,
    dataSourceId: number,
    usersPlatformId: number,
    entityType: string,
    connectionDetails: IAPIConnectionDetails,
    dateRange: { startDate: string; endDate: string }
): Promise<number> {
    switch (entityType.toLowerCase()) {
        case 'campaigns':
            return await this.syncCampaigns(manager, schemaName, dataSourceId, usersPlatformId, connectionDetails, dateRange);
        case 'adsets':
            return await this.syncAdSets(manager, schemaName, dataSourceId, usersPlatformId, connectionDetails);
        case 'ads':
            return await this.syncAds(manager, schemaName, dataSourceId, usersPlatformId, connectionDetails);
        case 'insights':
            return await this.syncInsights(manager, schemaName, dataSourceId, usersPlatformId, connectionDetails, dateRange);
        
        // ⭐ NEW
        case 'creatives':
            return await this.syncCreatives(manager, schemaName, dataSourceId, usersPlatformId, connectionDetails);
        
        // ⭐ NEW
        case 'custom_conversions':
            return await this.syncCustomConversions(manager, schemaName, dataSourceId, usersPlatformId, connectionDetails);
        
        default:
            console.warn(`⚠️ Unknown sync type: ${entityType}`);
            return 0;
    }
}

Add new sync methods (after existing sync methods, around line 400):

/**
 * Sync ad creatives
 */
private async syncCreatives(
    manager: any,
    schemaName: string,
    dataSourceId: number,
    usersPlatformId: number,
    connectionDetails: IAPIConnectionDetails
): Promise<number> {
    const tableMetadataService = TableMetadataService.getInstance();
    const logicalTableName = 'creatives';
    const tableName = tableMetadataService.generatePhysicalTableName(dataSourceId, logicalTableName);
    const adAccountId = connectionDetails.api_config?.ad_account_id!;
    
    // Create table if not exists
    await this.createCreativesTable(manager, schemaName, tableName);
    
    // Store table metadata
    await tableMetadataService.storeTableMetadata(manager, {
        dataSourceId,
        usersPlatformId,
        schemaName,
        physicalTableName: tableName,
        logicalTableName,
        originalSheetName: logicalTableName,
        tableType: 'meta_ads'
    });
    
    // Fetch creatives from Meta API
    const creatives = await this.metaAdsService.getCreatives(
        adAccountId,
        connectionDetails.oauth_access_token
    );
    
    if (creatives.length === 0) {
        console.log('   No creatives found');
        return 0;
    }
    
    // Insert or update creatives in batches
    const batchSize = 500;
    let totalInserted = 0;
    
    for (let i = 0; i < creatives.length; i += batchSize) {
        const batch = creatives.slice(i, i + batchSize);
        const records = batch.map(creative => this.transformCreative(creative));
        
        await this.batchUpsert(manager, schemaName, tableName, records, ['id']);
        totalInserted += batch.length;
    }
    
    return totalInserted;
}

/**
 * Sync custom conversions
 */
private async syncCustomConversions(
    manager: any,
    schemaName: string,
    dataSourceId: number,
    usersPlatformId: number,
    connectionDetails: IAPIConnectionDetails
): Promise<number> {
    const tableMetadataService = TableMetadataService.getInstance();
    const logicalTableName = 'custom_conversions';
    const tableName = tableMetadataService.generatePhysicalTableName(dataSourceId, logicalTableName);
    const adAccountId = connectionDetails.api_config?.ad_account_id!;
    
    // Create table if not exists
    await this.createCustomConversionsTable(manager, schemaName, tableName);
    
    // Store table metadata
    await tableMetadataService.storeTableMetadata(manager, {
        dataSourceId,
        usersPlatformId,
        schemaName,
        physicalTableName: tableName,
        logicalTableName,
        originalSheetName: logicalTableName,
        tableType: 'meta_ads'
    });
    
    // Fetch custom conversions from Meta API
    const conversions = await this.metaAdsService.getCustomConversions(
        adAccountId,
        connectionDetails.oauth_access_token
    );
    
    if (conversions.length === 0) {
        console.log('   No custom conversions found');
        return 0;
    }
    
    // Insert or update conversions in batches
    const batchSize = 500;
    let totalInserted = 0;
    
    for (let i = 0; i < conversions.length; i += batchSize) {
        const batch = conversions.slice(i, i + batchSize);
        const records = batch.map(conversion => this.transformCustomConversion(conversion));
        
        await this.batchUpsert(manager, schemaName, tableName, records, ['id']);
        totalInserted += batch.length;
    }
    
    return totalInserted;
}

/**
 * Create creatives table
 */
private async createCreativesTable(manager: any, schemaName: string, tableName: string): Promise<void> {
    const createTableQuery = `
        CREATE TABLE IF NOT EXISTS ${schemaName}."${tableName}" (
            id TEXT PRIMARY KEY,
            name TEXT,
            title TEXT,
            body TEXT,
            call_to_action_type TEXT,
            link_url TEXT,
            image_url TEXT,
            video_id TEXT,
            asset_feed_spec JSONB,
            object_story_spec JSONB,
            status TEXT,
            effective_object_story_id TEXT,
            synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
        
        CREATE INDEX IF NOT EXISTS idx_${tableName}_status 
        ON ${schemaName}."${tableName}"(status);
        
        CREATE INDEX IF NOT EXISTS idx_${tableName}_synced_at 
        ON ${schemaName}."${tableName}"(synced_at);
    `;
    
    await manager.query(createTableQuery);
    console.log(`✅ Created/verified creatives table: ${schemaName}."${tableName}"`);
}

/**
 * Create custom conversions table
 */
private async createCustomConversionsTable(manager: any, schemaName: string, tableName: string): Promise<void> {
    const createTableQuery = `
        CREATE TABLE IF NOT EXISTS ${schemaName}."${tableName}" (
            id TEXT PRIMARY KEY,
            name TEXT NOT NULL,
            rule TEXT,
            event_source_type TEXT,
            pixel_id TEXT,
            custom_event_type TEXT,
            default_conversion_value NUMERIC(15,2),
            description TEXT,
            creation_time TIMESTAMP,
            last_fired_time TIMESTAMP,
            is_archived BOOLEAN DEFAULT FALSE,
            synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );
        
        CREATE INDEX IF NOT EXISTS idx_${tableName}_event_type 
        ON ${schemaName}."${tableName}"(custom_event_type);
        
        CREATE INDEX IF NOT EXISTS idx_${tableName}_pixel_id 
        ON ${schemaName}."${tableName}"(pixel_id);
        
        CREATE INDEX IF NOT EXISTS idx_${tableName}_archived 
        ON ${schemaName}."${tableName}"(is_archived);
    `;
    
    await manager.query(createTableQuery);
    console.log(`✅ Created/verified custom conversions table: ${schemaName}."${tableName}"`);
}

/**
 * Transform creative data for database storage
 */
private transformCreative(creative: IMetaAdCreative): any {
    return {
        id: creative.id,
        name: creative.name || null,
        title: creative.title || null,
        body: creative.body || null,
        call_to_action_type: creative.call_to_action_type || null,
        link_url: creative.link_url || null,
        image_url: creative.image_url || null,
        video_id: creative.video_id || null,
        asset_feed_spec: creative.asset_feed_spec ? JSON.stringify(creative.asset_feed_spec) : null,
        object_story_spec: creative.object_story_spec ? JSON.stringify(creative.object_story_spec) : null,
        status: creative.status || null,
        effective_object_story_id: creative.effective_object_story_id || null,
        updated_at: new Date(),
    };
}

/**
 * Transform custom conversion data for database storage
 */
private transformCustomConversion(conversion: IMetaCustomConversion): any {
    return {
        id: conversion.id,
        name: conversion.name,
        rule: conversion.rule || null,
        event_source_type: conversion.event_source_type || null,
        pixel_id: conversion.pixel_id || null,
        custom_event_type: conversion.custom_event_type || null,
        default_conversion_value: conversion.default_conversion_value || null,
        description: conversion.description || null,
        creation_time: conversion.creation_time ? new Date(conversion.creation_time) : null,
        last_fired_time: conversion.last_fired_time ? new Date(conversion.last_fired_time) : null,
        is_archived: conversion.is_archived || false,
        updated_at: new Date(),
    };
}

1.5 Update DataSourceProcessor Delete Pattern

File: backend/src/processors/DataSourceProcessor.ts

Update the Meta Ads delete block (around line 450):

if (dataSource.data_type === EDataSourceType.META_ADS) {
    try {
        // Step 1: Drop tables tracked in metadata
        const metadataResults = await dbConnector.query(
            `SELECT physical_table_name FROM dra_table_metadata
             WHERE schema_name = 'dra_meta_ads' AND data_source_id = $1`,
            [dataSource.id]
        );
        for (const row of metadataResults) {
            await dbConnector.query(`DROP TABLE IF EXISTS dra_meta_ads."${row.physical_table_name}" CASCADE`);
        }
        // Step 2: Fallback for tables not tracked in metadata
        const fallbackTables = await dbConnector.query(
            `SELECT table_name FROM information_schema.tables
             WHERE table_schema = 'dra_meta_ads' AND table_name LIKE $1`,
            [`ds${dataSource.id}_%`]
        );
        for (const row of fallbackTables) {
            await dbConnector.query(`DROP TABLE IF EXISTS dra_meta_ads."${row.table_name}" CASCADE`);
        }
    } catch (error) { console.error('Error dropping Meta Ads tables:', error); }
}

No changes needed - existing pattern already handles new tables via ds{id}_% prefix.


1.6 Update DataModelProcessor (Column Naming)

File: backend/src/processors/DataModelProcessor.ts

Add 'dra_meta_ads' to the three-branch schema allowlist (lines ~651, ~770, ~830):

// Example from line 651 (CREATE TABLE column generation)
const isApiIntegratedSchema =
    schemaName === 'dra_excel' || schemaName === 'dra_pdf' || schemaName === 'dra_mongodb' ||
    schemaName === 'dra_google_analytics' || schemaName === 'dra_google_ad_manager' ||
    schemaName === 'dra_google_ads' || schemaName === 'dra_meta_ads' ||  // ⭐ ADD THIS
    schemaName === 'dra_linkedin_ads' || schemaName === 'dra_hubspot' || schemaName === 'dra_klaviyo';

Repeat at lines ~770 and ~830 (column data types map + INSERT rowKey construction).


1.7 Update DataSamplingService (AI Insights)

File: backend/src/services/DataSamplingService.ts

Add to apiIntegratedSchemas map (around line 540):

const apiIntegratedSchemas: Record<string, string> = {
    'google_analytics': 'dra_google_analytics',
    'google_ad_manager': 'dra_google_ad_manager',
    'google_ads': 'dra_google_ads',
    'meta_ads': 'dra_meta_ads',  // ⭐ ADD THIS
    'linkedin_ads': 'dra_linkedin_ads',
    'hubspot': 'dra_hubspot',
    'klaviyo': 'dra_klaviyo',
    'excel': 'dra_excel',
    'pdf': 'dra_pdf',
    'mongodb': 'dra_mongodb',
};

📦 Phase 2: Frontend Integration (Week 2)

2.1 Update Report Type Options

File: frontend/pages/projects/[projectid]/data-sources/connect/meta-ads.vue

Update reportTypeOptions array (around line 60):

const reportTypeOptions = [
    { id: 'campaigns', name: 'Campaigns', description: 'Campaign-level data (name, status, budget, objective)' },
    { id: 'adsets', name: 'Ad Sets', description: 'Ad set-level data (targeting, schedule, bid strategy)' },
    { id: 'ads', name: 'Ads', description: 'Individual ads (creative, status, preview URL)' },
    { id: 'insights', name: 'Insights', description: 'Performance metrics (impressions, clicks, spend, conversions)' },
    
    // ⭐ NEW
    { id: 'creatives', name: 'Ad Creatives', description: 'Creative assets (images, videos, copy, CTAs, headlines)' },
    
    // ⭐ NEW
    { id: 'custom_conversions', name: 'Custom Conversions', description: 'Conversion events (purchases, sign-ups, add-to-cart)' },
];

Default selection (around line 50):

selectedReportTypes: ['campaigns', 'adsets', 'ads', 'insights', 'creatives', 'custom_conversions'] as string[],

2.2 Update "What Gets Synced" Section

File: frontend/pages/projects/[projectid]/data-sources/connect/meta-ads.vue

Update the template section (around line 200):

<div class="space-y-3">
    <h3 class="font-semibold text-gray-900">What Gets Synced:</h3>
    <ul class="space-y-2 text-sm text-gray-600">
        <li class="flex items-start">
            <font-awesome-icon :icon="['fas', 'check']" class="w-4 h-4 text-green-500 mt-0.5 mr-2 flex-shrink-0" />
            <span><strong>Campaigns:</strong> Campaign name, status, budget, objective, schedule</span>
        </li>
        <li class="flex items-start">
            <font-awesome-icon :icon="['fas', 'check']" class="w-4 h-4 text-green-500 mt-0.5 mr-2 flex-shrink-0" />
            <span><strong>Ad Sets:</strong> Targeting parameters, bid strategy, daily/lifetime budget</span>
        </li>
        <li class="flex items-start">
            <font-awesome-icon :icon="['fas', 'check']" class="w-4 h-4 text-green-500 mt-0.5 mr-2 flex-shrink-0" />
            <span><strong>Ads:</strong> Individual ad names, status, creative links</span>
        </li>
        <li class="flex items-start">
            <font-awesome-icon :icon="['fas', 'check']" class="w-4 h-4 text-green-500 mt-0.5 mr-2 flex-shrink-0" />
            <span><strong>Insights:</strong> Impressions, clicks, spend, conversions, CPM, CPC, CTR</span>
        </li>
        
        <!-- ⭐ NEW -->
        <li class="flex items-start">
            <font-awesome-icon :icon="['fas', 'check']" class="w-4 h-4 text-green-500 mt-0.5 mr-2 flex-shrink-0" />
            <span><strong>Ad Creatives:</strong> Image/video URLs, headlines, body text, CTAs, creative type</span>
        </li>
        
        <!-- ⭐ NEW -->
        <li class="flex items-start">
            <font-awesome-icon :icon="['fas', 'check']" class="w-4 h-4 text-green-500 mt-0.5 mr-2 flex-shrink-0" />
            <span><strong>Custom Conversions:</strong> Conversion events, pixel tracking, conversion values</span>
        </li>
    </ul>
</div>

2.3 Update Data Source Images

Files:

  • frontend/pages/projects/[projectid]/data-sources/index.vue (line ~150)
  • frontend/pages/marketing-projects/[projectid]/data-sources/index.vue (line ~150)

No changes needed - Meta Ads already uses the Meta logo for all report types. Creative/conversion tables will appear as additional rows in the table list under the same data source.


2.4 Update Feature Flags (Optional - Production Readiness)

File: frontend/constants/featureFlags.ts

export const FEATURE_FLAGS = {
    META_ADS_ENABLED: true,  // ⭐ CHANGE TO TRUE after App Review approval
    META_ADS_CREATIVES_ENABLED: false,  // ⭐ NEW - set to true after testing
    META_ADS_CONVERSIONS_ENABLED: false,  // ⭐ NEW - set to true after testing
    // ... other flags
} as const;

Usage in connect page (optional gating):

// Only show if feature flag enabled
const reportTypeOptions = computed(() => {
    const baseOptions = [
        { id: 'campaigns', ... },
        { id: 'adsets', ... },
        { id: 'ads', ... },
        { id: 'insights', ... },
    ];
    
    if (FEATURE_FLAGS.META_ADS_CREATIVES_ENABLED) {
        baseOptions.push({ id: 'creatives', name: 'Ad Creatives', ... });
    }
    
    if (FEATURE_FLAGS.META_ADS_CONVERSIONS_ENABLED) {
        baseOptions.push({ id: 'custom_conversions', name: 'Custom Conversions', ... });
    }
    
    return baseOptions;
});

📦 Phase 3: Meta App Review & Testing (Week 3)

3.1 Meta App Review Submission

Prerequisites:

  1. ✅ Valid Business Manager linked to your Meta App
  2. ✅ Privacy Policy URL configured at https://developers.facebook.com/apps/{app-id}/settings/basic/
  3. ✅ App configured with correct redirect URIs
  4. ✅ Test ad account with creatives and custom conversions for demonstration

Submission Steps:

  1. Go to App Review Dashboard:

  2. Request ads_management Permission:

    • Click "Request" next to ads_management
    • Use case: "Analytics platform - read-only access to ad creatives and custom conversions"
    • Business verification: Already complete (if you have Business Manager verified)
  3. Provide Screenshots/Demo:

    • Screenshot 1: OAuth connection flow showing scope consent screen
    • Screenshot 2: Data source detail page showing synced creatives table
    • Screenshot 3: Data source detail page showing synced custom conversions table
    • Video (optional): 2-minute screen recording showing connection → sync → data display
  4. Privacy Policy Requirements:

    • Must include: "We access your Meta Ads data (including creatives and conversion tracking) solely for analytics and reporting purposes."
    • Must include: GDPR/CCPA compliance language
    • Must include: Data retention policy (e.g., "Data is stored for duration of subscription + 30 days")
  5. Review Timeline:

    • Standard: 3-5 business days
    • Expedited (if Business verified): 1-2 business days

3.2 Testing Checklist

Backend Tests:

# Test OAuth flow with new scope
curl -X GET "http://localhost:3002/meta-ads/connect?projectId=1" \
  -H "Authorization: Bearer {jwt_token}"

# Verify scope in returned auth URL includes ads_management

# Test creatives sync
curl -X POST "http://localhost:3002/meta-ads/sync/1" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json"

# Verify in PostgreSQL
psql -U your_user -d dra_database
\c dra_database
SET search_path TO dra_meta_ads;
\dt  # Should show creatives and custom_conversions tables
SELECT * FROM "ds1_abc12345" LIMIT 5;  # Replace with actual table name

Frontend Tests:

  1. Connection Flow:

    • Navigate to /projects/1/data-sources/connect/meta-ads
    • Verify "Ad Creatives" and "Custom Conversions" checkboxes appear
    • Click "Connect with Meta" → Verify consent screen shows ads_management scope
    • Complete OAuth → Verify redirect back to platform
  2. Sync Flow:

    • Data source detail page → Click "Sync Now"
    • Verify sync progress includes creatives + conversions in logs
    • Check sync history → Should show record counts for new tables
  3. Data Model Builder:

    • Create new data model
    • Verify creatives and custom_conversions tables appear in table selector
    • Drag creative table → Verify columns show (id, name, title, body, image_url, etc.)
    • Drag conversions table → Verify columns show (id, name, custom_event_type, pixel_id, etc.)
  4. AI Data Modeler:

    • Open AI Data Modeler for Meta Ads source
    • Ask: "Show me which creatives drove the most conversions"
    • Verify Gemini can see creatives + conversions tables in schema context
    • Apply generated model → Verify JOIN between ads, creatives, and insights

Attribution Tests:

  1. Create Attribution Report (if attribution panel exists):

    • Navigate to Attribution panel
    • Generate report with Meta Ads data source
    • Verify conversion events from custom_conversions table appear in funnel
    • Verify creative details from creatives table appear in touchpoint analysis
  2. Creative Performance Analysis:

    • Create dashboard with chart: "Top 10 Creatives by Conversion Value"
    • JOIN: insights (conversion_value) ← ads (creative_id) ← creatives (name, image_url)
    • Verify query returns creative names with conversion totals

3.3 Documentation Updates

Update Files:

  1. documentation/META_ADS_IMPLEMENTATION.md (create if missing):

    • Complete architecture overview
    • Table schemas for all 6 tables
    • API permission requirements
    • OAuth flow diagram
    • Sample queries for creative attribution
  2. documentation/FEATURES.md:

    - [x] Meta Ads
        - [x] OAuth 2.0 Authentication
        - [x] Campaigns
        - [x] Ad Sets
        - [x] Ads
        - [x] Insights (Performance Metrics)
        - [x] Ad Creatives (Images, Videos, Copy, CTAs)
        - [x] Custom Conversions (Pixel Events, Conversion Tracking)
  3. README.md (if Meta Ads mentioned):

    • Update feature list to include creative-level attribution

📊 Success Metrics

Technical Metrics

  • ✅ All API calls return 200 OK with valid data
  • ✅ Tables created with correct schema (13 columns for creatives, 11 for conversions)
  • ✅ Sync completes in < 5 minutes for accounts with < 10,000 creatives
  • ✅ No sync errors in production logs for 7 days post-launch
  • ✅ AI Data Modeler can successfully JOIN creatives with insights

Business Metrics

  • 🎯 Attribution Completeness: 90%+ of Meta Ads conversions linked to specific creatives
  • 🎯 User Adoption: 30%+ of Meta Ads users enable creative/conversion sync within 30 days
  • 🎯 Creative ROI Visibility: Users can answer "Which creative drove the most revenue?" within 2 clicks
  • 🎯 Support Tickets: < 5 tickets related to missing conversion data in first month

🚨 Risks & Mitigations

Risk 1: App Review Rejection

Likelihood: Low
Impact: High (delays launch by 1-2 weeks)
Mitigation:

  • Ensure privacy policy explicitly mentions creative and conversion data access
  • Provide detailed use case: "Read-only analytics for marketing ROI tracking"
  • Include screenshots showing data is never modified/deleted
  • Highlight that ads_management is used in read-only mode (no write operations)

Risk 2: Existing Users Need Re-Authentication

Likelihood: High
Impact: Medium (user friction)
Mitigation:

  • Display banner on data source detail page: "Re-connect to enable creative tracking"
  • Provide 1-click re-auth button
  • Store original connection timestamp → only show banner for connections created before permission update
  • Email notification: "New Meta Ads features available - reconnect to unlock"

Risk 3: Large Creative Counts Slow Sync

Likelihood: Medium
Impact: Medium (sync timeouts)
Mitigation:

  • Implement batch pagination (500 creatives per batch)
  • Add progress updates via Socket.IO
  • Set timeout to 10 minutes (up from default 5 minutes)
  • Consider background job queue for accounts with > 5,000 creatives

Risk 4: Custom Conversions Don't Match Insights Data

Likelihood: Medium
Impact: High (attribution accuracy)
Mitigation:

  • Document that custom conversions are configuration (what events are tracked)
  • Actual conversion values come from insights table with action_type breakdown
  • Provide sample JOIN query in docs:
    SELECT 
        cc.name AS conversion_name,
        i.conversions,
        i.conversion_value
    FROM dra_meta_ads.insights i
    JOIN dra_meta_ads.custom_conversions cc 
        ON i.action_type = cc.custom_event_type

🎯 Post-Launch Enhancements (Future Phases)

Phase 4: Creative-Level Insights (Optional)

  • Implement getCreativeInsights() method (already stubbed in plan)
  • Create separate creative_insights table with daily performance by creative
  • Enables time-series analysis: "Video Creative A performed better on weekends"

Phase 5: Lead Forms & Leads

  • Add leads_retrieval permission (requires stricter App Review)
  • Sync lead form configurations
  • Sync actual lead submissions (PII data - needs GDPR compliance)

Phase 6: Attribution Panel Integration

  • Add "Meta Ads Creative Attribution" tab to existing Attribution panel
  • Show creative performance across attribution models
  • Visualization: "First-touch vs. Last-touch attribution by creative type"

📝 Implementation Checklist

Phase 1: Backend (Week 1)

  • Update MetaOAuthService.getMetaAdsScopes() to include ads_management
  • Add TypeScript interfaces to IMetaAds.ts
  • Implement MetaAdsService.getCreatives()
  • Implement MetaAdsService.getCustomConversions()
  • Add syncCreatives() method to MetaAdsDriver
  • Add syncCustomConversions() method to MetaAdsDriver
  • Create createCreativesTable() method
  • Create createCustomConversionsTable() method
  • Add transform methods (transformCreative, transformCustomConversion)
  • Update syncEntityType() switch statement
  • Update DataModelProcessor (3 locations with schema allowlist)
  • Update DataSamplingService with 'meta_ads': 'dra_meta_ads'
  • Test backend with curl/Postman

Phase 2: Frontend (Week 2)

  • Update reportTypeOptions in meta-ads.vue
  • Update default selectedReportTypes array
  • Update "What Gets Synced" template section
  • Add feature flags (optional)
  • Test OAuth flow in browser
  • Test sync flow and verify tables created
  • Test Data Model Builder with new tables
  • Test AI Data Modeler schema awareness

Phase 3: App Review & Testing (Week 3)

  • Review privacy policy for Meta compliance
  • Prepare screenshots for App Review
  • Submit ads_management permission request
  • Wait for approval (3-5 days)
  • Run backend test suite
  • Run frontend SSR validation (npm run validate:ssr)
  • Manual testing with real Meta Ads account
  • Attribution accuracy validation
  • Update documentation (FEATURES.md, README.md)
  • Create META_ADS_IMPLEMENTATION.md complete guide

Post-Launch

  • Monitor sync error rates in production
  • Collect user feedback on creative attribution accuracy
  • Plan Phase 4 (Creative-Level Insights) if demand exists

🔗 Related Documentation


📞 Support & Questions

Questions During Implementation:

  1. Check existing LinkedIn Ads implementation for OAuth/sync patterns
  2. Review Google Ads driver for insights-level data handling
  3. Consult copilot-instructions.md for coding conventions

Meta API Documentation:


End of Implementation Plan

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions