nordabiz/gbp_performance_service.py
Maciej Pienczyn e8b7f2214f
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
feat(api): Expand Google API coverage to ~100% (GBP Performance + GSC extensions)
Add GBP Performance API integration for visibility metrics (Maps/Search
impressions, call/website clicks, direction requests, search keywords).
Extend Search Console with URL Inspection, Sitemaps, device/country/type
breakdowns, and period-over-period trend comparison. Change OAuth scope
from webmasters.readonly to webmasters for URL Inspection support.

Migration 064 adds 24 new columns to company_website_analysis.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 12:05:08 +01:00

195 lines
6.7 KiB
Python

"""
Google Business Profile Performance API Client
===============================================
Uses OAuth 2.0 to fetch visibility and interaction metrics from GBP.
Provides daily metrics time series and monthly search keyword impressions.
API docs: https://developers.google.com/my-business/reference/performance/rest
"""
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import requests
logger = logging.getLogger(__name__)
# Metrics to request from getDailyMetricsTimeSeries
DAILY_METRICS = [
'BUSINESS_IMPRESSIONS_DESKTOP_MAPS',
'BUSINESS_IMPRESSIONS_MOBILE_MAPS',
'BUSINESS_IMPRESSIONS_DESKTOP_SEARCH',
'BUSINESS_IMPRESSIONS_MOBILE_SEARCH',
'CALL_CLICKS',
'WEBSITE_CLICKS',
'BUSINESS_DIRECTION_REQUESTS',
'BUSINESS_CONVERSATIONS',
'BUSINESS_BOOKINGS',
]
class GBPPerformanceService:
"""Google Business Profile Performance API client."""
BASE_URL = "https://businessprofileperformance.googleapis.com/v1"
def __init__(self, access_token: str):
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
})
self.session.timeout = 20
def get_daily_metrics(self, location_name: str, days: int = 30) -> Dict:
"""Get aggregated daily metrics for a location.
Args:
location_name: Full location resource name (e.g., 'locations/123456')
days: Number of days to aggregate (default 30)
Returns:
Dict with aggregated metrics:
{
'maps_impressions': int,
'search_impressions': int,
'call_clicks': int,
'website_clicks': int,
'direction_requests': int,
'conversations': int,
'period_days': int,
}
"""
result = {
'maps_impressions': 0,
'search_impressions': 0,
'call_clicks': 0,
'website_clicks': 0,
'direction_requests': 0,
'conversations': 0,
'period_days': days,
}
end_date = datetime.now() - timedelta(days=1) # Yesterday (today may be incomplete)
start_date = end_date - timedelta(days=days)
# Normalize location_name format
if not location_name.startswith('locations/'):
location_name = f'locations/{location_name}'
for metric in DAILY_METRICS:
try:
resp = self.session.get(
f"{self.BASE_URL}/{location_name}:getDailyMetricsTimeSeries",
params={
'dailyMetric': metric,
'dailyRange.startDate.year': start_date.year,
'dailyRange.startDate.month': start_date.month,
'dailyRange.startDate.day': start_date.day,
'dailyRange.endDate.year': end_date.year,
'dailyRange.endDate.month': end_date.month,
'dailyRange.endDate.day': end_date.day,
}
)
if resp.status_code != 200:
logger.debug(f"Performance API metric {metric} returned {resp.status_code}")
continue
data = resp.json()
time_series = data.get('timeSeries', {})
daily_values = time_series.get('datedValues', [])
total = sum(
dv.get('value', 0) for dv in daily_values
if dv.get('value') is not None
)
# Map metric to result key
if 'MAPS' in metric:
result['maps_impressions'] += total
elif 'SEARCH' in metric:
result['search_impressions'] += total
elif metric == 'CALL_CLICKS':
result['call_clicks'] = total
elif metric == 'WEBSITE_CLICKS':
result['website_clicks'] = total
elif metric == 'BUSINESS_DIRECTION_REQUESTS':
result['direction_requests'] = total
elif metric == 'BUSINESS_CONVERSATIONS':
result['conversations'] = total
except Exception as e:
logger.debug(f"Performance API metric {metric} failed: {e}")
continue
logger.info(
f"GBP Performance for {location_name}: "
f"maps={result['maps_impressions']}, search={result['search_impressions']}, "
f"calls={result['call_clicks']}, web={result['website_clicks']}"
)
return result
def get_search_keywords(self, location_name: str) -> List[Dict]:
"""Get monthly search keyword impressions.
Args:
location_name: Full location resource name
Returns:
List of dicts: [{'keyword': str, 'impressions': int}, ...]
"""
# Normalize location_name format
if not location_name.startswith('locations/'):
location_name = f'locations/{location_name}'
try:
resp = self.session.get(
f"{self.BASE_URL}/{location_name}/searchkeywords/impressions/monthly",
params={'pageSize': 20}
)
if resp.status_code != 200:
logger.debug(f"Search keywords API returned {resp.status_code}")
return []
data = resp.json()
keywords = []
for item in data.get('searchKeywordsCounts', []):
keyword = item.get('searchKeyword', '')
# Get the most recent month's data
monthly = item.get('insightsValue', {})
impressions = monthly.get('value', 0)
if keyword:
keywords.append({
'keyword': keyword,
'impressions': impressions,
})
# Sort by impressions descending
keywords.sort(key=lambda x: x['impressions'], reverse=True)
logger.info(f"GBP search keywords for {location_name}: {len(keywords)} keywords")
return keywords[:20]
except Exception as e:
logger.warning(f"GBP search keywords failed for {location_name}: {e}")
return []
def get_all_performance_data(self, location_name: str, days: int = 30) -> Dict:
"""Get all performance data (metrics + keywords) in one call.
Returns:
Dict with all performance data combined.
"""
metrics = self.get_daily_metrics(location_name, days)
keywords = self.get_search_keywords(location_name)
metrics['search_keywords'] = keywords
return metrics