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:
- 🔴 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"
- 🔴 Missing Conversion Event Tracking: Can see impressions/clicks but NOT actual business outcomes (purchases, sign-ups, leads)
- 🔴 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:
- ✅ Valid Business Manager linked to your Meta App
- ✅ Privacy Policy URL configured at https://developers.facebook.com/apps/{app-id}/settings/basic/
- ✅ App configured with correct redirect URIs
- ✅ Test ad account with creatives and custom conversions for demonstration
Submission Steps:
-
Go to App Review Dashboard:
-
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)
-
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
-
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")
-
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:
-
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
-
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
-
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.)
-
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:
-
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
-
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:
-
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
-
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)
-
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)
Phase 2: Frontend (Week 2)
Phase 3: App Review & Testing (Week 3)
Post-Launch
🔗 Related Documentation
📞 Support & Questions
Questions During Implementation:
- Check existing LinkedIn Ads implementation for OAuth/sync patterns
- Review Google Ads driver for insights-level data handling
- Consult
copilot-instructions.md for coding conventions
Meta API Documentation:
End of Implementation Plan
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:
Current vs. Desired State
Business Impact
✅ What's Already Implemented
Backend Infrastructure ✅
dra_meta_adsschemads{id}_{hash8}) and metadata storageexpensiveOperationsLimiter(30 req/15min) on sync endpointsFrontend Integration ✅
Database Schema ✅
dra_meta_adscampaigns,adsets,ads,insightsdra_table_metadataAPI Permissions (Current)⚠️
❌ Critical Gaps (What's Missing)
Gap 1: Ad Creatives Table
Gap 2: Custom Conversions Table
Gap 3: API Permissions
ads_managementpermission for both tables (requires App Review)Gap 4: Frontend Configuration
🎯 Proposed Solution: 3-Phase Implementation
Phase Overview
📦 Phase 1: Backend Infrastructure (Week 1)
1.1 Update OAuth Permissions
File:
backend/src/services/MetaOAuthService.tsChange:
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.tsAdd:
1.3 Implement MetaAdsService Methods
File:
backend/src/services/MetaAdsService.tsAdd (after existing methods, around line 400):
1.4 Add MetaAdsDriver Sync Methods
File:
backend/src/drivers/MetaAdsDriver.tsUpdate
syncEntityType()method (around line 175):Add new sync methods (after existing sync methods, around line 400):
1.5 Update DataSourceProcessor Delete Pattern
File:
backend/src/processors/DataSourceProcessor.tsUpdate the Meta Ads delete block (around line 450):
No changes needed - existing pattern already handles new tables via
ds{id}_%prefix.1.6 Update DataModelProcessor (Column Naming)
File:
backend/src/processors/DataModelProcessor.tsAdd
'dra_meta_ads'to the three-branch schema allowlist (lines ~651, ~770, ~830):Repeat at lines ~770 and ~830 (column data types map + INSERT rowKey construction).
1.7 Update DataSamplingService (AI Insights)
File:
backend/src/services/DataSamplingService.tsAdd to
apiIntegratedSchemasmap (around line 540):📦 Phase 2: Frontend Integration (Week 2)
2.1 Update Report Type Options
File:
frontend/pages/projects/[projectid]/data-sources/connect/meta-ads.vueUpdate
reportTypeOptionsarray (around line 60):Default selection (around line 50):
2.2 Update "What Gets Synced" Section
File:
frontend/pages/projects/[projectid]/data-sources/connect/meta-ads.vueUpdate the template section (around line 200):
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.tsUsage in connect page (optional gating):
📦 Phase 3: Meta App Review & Testing (Week 3)
3.1 Meta App Review Submission
Prerequisites:
Submission Steps:
Go to App Review Dashboard:
Request
ads_managementPermission:ads_managementProvide Screenshots/Demo:
Privacy Policy Requirements:
Review Timeline:
3.2 Testing Checklist
Backend Tests:
Frontend Tests:
Connection Flow:
/projects/1/data-sources/connect/meta-adsads_managementscopeSync Flow:
Data Model Builder:
AI Data Modeler:
Attribution Tests:
Create Attribution Report (if attribution panel exists):
custom_conversionstable appear in funnelcreativestable appear in touchpoint analysisCreative Performance Analysis:
insights(conversion_value) ←ads(creative_id) ←creatives(name, image_url)3.3 Documentation Updates
Update Files:
documentation/META_ADS_IMPLEMENTATION.md(create if missing):documentation/FEATURES.md:README.md(if Meta Ads mentioned):📊 Success Metrics
Technical Metrics
Business Metrics
🚨 Risks & Mitigations
Risk 1: App Review Rejection
Likelihood: Low
Impact: High (delays launch by 1-2 weeks)
Mitigation:
ads_managementis used in read-only mode (no write operations)Risk 2: Existing Users Need Re-Authentication
Likelihood: High
Impact: Medium (user friction)
Mitigation:
Risk 3: Large Creative Counts Slow Sync
Likelihood: Medium
Impact: Medium (sync timeouts)
Mitigation:
Risk 4: Custom Conversions Don't Match Insights Data
Likelihood: Medium
Impact: High (attribution accuracy)
Mitigation:
insightstable withaction_typebreakdown🎯 Post-Launch Enhancements (Future Phases)
Phase 4: Creative-Level Insights (Optional)
getCreativeInsights()method (already stubbed in plan)creative_insightstable with daily performance by creativePhase 5: Lead Forms & Leads
leads_retrievalpermission (requires stricter App Review)Phase 6: Attribution Panel Integration
📝 Implementation Checklist
Phase 1: Backend (Week 1)
MetaOAuthService.getMetaAdsScopes()to includeads_managementIMetaAds.tsMetaAdsService.getCreatives()MetaAdsService.getCustomConversions()syncCreatives()method toMetaAdsDriversyncCustomConversions()method toMetaAdsDrivercreateCreativesTable()methodcreateCustomConversionsTable()methodtransformCreative,transformCustomConversion)syncEntityType()switch statementDataModelProcessor(3 locations with schema allowlist)DataSamplingServicewith'meta_ads': 'dra_meta_ads'Phase 2: Frontend (Week 2)
reportTypeOptionsinmeta-ads.vueselectedReportTypesarrayPhase 3: App Review & Testing (Week 3)
ads_managementpermission requestnpm run validate:ssr)META_ADS_IMPLEMENTATION.mdcomplete guidePost-Launch
🔗 Related Documentation
📞 Support & Questions
Questions During Implementation:
copilot-instructions.mdfor coding conventionsMeta API Documentation:
End of Implementation Plan