Added some logging
This commit is contained in:
@@ -4,6 +4,7 @@ Runs periodically to build time-series data for dashboards.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta, timezone, date
|
from datetime import datetime, timedelta, timezone, date
|
||||||
from typing import Optional, Dict
|
from typing import Optional, Dict
|
||||||
@@ -20,6 +21,9 @@ from .database import TimescaleDBClient
|
|||||||
from .rate_limiter import MetaRateLimiter
|
from .rate_limiter import MetaRateLimiter
|
||||||
from .token_manager import MetaTokenManager
|
from .token_manager import MetaTokenManager
|
||||||
|
|
||||||
|
# Set up logger
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ScheduledInsightsGrabber:
|
class ScheduledInsightsGrabber:
|
||||||
"""
|
"""
|
||||||
@@ -340,11 +344,19 @@ class ScheduledInsightsGrabber:
|
|||||||
for campaign in campaigns:
|
for campaign in campaigns:
|
||||||
campaign_id = campaign['id']
|
campaign_id = campaign['id']
|
||||||
campaign_name = campaign.get('name')
|
campaign_name = campaign.get('name')
|
||||||
|
campaign_dict = dict(campaign)
|
||||||
|
|
||||||
|
# DEBUG: Log all campaign data before upsert
|
||||||
|
logger.info(
|
||||||
|
f"Campaign metadata before upsert: id={campaign_id}, "
|
||||||
|
f"name={campaign_name!r}, status={campaign.get('status')}, "
|
||||||
|
f"objective={campaign.get('objective')}, raw={campaign_dict}"
|
||||||
|
)
|
||||||
|
|
||||||
# Track campaigns without names for debugging
|
# Track campaigns without names for debugging
|
||||||
if not campaign_name:
|
if not campaign_name:
|
||||||
campaigns_without_name.append(campaign_id)
|
campaigns_without_name.append(campaign_id)
|
||||||
print(f" WARNING: Campaign {campaign_id} has no name. Raw data: {dict(campaign)}")
|
logger.warning(f"Campaign {campaign_id} has no name. Raw data: {campaign_dict}")
|
||||||
|
|
||||||
await self.db.upsert_campaign(
|
await self.db.upsert_campaign(
|
||||||
campaign_id=campaign_id,
|
campaign_id=campaign_id,
|
||||||
@@ -358,6 +370,7 @@ class ScheduledInsightsGrabber:
|
|||||||
print(f" {count} campaigns cached for {account_id}")
|
print(f" {count} campaigns cached for {account_id}")
|
||||||
if campaigns_without_name:
|
if campaigns_without_name:
|
||||||
print(f" ⚠️ {len(campaigns_without_name)} campaigns without names: {campaigns_without_name}")
|
print(f" ⚠️ {len(campaigns_without_name)} campaigns without names: {campaigns_without_name}")
|
||||||
|
logger.warning(f"{len(campaigns_without_name)} campaigns without names: {campaigns_without_name}")
|
||||||
|
|
||||||
async def cache_adsets_metadata(self, account_id: str, limit: int = 100):
|
async def cache_adsets_metadata(self, account_id: str, limit: int = 100):
|
||||||
"""
|
"""
|
||||||
@@ -1201,6 +1214,16 @@ class ScheduledInsightsGrabber:
|
|||||||
|
|
||||||
async def async_main():
|
async def async_main():
|
||||||
"""Async main entry point for scheduled grabber."""
|
"""Async main entry point for scheduled grabber."""
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('meta_api_grabber.log'),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Initialize with max_accounts=3 for conservative start
|
# Initialize with max_accounts=3 for conservative start
|
||||||
# Set max_accounts=None to process all accessible accounts
|
# Set max_accounts=None to process all accessible accounts
|
||||||
|
|||||||
91
src/meta_api_grabber/test_campaign_insights.py
Normal file
91
src/meta_api_grabber/test_campaign_insights.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
Test to see what campaign insights API actually returns for campaign_name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from facebook_business.api import FacebookAdsApi
|
||||||
|
from facebook_business.adobjects.adaccount import AdAccount
|
||||||
|
from facebook_business.adobjects.adsinsights import AdsInsights
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Initialize Facebook Ads API
|
||||||
|
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||||
|
app_secret = os.getenv("META_APP_SECRET")
|
||||||
|
app_id = os.getenv("META_APP_ID")
|
||||||
|
|
||||||
|
if not all([access_token, app_secret, app_id]):
|
||||||
|
raise ValueError("Missing required environment variables")
|
||||||
|
|
||||||
|
FacebookAdsApi.init(
|
||||||
|
app_id=app_id,
|
||||||
|
app_secret=app_secret,
|
||||||
|
access_token=access_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
account_id = "act_238334370765317"
|
||||||
|
|
||||||
|
print(f"Testing campaign INSIGHTS fetch for account: {account_id}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Get campaign insights (this is what scheduled_grabber.py does)
|
||||||
|
print("\nFetching campaign-level insights with 'today' preset...")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
AdsInsights.Field.campaign_id,
|
||||||
|
AdsInsights.Field.campaign_name,
|
||||||
|
AdsInsights.Field.impressions,
|
||||||
|
AdsInsights.Field.clicks,
|
||||||
|
AdsInsights.Field.spend,
|
||||||
|
AdsInsights.Field.ctr,
|
||||||
|
AdsInsights.Field.cpc,
|
||||||
|
AdsInsights.Field.cpm,
|
||||||
|
AdsInsights.Field.reach,
|
||||||
|
AdsInsights.Field.actions,
|
||||||
|
AdsInsights.Field.date_start,
|
||||||
|
AdsInsights.Field.date_stop,
|
||||||
|
]
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"date_preset": "today",
|
||||||
|
"level": "campaign",
|
||||||
|
"limit": 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
ad_account = AdAccount(account_id)
|
||||||
|
insights = ad_account.get_insights(
|
||||||
|
fields=fields,
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
|
||||||
|
insights_list = list(insights)
|
||||||
|
print(f"Found {len(insights_list)} campaign insights\n")
|
||||||
|
|
||||||
|
campaigns_without_names = []
|
||||||
|
for i, insight in enumerate(insights_list, 1):
|
||||||
|
insight_dict = dict(insight)
|
||||||
|
campaign_id = insight_dict.get('campaign_id')
|
||||||
|
campaign_name = insight_dict.get('campaign_name')
|
||||||
|
|
||||||
|
print(f"Campaign Insight {i}:")
|
||||||
|
print(f" Campaign ID: {campaign_id}")
|
||||||
|
print(f" Campaign Name: {campaign_name if campaign_name else '❌ MISSING'}")
|
||||||
|
print(f" Impressions: {insight_dict.get('impressions')}")
|
||||||
|
print(f" Spend: {insight_dict.get('spend')}")
|
||||||
|
print(f" Keys available: {list(insight_dict.keys())}")
|
||||||
|
print(f" Raw JSON: {json.dumps(insight_dict, indent=4, default=str)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not campaign_name:
|
||||||
|
campaigns_without_names.append(campaign_id)
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
if campaigns_without_names:
|
||||||
|
print(f"⚠️ {len(campaigns_without_names)} campaigns WITHOUT names in insights:")
|
||||||
|
print(f" {campaigns_without_names}")
|
||||||
|
else:
|
||||||
|
print("✓ All campaigns have names in insights!")
|
||||||
118
src/meta_api_grabber/test_campaigns.py
Normal file
118
src/meta_api_grabber/test_campaigns.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
Test script to diagnose campaign name issues for a specific ad account.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from facebook_business.api import FacebookAdsApi
|
||||||
|
from facebook_business.adobjects.adaccount import AdAccount
|
||||||
|
from facebook_business.adobjects.campaign import Campaign
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Initialize Facebook Ads API
|
||||||
|
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||||
|
app_secret = os.getenv("META_APP_SECRET")
|
||||||
|
app_id = os.getenv("META_APP_ID")
|
||||||
|
|
||||||
|
if not all([access_token, app_secret, app_id]):
|
||||||
|
raise ValueError("Missing required environment variables")
|
||||||
|
|
||||||
|
FacebookAdsApi.init(
|
||||||
|
app_id=app_id,
|
||||||
|
app_secret=app_secret,
|
||||||
|
access_token=access_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test account ID
|
||||||
|
account_id = "act_238334370765317"
|
||||||
|
|
||||||
|
print(f"Testing campaign fetch for account: {account_id}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Get campaigns using get_campaigns
|
||||||
|
print("\n1. Testing AdAccount.get_campaigns()...")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
ad_account = AdAccount(account_id)
|
||||||
|
campaigns = ad_account.get_campaigns(
|
||||||
|
fields=[
|
||||||
|
Campaign.Field.name,
|
||||||
|
Campaign.Field.status,
|
||||||
|
Campaign.Field.objective,
|
||||||
|
Campaign.Field.id,
|
||||||
|
],
|
||||||
|
params={'limit': 10}
|
||||||
|
)
|
||||||
|
|
||||||
|
campaigns_list = list(campaigns)
|
||||||
|
print(f"Found {len(campaigns_list)} campaigns\n")
|
||||||
|
|
||||||
|
campaigns_without_names = []
|
||||||
|
for i, campaign in enumerate(campaigns_list, 1):
|
||||||
|
campaign_dict = dict(campaign)
|
||||||
|
campaign_id = campaign_dict.get('id')
|
||||||
|
campaign_name = campaign_dict.get('name')
|
||||||
|
|
||||||
|
print(f"Campaign {i}:")
|
||||||
|
print(f" ID: {campaign_id}")
|
||||||
|
print(f" Name: {campaign_name if campaign_name else '❌ MISSING'}")
|
||||||
|
print(f" Status: {campaign_dict.get('status')}")
|
||||||
|
print(f" Objective: {campaign_dict.get('objective')}")
|
||||||
|
print(f" Raw data: {json.dumps(campaign_dict, indent=4)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not campaign_name:
|
||||||
|
campaigns_without_names.append(campaign_id)
|
||||||
|
|
||||||
|
# If we found campaigns without names, try fetching them individually
|
||||||
|
if campaigns_without_names:
|
||||||
|
print("\n2. Retrying campaigns without names (individual fetch)...")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
for campaign_id in campaigns_without_names:
|
||||||
|
print(f"\nFetching campaign {campaign_id} directly...")
|
||||||
|
try:
|
||||||
|
campaign_obj = Campaign(campaign_id)
|
||||||
|
campaign_data = campaign_obj.api_get(
|
||||||
|
fields=[
|
||||||
|
Campaign.Field.name,
|
||||||
|
Campaign.Field.status,
|
||||||
|
Campaign.Field.objective,
|
||||||
|
Campaign.Field.account_id,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
campaign_dict = dict(campaign_data)
|
||||||
|
print(f" Direct fetch result:")
|
||||||
|
print(f" Name: {campaign_dict.get('name', '❌ STILL MISSING')}")
|
||||||
|
print(f" Status: {campaign_dict.get('status')}")
|
||||||
|
print(f" Raw data: {json.dumps(campaign_dict, indent=4)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error fetching campaign: {e}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"Summary: {len(campaigns_without_names)} out of {len(campaigns_list)} campaigns have missing names")
|
||||||
|
print(f"Missing campaign IDs: {campaigns_without_names}")
|
||||||
|
else:
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("✓ All campaigns have names!")
|
||||||
|
|
||||||
|
print("\n3. Checking account permissions...")
|
||||||
|
print("-" * 60)
|
||||||
|
try:
|
||||||
|
account_data = ad_account.api_get(
|
||||||
|
fields=['name', 'account_status', 'capabilities', 'business']
|
||||||
|
)
|
||||||
|
account_dict = dict(account_data)
|
||||||
|
print(f"Account Name: {account_dict.get('name')}")
|
||||||
|
print(f"Account Status: {account_dict.get('account_status')}")
|
||||||
|
print(f"Capabilities: {json.dumps(account_dict.get('capabilities', []), indent=2)}")
|
||||||
|
print(f"Business ID: {account_dict.get('business')}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching account details: {e}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Test complete!")
|
||||||
108
src/meta_api_grabber/test_rate_limiter_issue.py
Normal file
108
src/meta_api_grabber/test_rate_limiter_issue.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
Test to diagnose if the rate limiter is causing campaign name issues.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from facebook_business.api import FacebookAdsApi
|
||||||
|
from facebook_business.adobjects.adaccount import AdAccount
|
||||||
|
from facebook_business.adobjects.campaign import Campaign
|
||||||
|
|
||||||
|
from rate_limiter import MetaRateLimiter
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Initialize Facebook Ads API
|
||||||
|
access_token = os.getenv("META_ACCESS_TOKEN")
|
||||||
|
app_secret = os.getenv("META_APP_SECRET")
|
||||||
|
app_id = os.getenv("META_APP_ID")
|
||||||
|
|
||||||
|
if not all([access_token, app_secret, app_id]):
|
||||||
|
raise ValueError("Missing required environment variables")
|
||||||
|
|
||||||
|
FacebookAdsApi.init(
|
||||||
|
app_id=app_id,
|
||||||
|
app_secret=app_secret,
|
||||||
|
access_token=access_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
account_id = "act_238334370765317"
|
||||||
|
|
||||||
|
# Initialize rate limiter
|
||||||
|
rate_limiter = MetaRateLimiter(
|
||||||
|
base_delay=0.1, # Fast for testing
|
||||||
|
throttle_threshold=75.0,
|
||||||
|
max_retry_delay=300.0,
|
||||||
|
max_retries=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _rate_limited_request(func, *args, **kwargs):
|
||||||
|
"""Execute a request with rate limiting (same as in scheduled_grabber.py)."""
|
||||||
|
return await rate_limiter.execute_with_retry(func, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_direct_fetch():
|
||||||
|
"""Test direct fetch without rate limiter."""
|
||||||
|
print("1. DIRECT FETCH (no rate limiter)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
ad_account = AdAccount(account_id)
|
||||||
|
campaigns = ad_account.get_campaigns(
|
||||||
|
fields=[
|
||||||
|
Campaign.Field.name,
|
||||||
|
Campaign.Field.status,
|
||||||
|
Campaign.Field.objective,
|
||||||
|
],
|
||||||
|
params={'limit': 5}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Type of campaigns: {type(campaigns)}")
|
||||||
|
print(f"Campaigns object: {campaigns}\n")
|
||||||
|
|
||||||
|
for i, campaign in enumerate(campaigns, 1):
|
||||||
|
campaign_dict = dict(campaign)
|
||||||
|
print(f"Campaign {i}:")
|
||||||
|
print(f" ID: {campaign_dict.get('id')}")
|
||||||
|
print(f" Name: {campaign_dict.get('name', '❌ MISSING')}")
|
||||||
|
print(f" Keys in dict: {list(campaign_dict.keys())}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rate_limited_fetch():
|
||||||
|
"""Test fetch WITH rate limiter."""
|
||||||
|
print("\n2. RATE LIMITED FETCH (with rate limiter)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
ad_account = AdAccount(account_id)
|
||||||
|
campaigns = await _rate_limited_request(
|
||||||
|
ad_account.get_campaigns,
|
||||||
|
fields=[
|
||||||
|
Campaign.Field.name,
|
||||||
|
Campaign.Field.status,
|
||||||
|
Campaign.Field.objective,
|
||||||
|
],
|
||||||
|
params={'limit': 5}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Type of campaigns: {type(campaigns)}")
|
||||||
|
print(f"Campaigns object: {campaigns}\n")
|
||||||
|
|
||||||
|
for i, campaign in enumerate(campaigns, 1):
|
||||||
|
campaign_dict = dict(campaign)
|
||||||
|
print(f"Campaign {i}:")
|
||||||
|
print(f" ID: {campaign_dict.get('id')}")
|
||||||
|
print(f" Name: {campaign_dict.get('name', '❌ MISSING')}")
|
||||||
|
print(f" Keys in dict: {list(campaign_dict.keys())}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await test_direct_fetch()
|
||||||
|
await test_rate_limited_fetch()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Reference in New Issue
Block a user