From 845a130aadfec48c9e1957d687c6aa6ff3122f38 Mon Sep 17 00:00:00 2001 From: Jonas Linter Date: Tue, 28 Oct 2025 15:25:42 +0100 Subject: [PATCH] Added some logging --- src/meta_api_grabber/scheduled_grabber.py | 25 +++- .../test_campaign_insights.py | 91 ++++++++++++++ src/meta_api_grabber/test_campaigns.py | 118 ++++++++++++++++++ .../test_rate_limiter_issue.py | 108 ++++++++++++++++ 4 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 src/meta_api_grabber/test_campaign_insights.py create mode 100644 src/meta_api_grabber/test_campaigns.py create mode 100644 src/meta_api_grabber/test_rate_limiter_issue.py diff --git a/src/meta_api_grabber/scheduled_grabber.py b/src/meta_api_grabber/scheduled_grabber.py index 50a3a36..4a4cb2a 100644 --- a/src/meta_api_grabber/scheduled_grabber.py +++ b/src/meta_api_grabber/scheduled_grabber.py @@ -4,6 +4,7 @@ Runs periodically to build time-series data for dashboards. """ import asyncio +import logging import os from datetime import datetime, timedelta, timezone, date from typing import Optional, Dict @@ -20,6 +21,9 @@ from .database import TimescaleDBClient from .rate_limiter import MetaRateLimiter from .token_manager import MetaTokenManager +# Set up logger +logger = logging.getLogger(__name__) + class ScheduledInsightsGrabber: """ @@ -340,11 +344,19 @@ class ScheduledInsightsGrabber: for campaign in campaigns: campaign_id = campaign['id'] 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 if not campaign_name: 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( campaign_id=campaign_id, @@ -358,6 +370,7 @@ class ScheduledInsightsGrabber: print(f" {count} campaigns cached for {account_id}") if 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): """ @@ -1201,6 +1214,16 @@ class ScheduledInsightsGrabber: async def async_main(): """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: # Initialize with max_accounts=3 for conservative start # Set max_accounts=None to process all accessible accounts diff --git a/src/meta_api_grabber/test_campaign_insights.py b/src/meta_api_grabber/test_campaign_insights.py new file mode 100644 index 0000000..c556743 --- /dev/null +++ b/src/meta_api_grabber/test_campaign_insights.py @@ -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!") diff --git a/src/meta_api_grabber/test_campaigns.py b/src/meta_api_grabber/test_campaigns.py new file mode 100644 index 0000000..f226aa4 --- /dev/null +++ b/src/meta_api_grabber/test_campaigns.py @@ -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!") diff --git a/src/meta_api_grabber/test_rate_limiter_issue.py b/src/meta_api_grabber/test_rate_limiter_issue.py new file mode 100644 index 0000000..49eea8b --- /dev/null +++ b/src/meta_api_grabber/test_rate_limiter_issue.py @@ -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())