How to Build a Cross-Platform Ad Performance Dashboard
Combine Meta, Google, and TikTok ad data in one unified dashboard.
Jay Banlasan
The AI Systems Guy
A cross platform ad performance dashboard puts every ad account on one screen. No more switching between Meta Ads Manager, Google Ads, and TikTok. I built this for a client running ads on three platforms and it cut their weekly reporting time from 3 hours to 10 minutes.
The trick is normalizing the data. Each platform uses different field names for the same metrics. You fix that once in the ingestion layer, then everything downstream is clean.
What You Need Before Starting
- API access to each platform (Meta, Google Ads, TikTok)
- Python 3.8+ with
requests,sqlite3 - A web framework for the dashboard (Flask works fine)
- SQLite for the unified data store
Step 1: Create the Unified Schema
import sqlite3
def init_db(db_path="ads_unified.db"):
conn = sqlite3.connect(db_path)
conn.execute("""CREATE TABLE IF NOT EXISTS unified_ads (
id INTEGER PRIMARY KEY,
platform TEXT,
account_name TEXT,
campaign_name TEXT,
adset_name TEXT,
ad_name TEXT,
date TEXT,
spend REAL,
impressions INTEGER,
clicks INTEGER,
conversions REAL,
revenue REAL,
cpc REAL,
ctr REAL,
cpa REAL,
roas REAL,
fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP
)""")
conn.commit()
return conn
Step 2: Normalize Data from Each Platform
def normalize_meta(raw_data, account_name):
rows = []
for ad in raw_data:
spend = float(ad.get("spend", 0))
clicks = int(ad.get("clicks", 0))
conversions = sum(int(a["value"]) for a in ad.get("actions", []) if a["action_type"] in ["lead", "purchase"])
revenue = sum(float(a["value"]) for a in ad.get("action_values", []) if a["action_type"] == "purchase")
rows.append({
"platform": "meta",
"account_name": account_name,
"campaign_name": ad.get("campaign_name", ""),
"adset_name": ad.get("adset_name", ""),
"ad_name": ad.get("ad_name", ""),
"date": ad.get("date_start", ""),
"spend": spend,
"impressions": int(ad.get("impressions", 0)),
"clicks": clicks,
"conversions": conversions,
"revenue": revenue,
"cpc": spend / clicks if clicks > 0 else 0,
"ctr": float(ad.get("ctr", 0)),
"cpa": spend / conversions if conversions > 0 else 0,
"roas": revenue / spend if spend > 0 else 0,
})
return rows
def normalize_google(raw_data, account_name):
rows = []
for row in raw_data:
spend = row["cost_micros"] / 1_000_000
clicks = row["clicks"]
conversions = row["conversions"]
rows.append({
"platform": "google",
"account_name": account_name,
"campaign_name": row["campaign_name"],
"adset_name": row.get("ad_group_name", ""),
"ad_name": row.get("ad_name", ""),
"date": row["date"],
"spend": spend,
"impressions": row["impressions"],
"clicks": clicks,
"conversions": conversions,
"revenue": row.get("conversion_value", 0),
"cpc": spend / clicks if clicks > 0 else 0,
"ctr": (clicks / row["impressions"] * 100) if row["impressions"] > 0 else 0,
"cpa": spend / conversions if conversions > 0 else 0,
"roas": row.get("conversion_value", 0) / spend if spend > 0 else 0,
})
return rows
Step 3: Build the Summary Queries
def platform_summary(db_path, date):
conn = sqlite3.connect(db_path)
rows = conn.execute("""
SELECT platform,
SUM(spend) as total_spend,
SUM(clicks) as total_clicks,
SUM(conversions) as total_conversions,
SUM(revenue) as total_revenue,
CASE WHEN SUM(conversions) > 0 THEN SUM(spend)/SUM(conversions) ELSE 0 END as avg_cpa,
CASE WHEN SUM(spend) > 0 THEN SUM(revenue)/SUM(spend) ELSE 0 END as avg_roas
FROM unified_ads
WHERE date = ?
GROUP BY platform
""", (date,)).fetchall()
conn.close()
return rows
def total_summary(db_path, start_date, end_date):
conn = sqlite3.connect(db_path)
row = conn.execute("""
SELECT SUM(spend), SUM(clicks), SUM(conversions), SUM(revenue),
CASE WHEN SUM(conversions) > 0 THEN SUM(spend)/SUM(conversions) ELSE 0 END,
CASE WHEN SUM(spend) > 0 THEN SUM(revenue)/SUM(spend) ELSE 0 END
FROM unified_ads
WHERE date BETWEEN ? AND ?
""", (start_date, end_date)).fetchone()
conn.close()
return row
Step 4: Serve the Dashboard
from flask import Flask, render_template_string
from datetime import datetime, timedelta
app = Flask(__name__)
DASHBOARD_HTML = """
<html><body style="font-family:sans-serif;background:#0a0a0a;color:#e5e7eb;padding:2rem;">
<h1>Ad Performance Dashboard</h1>
<h2>{{ date }}</h2>
<table style="width:100%;border-collapse:collapse;">
<tr style="border-bottom:1px solid #333;"><th>Platform</th><th>Spend</th><th>Clicks</th><th>Conversions</th><th>CPA</th><th>ROAS</th></tr>
{% for row in rows %}
<tr style="border-bottom:1px solid #222;">
<td>{{ row[0] }}</td><td>${{ "%.2f"|format(row[1]) }}</td><td>{{ row[2] }}</td>
<td>{{ row[3] }}</td><td>${{ "%.2f"|format(row[5]) }}</td><td>{{ "%.2f"|format(row[6]) }}x</td>
</tr>{% endfor %}
</table></body></html>
"""
@app.route("/")
def dashboard():
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
rows = platform_summary("ads_unified.db", yesterday)
return render_template_string(DASHBOARD_HTML, date=yesterday, rows=rows)
Step 5: Automate the Data Pull
Run all platform fetchers, then serve:
# Cron: pull all platform data at 6 AM, dashboard is fresh by 7 AM
0 6 * * * cd /path/to/project && python3 pull_all_platforms.py
One database. One dashboard. Every platform.
What to Build Next
Add date range filtering and campaign-level drill-downs. Then add an AI summary that explains what changed day over day and recommends actions.
Related Reading
- AI-Powered Reporting That Actually Gets Read - building reports that drive action
- Cross-Functional AI: When Marketing Talks to Operations - connecting different data sources
- The Pipeline Architecture - designing data pipelines that scale
Want this system built for your business?
Get a free assessment. We will map every system your business needs and show you the ROI.
Get Your Free Assessment