Base code
This commit is contained in:
19
backend/services/analytics/handlers/__init__.py
Normal file
19
backend/services/analytics/handlers/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Analytics Handlers Package
|
||||
|
||||
Contains platform-specific analytics handlers.
|
||||
"""
|
||||
|
||||
from .base_handler import BaseAnalyticsHandler
|
||||
from .gsc_handler import GSCAnalyticsHandler
|
||||
from .bing_handler import BingAnalyticsHandler
|
||||
from .wordpress_handler import WordPressAnalyticsHandler
|
||||
from .wix_handler import WixAnalyticsHandler
|
||||
|
||||
__all__ = [
|
||||
'BaseAnalyticsHandler',
|
||||
'GSCAnalyticsHandler',
|
||||
'BingAnalyticsHandler',
|
||||
'WordPressAnalyticsHandler',
|
||||
'WixAnalyticsHandler'
|
||||
]
|
||||
88
backend/services/analytics/handlers/base_handler.py
Normal file
88
backend/services/analytics/handlers/base_handler.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Base Analytics Handler
|
||||
|
||||
Abstract base class for platform-specific analytics handlers.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..models.analytics_data import AnalyticsData
|
||||
from ..models.platform_types import PlatformType
|
||||
|
||||
|
||||
class BaseAnalyticsHandler(ABC):
|
||||
"""Abstract base class for platform analytics handlers"""
|
||||
|
||||
def __init__(self, platform_type: PlatformType):
|
||||
self.platform_type = platform_type
|
||||
self.platform_name = platform_type.value
|
||||
|
||||
@abstractmethod
|
||||
async def get_analytics(self, user_id: str) -> AnalyticsData:
|
||||
"""
|
||||
Get analytics data for the platform
|
||||
|
||||
Args:
|
||||
user_id: User ID to get analytics for
|
||||
|
||||
Returns:
|
||||
AnalyticsData object with platform metrics
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_connection_status(self, user_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get connection status for the platform
|
||||
|
||||
Args:
|
||||
user_id: User ID to check connection for
|
||||
|
||||
Returns:
|
||||
Dictionary with connection status information
|
||||
"""
|
||||
pass
|
||||
|
||||
def create_error_response(self, error_message: str) -> AnalyticsData:
|
||||
"""Create a standardized error response"""
|
||||
return AnalyticsData(
|
||||
platform=self.platform_name,
|
||||
metrics={},
|
||||
date_range={'start': '', 'end': ''},
|
||||
last_updated=datetime.now().isoformat(),
|
||||
status='error',
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
def create_partial_response(self, metrics: Dict[str, Any], error_message: str = None) -> AnalyticsData:
|
||||
"""Create a standardized partial response"""
|
||||
return AnalyticsData(
|
||||
platform=self.platform_name,
|
||||
metrics=metrics,
|
||||
date_range={'start': '', 'end': ''},
|
||||
last_updated=datetime.now().isoformat(),
|
||||
status='partial',
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
def create_success_response(self, metrics: Dict[str, Any], date_range: Dict[str, str] = None) -> AnalyticsData:
|
||||
"""Create a standardized success response"""
|
||||
return AnalyticsData(
|
||||
platform=self.platform_name,
|
||||
metrics=metrics,
|
||||
date_range=date_range or {'start': '', 'end': ''},
|
||||
last_updated=datetime.now().isoformat(),
|
||||
status='success'
|
||||
)
|
||||
|
||||
def log_analytics_request(self, user_id: str, operation: str):
|
||||
"""Log analytics request for monitoring"""
|
||||
from loguru import logger
|
||||
logger.info(f"{self.platform_name} analytics: {operation} for user {user_id}")
|
||||
|
||||
def log_analytics_error(self, user_id: str, operation: str, error: Exception):
|
||||
"""Log analytics error for monitoring"""
|
||||
from loguru import logger
|
||||
logger.error(f"{self.platform_name} analytics: {operation} failed for user {user_id}: {error}")
|
||||
279
backend/services/analytics/handlers/bing_handler.py
Normal file
279
backend/services/analytics/handlers/bing_handler.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Bing Webmaster Tools Analytics Handler
|
||||
|
||||
Handles Bing Webmaster Tools analytics data retrieval and processing.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from loguru import logger
|
||||
|
||||
from services.integrations.bing_oauth import BingOAuthService
|
||||
from ...analytics_cache_service import analytics_cache
|
||||
from ..models.analytics_data import AnalyticsData
|
||||
from ..models.platform_types import PlatformType
|
||||
from .base_handler import BaseAnalyticsHandler
|
||||
from ..insights.bing_insights_service import BingInsightsService
|
||||
from services.bing_analytics_storage_service import BingAnalyticsStorageService
|
||||
import os
|
||||
|
||||
|
||||
class BingAnalyticsHandler(BaseAnalyticsHandler):
|
||||
"""Handler for Bing Webmaster Tools analytics"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(PlatformType.BING)
|
||||
self.bing_service = BingOAuthService()
|
||||
# Initialize insights service
|
||||
database_url = os.getenv('DATABASE_URL', 'sqlite:///./bing_analytics.db')
|
||||
self.insights_service = BingInsightsService(database_url)
|
||||
# Storage service used in onboarding step 5
|
||||
self.storage_service = BingAnalyticsStorageService(os.getenv('DATABASE_URL', 'sqlite:///alwrity.db'))
|
||||
|
||||
async def get_analytics(self, user_id: str) -> AnalyticsData:
|
||||
"""
|
||||
Get Bing Webmaster analytics data using Bing Webmaster API
|
||||
|
||||
Note: Bing Webmaster provides SEO insights and search performance data
|
||||
"""
|
||||
self.log_analytics_request(user_id, "get_analytics")
|
||||
|
||||
# Check cache first - this is an expensive operation
|
||||
cached_data = analytics_cache.get('bing_analytics', user_id)
|
||||
if cached_data:
|
||||
logger.info("Using cached Bing analytics for user {user_id}", user_id=user_id)
|
||||
return AnalyticsData(**cached_data)
|
||||
|
||||
logger.info("Fetching fresh Bing analytics for user {user_id} (expensive operation)", user_id=user_id)
|
||||
try:
|
||||
# Get user's Bing connection status with detailed token info
|
||||
token_status = self.bing_service.get_user_token_status(user_id)
|
||||
|
||||
if not token_status.get('has_active_tokens'):
|
||||
if token_status.get('has_expired_tokens'):
|
||||
return self.create_error_response('Bing Webmaster tokens expired - please reconnect')
|
||||
else:
|
||||
return self.create_error_response('Bing Webmaster not connected')
|
||||
|
||||
# Try once to fetch sites (may return empty if tokens are valid but no verified sites); do not block
|
||||
sites = self.bing_service.get_user_sites(user_id)
|
||||
|
||||
# Get active tokens for access token
|
||||
active_tokens = token_status.get('active_tokens', [])
|
||||
if not active_tokens:
|
||||
return self.create_error_response('No active Bing Webmaster tokens available')
|
||||
|
||||
# Get the first active token's access token
|
||||
token_info = active_tokens[0]
|
||||
access_token = token_info.get('access_token')
|
||||
|
||||
# Cache the sites for future use (even if empty)
|
||||
analytics_cache.set('bing_sites', user_id, sites or [], ttl_override=2*60*60)
|
||||
logger.info(f"Cached Bing sites for analytics for user {user_id} (TTL: 2 hours)")
|
||||
|
||||
if not access_token:
|
||||
return self.create_error_response('Bing Webmaster access token not available')
|
||||
|
||||
# Do NOT call live Bing APIs here; use stored analytics like step 5
|
||||
query_stats = {}
|
||||
try:
|
||||
# If sites available, use first; otherwise ask storage for any stored summary
|
||||
site_url_for_storage = sites[0].get('Url', '') if (sites and isinstance(sites[0], dict)) else None
|
||||
stored = self.storage_service.get_analytics_summary(user_id, site_url_for_storage, days=30)
|
||||
if stored and isinstance(stored, dict):
|
||||
query_stats = {
|
||||
'total_clicks': stored.get('summary', {}).get('total_clicks', 0),
|
||||
'total_impressions': stored.get('summary', {}).get('total_impressions', 0),
|
||||
'total_queries': stored.get('summary', {}).get('total_queries', 0),
|
||||
'avg_ctr': stored.get('summary', {}).get('total_ctr', 0),
|
||||
'avg_position': stored.get('summary', {}).get('avg_position', 0),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Bing analytics: Failed to read stored analytics summary: {e}")
|
||||
|
||||
# Get enhanced insights from database
|
||||
insights = self._get_enhanced_insights(user_id, sites[0].get('Url', '') if sites else '')
|
||||
|
||||
# Extract comprehensive site information with actual metrics
|
||||
metrics = {
|
||||
'connection_status': 'connected',
|
||||
'connected_sites': len(sites),
|
||||
'sites': sites[:5] if sites else [],
|
||||
'connected_since': token_info.get('created_at', ''),
|
||||
'scope': token_info.get('scope', ''),
|
||||
'total_clicks': query_stats.get('total_clicks', 0),
|
||||
'total_impressions': query_stats.get('total_impressions', 0),
|
||||
'total_queries': query_stats.get('total_queries', 0),
|
||||
'avg_ctr': query_stats.get('avg_ctr', 0),
|
||||
'avg_position': query_stats.get('avg_position', 0),
|
||||
'insights': insights,
|
||||
'note': 'Bing Webmaster API provides SEO insights, search performance, and index status data'
|
||||
}
|
||||
|
||||
# If no stored data or no sites, return partial like step 5, else success
|
||||
if (not sites) or (metrics.get('total_impressions', 0) == 0 and metrics.get('total_clicks', 0) == 0):
|
||||
result = self.create_partial_response(metrics=metrics, error_message='Connected to Bing; waiting for stored analytics or site verification')
|
||||
else:
|
||||
result = self.create_success_response(metrics=metrics)
|
||||
|
||||
# Cache the result to avoid expensive API calls
|
||||
analytics_cache.set('bing_analytics', user_id, result.__dict__)
|
||||
logger.info("Cached Bing analytics data for user {user_id}", user_id=user_id)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log_analytics_error(user_id, "get_analytics", e)
|
||||
error_result = self.create_error_response(str(e))
|
||||
|
||||
# Cache error result for shorter time to retry sooner
|
||||
analytics_cache.set('bing_analytics', user_id, error_result.__dict__, ttl_override=300) # 5 minutes
|
||||
return error_result
|
||||
|
||||
def get_connection_status(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get Bing Webmaster connection status"""
|
||||
self.log_analytics_request(user_id, "get_connection_status")
|
||||
|
||||
try:
|
||||
bing_connection = self.bing_service.get_connection_status(user_id)
|
||||
return {
|
||||
'connected': bing_connection.get('connected', False),
|
||||
'sites_count': bing_connection.get('total_sites', 0),
|
||||
'sites': bing_connection.get('sites', []),
|
||||
'error': None
|
||||
}
|
||||
except Exception as e:
|
||||
self.log_analytics_error(user_id, "get_connection_status", e)
|
||||
return {
|
||||
'connected': False,
|
||||
'sites_count': 0,
|
||||
'sites': [],
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _extract_user_sites(self, sites_data: Any) -> list:
|
||||
"""Extract user sites from Bing API response"""
|
||||
if isinstance(sites_data, dict):
|
||||
if 'd' in sites_data:
|
||||
d_data = sites_data['d']
|
||||
if isinstance(d_data, dict) and 'results' in d_data:
|
||||
return d_data['results']
|
||||
elif isinstance(d_data, list):
|
||||
return d_data
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
elif isinstance(sites_data, list):
|
||||
return sites_data
|
||||
else:
|
||||
return []
|
||||
|
||||
async def _get_query_stats(self, user_id: str, sites: list) -> Dict[str, Any]:
|
||||
"""Get query statistics for Bing sites"""
|
||||
query_stats = {}
|
||||
logger.info(f"Bing sites found: {len(sites)} sites")
|
||||
|
||||
if sites:
|
||||
first_site = sites[0]
|
||||
logger.info(f"First Bing site: {first_site}")
|
||||
# Bing API returns URL in 'Url' field (capital U)
|
||||
site_url = first_site.get('Url', '') if isinstance(first_site, dict) else str(first_site)
|
||||
logger.info(f"Extracted site URL: {site_url}")
|
||||
|
||||
if site_url:
|
||||
try:
|
||||
# Use the Bing service method to get query stats
|
||||
logger.info(f"Getting Bing query stats for site: {site_url}")
|
||||
query_data = self.bing_service.get_query_stats(
|
||||
user_id=user_id,
|
||||
site_url=site_url,
|
||||
start_date=(datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'),
|
||||
end_date=datetime.now().strftime('%Y-%m-%d'),
|
||||
page=0
|
||||
)
|
||||
|
||||
if "error" not in query_data:
|
||||
logger.info(f"Bing query stats response structure: {type(query_data)}, keys: {list(query_data.keys()) if isinstance(query_data, dict) else 'Not a dict'}")
|
||||
logger.info(f"Bing query stats raw response: {query_data}")
|
||||
|
||||
# Handle different response structures from Bing API
|
||||
queries = self._extract_queries(query_data)
|
||||
|
||||
logger.info(f"Bing queries extracted: {len(queries)} queries")
|
||||
if queries and len(queries) > 0:
|
||||
logger.info(f"First query sample: {queries[0] if isinstance(queries[0], dict) else queries[0]}")
|
||||
|
||||
# Calculate summary metrics
|
||||
total_clicks = sum(query.get('Clicks', 0) for query in queries if isinstance(query, dict))
|
||||
total_impressions = sum(query.get('Impressions', 0) for query in queries if isinstance(query, dict))
|
||||
total_queries = len(queries)
|
||||
avg_ctr = (total_clicks / total_impressions * 100) if total_impressions > 0 else 0
|
||||
avg_position = sum(query.get('AvgClickPosition', 0) for query in queries if isinstance(query, dict)) / total_queries if total_queries > 0 else 0
|
||||
|
||||
query_stats = {
|
||||
'total_clicks': total_clicks,
|
||||
'total_impressions': total_impressions,
|
||||
'total_queries': total_queries,
|
||||
'avg_ctr': round(avg_ctr, 2),
|
||||
'avg_position': round(avg_position, 2)
|
||||
}
|
||||
|
||||
logger.info(f"Bing query stats calculated: {query_stats}")
|
||||
else:
|
||||
logger.warning(f"Bing query stats error: {query_data['error']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting Bing query stats: {e}")
|
||||
|
||||
return query_stats
|
||||
|
||||
def _extract_queries(self, query_data: Any) -> list:
|
||||
"""Extract queries from Bing API response"""
|
||||
if isinstance(query_data, dict):
|
||||
if 'd' in query_data:
|
||||
d_data = query_data['d']
|
||||
logger.info(f"Bing 'd' data structure: {type(d_data)}, keys: {list(d_data.keys()) if isinstance(d_data, dict) else 'Not a dict'}")
|
||||
if isinstance(d_data, dict) and 'results' in d_data:
|
||||
return d_data['results']
|
||||
elif isinstance(d_data, list):
|
||||
return d_data
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
return []
|
||||
elif isinstance(query_data, list):
|
||||
return query_data
|
||||
else:
|
||||
return []
|
||||
|
||||
def _get_enhanced_insights(self, user_id: str, site_url: str) -> Dict[str, Any]:
|
||||
"""Get enhanced insights from stored Bing analytics data"""
|
||||
try:
|
||||
if not site_url:
|
||||
return {'status': 'no_site_url', 'message': 'No site URL available for insights'}
|
||||
|
||||
# Get performance insights
|
||||
performance_insights = self.insights_service.get_performance_insights(user_id, site_url, days=30)
|
||||
|
||||
# Get SEO insights
|
||||
seo_insights = self.insights_service.get_seo_insights(user_id, site_url, days=30)
|
||||
|
||||
# Get actionable recommendations
|
||||
recommendations = self.insights_service.get_actionable_recommendations(user_id, site_url, days=30)
|
||||
|
||||
return {
|
||||
'performance': performance_insights,
|
||||
'seo': seo_insights,
|
||||
'recommendations': recommendations,
|
||||
'last_analyzed': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting enhanced insights: {e}")
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Unable to generate insights: {str(e)}',
|
||||
'fallback': True
|
||||
}
|
||||
255
backend/services/analytics/handlers/gsc_handler.py
Normal file
255
backend/services/analytics/handlers/gsc_handler.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Google Search Console Analytics Handler
|
||||
|
||||
Handles GSC analytics data retrieval and processing.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from loguru import logger
|
||||
|
||||
from services.gsc_service import GSCService
|
||||
from ...analytics_cache_service import analytics_cache
|
||||
from ..models.analytics_data import AnalyticsData
|
||||
from ..models.platform_types import PlatformType
|
||||
from .base_handler import BaseAnalyticsHandler
|
||||
|
||||
|
||||
class GSCAnalyticsHandler(BaseAnalyticsHandler):
|
||||
"""Handler for Google Search Console analytics"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(PlatformType.GSC)
|
||||
self.gsc_service = GSCService()
|
||||
|
||||
async def get_analytics(self, user_id: str) -> AnalyticsData:
|
||||
"""
|
||||
Get Google Search Console analytics data with caching
|
||||
|
||||
Returns comprehensive SEO metrics including clicks, impressions, CTR, and position data.
|
||||
"""
|
||||
self.log_analytics_request(user_id, "get_analytics")
|
||||
|
||||
# Check cache first - GSC API calls can be expensive
|
||||
cached_data = analytics_cache.get('gsc_analytics', user_id)
|
||||
if cached_data:
|
||||
logger.info("Using cached GSC analytics for user {user_id}", user_id=user_id)
|
||||
return AnalyticsData(**cached_data)
|
||||
|
||||
logger.info("Fetching fresh GSC analytics for user {user_id}", user_id=user_id)
|
||||
try:
|
||||
# Get user's sites
|
||||
sites = self.gsc_service.get_site_list(user_id)
|
||||
logger.info(f"GSC Sites found for user {user_id}: {sites}")
|
||||
if not sites:
|
||||
logger.warning(f"No GSC sites found for user {user_id}")
|
||||
return self.create_error_response('No GSC sites found')
|
||||
|
||||
# Get analytics for the first site (or combine all sites)
|
||||
site_url = sites[0]['siteUrl']
|
||||
logger.info(f"Using GSC site URL: {site_url}")
|
||||
|
||||
# Get search analytics for last 30 days
|
||||
end_date = datetime.now().strftime('%Y-%m-%d')
|
||||
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
||||
logger.info(f"GSC Date range: {start_date} to {end_date}")
|
||||
|
||||
search_analytics = self.gsc_service.get_search_analytics(
|
||||
user_id=user_id,
|
||||
site_url=site_url,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
logger.info(f"GSC Search analytics retrieved for user {user_id}")
|
||||
|
||||
# Process GSC data into standardized format
|
||||
processed_metrics = self._process_gsc_metrics(search_analytics)
|
||||
|
||||
result = self.create_success_response(
|
||||
metrics=processed_metrics,
|
||||
date_range={'start': start_date, 'end': end_date}
|
||||
)
|
||||
|
||||
# Cache the result to avoid expensive API calls
|
||||
analytics_cache.set('gsc_analytics', user_id, result.__dict__)
|
||||
logger.info("Cached GSC analytics data for user {user_id}", user_id=user_id)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.log_analytics_error(user_id, "get_analytics", e)
|
||||
error_result = self.create_error_response(str(e))
|
||||
|
||||
# Cache error result for shorter time to retry sooner
|
||||
analytics_cache.set('gsc_analytics', user_id, error_result.__dict__, ttl_override=300) # 5 minutes
|
||||
return error_result
|
||||
|
||||
def get_connection_status(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get GSC connection status"""
|
||||
self.log_analytics_request(user_id, "get_connection_status")
|
||||
|
||||
try:
|
||||
sites = self.gsc_service.get_site_list(user_id)
|
||||
return {
|
||||
'connected': len(sites) > 0,
|
||||
'sites_count': len(sites),
|
||||
'sites': sites[:3] if sites else [], # Show first 3 sites
|
||||
'error': None
|
||||
}
|
||||
except Exception as e:
|
||||
self.log_analytics_error(user_id, "get_connection_status", e)
|
||||
return {
|
||||
'connected': False,
|
||||
'sites_count': 0,
|
||||
'sites': [],
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _process_gsc_metrics(self, search_analytics: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Process GSC raw data into standardized metrics"""
|
||||
try:
|
||||
# Debug: Log the raw search analytics data structure
|
||||
logger.info(f"GSC Raw search analytics structure: {search_analytics}")
|
||||
logger.info(f"GSC Raw search analytics keys: {list(search_analytics.keys())}")
|
||||
|
||||
# Handle new data structure with overall_metrics and query_data
|
||||
if 'overall_metrics' in search_analytics:
|
||||
# New structure from updated GSC service
|
||||
overall_rows = search_analytics.get('overall_metrics', {}).get('rows', [])
|
||||
query_rows = search_analytics.get('query_data', {}).get('rows', [])
|
||||
verification_rows = search_analytics.get('verification_data', {}).get('rows', [])
|
||||
|
||||
logger.info(f"GSC Overall metrics rows: {len(overall_rows)}")
|
||||
logger.info(f"GSC Query data rows: {len(query_rows)}")
|
||||
logger.info(f"GSC Verification rows: {len(verification_rows)}")
|
||||
|
||||
if overall_rows:
|
||||
logger.info(f"GSC Overall first row: {overall_rows[0]}")
|
||||
if query_rows:
|
||||
logger.info(f"GSC Query first row: {query_rows[0]}")
|
||||
|
||||
# Use query_rows for detailed insights, overall_rows for summary
|
||||
rows = query_rows if query_rows else overall_rows
|
||||
else:
|
||||
# Legacy structure
|
||||
rows = search_analytics.get('rows', [])
|
||||
logger.info(f"GSC Legacy rows count: {len(rows)}")
|
||||
if rows:
|
||||
logger.info(f"GSC Legacy first row structure: {rows[0]}")
|
||||
logger.info(f"GSC Legacy first row keys: {list(rows[0].keys()) if rows[0] else 'No rows'}")
|
||||
|
||||
# Calculate summary metrics - handle different response formats
|
||||
total_clicks = 0
|
||||
total_impressions = 0
|
||||
total_position = 0
|
||||
valid_rows = 0
|
||||
|
||||
for row in rows:
|
||||
# Handle different possible response formats
|
||||
clicks = row.get('clicks', 0)
|
||||
impressions = row.get('impressions', 0)
|
||||
position = row.get('position', 0)
|
||||
|
||||
# If position is 0 or None, skip it from average calculation
|
||||
if position and position > 0:
|
||||
total_position += position
|
||||
valid_rows += 1
|
||||
|
||||
total_clicks += clicks
|
||||
total_impressions += impressions
|
||||
|
||||
avg_ctr = (total_clicks / total_impressions * 100) if total_impressions > 0 else 0
|
||||
avg_position = total_position / valid_rows if valid_rows > 0 else 0
|
||||
|
||||
logger.info(f"GSC Calculated metrics - clicks: {total_clicks}, impressions: {total_impressions}, ctr: {avg_ctr}, position: {avg_position}, valid_rows: {valid_rows}")
|
||||
|
||||
# Get top performing queries - handle different data structures
|
||||
if rows and 'keys' in rows[0]:
|
||||
# New GSC API format with keys array
|
||||
top_queries = sorted(rows, key=lambda x: x.get('clicks', 0), reverse=True)[:10]
|
||||
|
||||
# Get top performing pages (if we have page data)
|
||||
page_data = {}
|
||||
for row in rows:
|
||||
# Handle different key structures
|
||||
keys = row.get('keys', [])
|
||||
if len(keys) > 1 and keys[1]: # Page data available
|
||||
page = keys[1].get('keys', ['Unknown'])[0] if isinstance(keys[1], dict) else str(keys[1])
|
||||
else:
|
||||
page = 'Unknown'
|
||||
|
||||
if page not in page_data:
|
||||
page_data[page] = {'clicks': 0, 'impressions': 0, 'ctr': 0, 'position': 0}
|
||||
page_data[page]['clicks'] += row.get('clicks', 0)
|
||||
page_data[page]['impressions'] += row.get('impressions', 0)
|
||||
else:
|
||||
# Legacy format or no keys structure
|
||||
top_queries = sorted(rows, key=lambda x: x.get('clicks', 0), reverse=True)[:10]
|
||||
page_data = {}
|
||||
|
||||
# Calculate page metrics
|
||||
for page in page_data:
|
||||
if page_data[page]['impressions'] > 0:
|
||||
page_data[page]['ctr'] = page_data[page]['clicks'] / page_data[page]['impressions'] * 100
|
||||
|
||||
top_pages = sorted(page_data.items(), key=lambda x: x[1]['clicks'], reverse=True)[:10]
|
||||
|
||||
return {
|
||||
'connection_status': 'connected',
|
||||
'connected_sites': 1, # GSC typically has one site per user
|
||||
'total_clicks': total_clicks,
|
||||
'total_impressions': total_impressions,
|
||||
'avg_ctr': round(avg_ctr, 2),
|
||||
'avg_position': round(avg_position, 2),
|
||||
'total_queries': len(rows),
|
||||
'top_queries': [
|
||||
{
|
||||
'query': self._extract_query_from_row(row),
|
||||
'clicks': row.get('clicks', 0),
|
||||
'impressions': row.get('impressions', 0),
|
||||
'ctr': round(row.get('ctr', 0) * 100, 2),
|
||||
'position': round(row.get('position', 0), 2)
|
||||
}
|
||||
for row in top_queries
|
||||
],
|
||||
'top_pages': [
|
||||
{
|
||||
'page': page,
|
||||
'clicks': data['clicks'],
|
||||
'impressions': data['impressions'],
|
||||
'ctr': round(data['ctr'], 2)
|
||||
}
|
||||
for page, data in top_pages
|
||||
],
|
||||
'note': 'Google Search Console provides search performance data, keyword rankings, and SEO insights'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing GSC metrics: {e}")
|
||||
return {
|
||||
'connection_status': 'error',
|
||||
'connected_sites': 0,
|
||||
'total_clicks': 0,
|
||||
'total_impressions': 0,
|
||||
'avg_ctr': 0,
|
||||
'avg_position': 0,
|
||||
'total_queries': 0,
|
||||
'top_queries': [],
|
||||
'top_pages': [],
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _extract_query_from_row(self, row: Dict[str, Any]) -> str:
|
||||
"""Extract query text from GSC API row data"""
|
||||
try:
|
||||
keys = row.get('keys', [])
|
||||
if keys and len(keys) > 0:
|
||||
first_key = keys[0]
|
||||
if isinstance(first_key, dict):
|
||||
return first_key.get('keys', ['Unknown'])[0]
|
||||
else:
|
||||
return str(first_key)
|
||||
return 'Unknown'
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting query from row: {e}")
|
||||
return 'Unknown'
|
||||
71
backend/services/analytics/handlers/wix_handler.py
Normal file
71
backend/services/analytics/handlers/wix_handler.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Wix Analytics Handler
|
||||
|
||||
Handles Wix analytics data retrieval and processing.
|
||||
Note: This is currently a placeholder implementation.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from loguru import logger
|
||||
|
||||
from services.wix_service import WixService
|
||||
from ..models.analytics_data import AnalyticsData
|
||||
from ..models.platform_types import PlatformType
|
||||
from .base_handler import BaseAnalyticsHandler
|
||||
|
||||
|
||||
class WixAnalyticsHandler(BaseAnalyticsHandler):
|
||||
"""Handler for Wix analytics"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(PlatformType.WIX)
|
||||
self.wix_service = WixService()
|
||||
|
||||
async def get_analytics(self, user_id: str) -> AnalyticsData:
|
||||
"""
|
||||
Get Wix analytics data using the Business Management API
|
||||
|
||||
Note: This requires the Wix Business Management API which may need additional permissions
|
||||
"""
|
||||
self.log_analytics_request(user_id, "get_analytics")
|
||||
|
||||
try:
|
||||
# TODO: Implement Wix analytics retrieval
|
||||
# This would require:
|
||||
# 1. Storing Wix access tokens in database
|
||||
# 2. Using Wix Business Management API
|
||||
# 3. Requesting analytics permissions during OAuth
|
||||
|
||||
# For now, return a placeholder response
|
||||
return self.create_partial_response(
|
||||
metrics={
|
||||
'connection_status': 'not_implemented',
|
||||
'connected_sites': 0,
|
||||
'page_views': 0,
|
||||
'visitors': 0,
|
||||
'bounce_rate': 0,
|
||||
'avg_session_duration': 0,
|
||||
'top_pages': [],
|
||||
'traffic_sources': {},
|
||||
'device_breakdown': {},
|
||||
'geo_distribution': {},
|
||||
'note': 'Wix analytics integration coming soon'
|
||||
},
|
||||
error_message='Wix analytics integration coming soon'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.log_analytics_error(user_id, "get_analytics", e)
|
||||
return self.create_error_response(str(e))
|
||||
|
||||
def get_connection_status(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get Wix connection status"""
|
||||
self.log_analytics_request(user_id, "get_connection_status")
|
||||
|
||||
# TODO: Implement actual Wix connection check
|
||||
return {
|
||||
'connected': False, # TODO: Implement actual Wix connection check
|
||||
'sites_count': 0,
|
||||
'sites': [],
|
||||
'error': 'Wix connection check not implemented'
|
||||
}
|
||||
119
backend/services/analytics/handlers/wordpress_handler.py
Normal file
119
backend/services/analytics/handlers/wordpress_handler.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
WordPress.com Analytics Handler
|
||||
|
||||
Handles WordPress.com analytics data retrieval and processing.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
from services.integrations.wordpress_oauth import WordPressOAuthService
|
||||
from ..models.analytics_data import AnalyticsData
|
||||
from ..models.platform_types import PlatformType
|
||||
from .base_handler import BaseAnalyticsHandler
|
||||
|
||||
|
||||
class WordPressAnalyticsHandler(BaseAnalyticsHandler):
|
||||
"""Handler for WordPress.com analytics"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(PlatformType.WORDPRESS)
|
||||
self.wordpress_service = WordPressOAuthService()
|
||||
|
||||
async def get_analytics(self, user_id: str) -> AnalyticsData:
|
||||
"""
|
||||
Get WordPress analytics data using WordPress.com REST API
|
||||
|
||||
Note: WordPress.com has limited analytics API access
|
||||
We'll try to get basic site stats and post data
|
||||
"""
|
||||
self.log_analytics_request(user_id, "get_analytics")
|
||||
|
||||
try:
|
||||
# Get user's WordPress tokens
|
||||
connection_status = self.wordpress_service.get_connection_status(user_id)
|
||||
|
||||
if not connection_status.get('connected'):
|
||||
return self.create_error_response('WordPress not connected')
|
||||
|
||||
# Get the first connected site
|
||||
sites = connection_status.get('sites', [])
|
||||
if not sites:
|
||||
return self.create_error_response('No WordPress sites found')
|
||||
|
||||
site = sites[0]
|
||||
access_token = site.get('access_token')
|
||||
blog_id = site.get('blog_id')
|
||||
|
||||
if not access_token or not blog_id:
|
||||
return self.create_error_response('WordPress access token not available')
|
||||
|
||||
# Try to get basic site stats from WordPress.com API
|
||||
headers = {
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
'User-Agent': 'ALwrity/1.0'
|
||||
}
|
||||
|
||||
# Get site info and basic stats
|
||||
site_info_url = f"https://public-api.wordpress.com/rest/v1.1/sites/{blog_id}"
|
||||
response = requests.get(site_info_url, headers=headers, timeout=10)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"WordPress API call failed: {response.status_code}")
|
||||
# Return basic connection info instead of full analytics
|
||||
return self.create_partial_response(
|
||||
metrics={
|
||||
'site_name': site.get('blog_url', 'Unknown'),
|
||||
'connection_status': 'connected',
|
||||
'blog_id': blog_id,
|
||||
'connected_since': site.get('created_at', ''),
|
||||
'note': 'WordPress.com API has limited analytics access'
|
||||
},
|
||||
error_message='WordPress.com API has limited analytics access'
|
||||
)
|
||||
|
||||
site_data = response.json()
|
||||
|
||||
# Extract basic site information
|
||||
metrics = {
|
||||
'site_name': site_data.get('name', 'Unknown'),
|
||||
'site_url': site_data.get('URL', ''),
|
||||
'blog_id': blog_id,
|
||||
'language': site_data.get('lang', ''),
|
||||
'timezone': site_data.get('timezone', ''),
|
||||
'is_private': site_data.get('is_private', False),
|
||||
'is_coming_soon': site_data.get('is_coming_soon', False),
|
||||
'connected_since': site.get('created_at', ''),
|
||||
'connection_status': 'connected',
|
||||
'connected_sites': len(sites),
|
||||
'note': 'WordPress.com API has limited analytics access. For detailed analytics, consider integrating with Google Analytics or Jetpack Stats.'
|
||||
}
|
||||
|
||||
return self.create_success_response(metrics=metrics)
|
||||
|
||||
except Exception as e:
|
||||
self.log_analytics_error(user_id, "get_analytics", e)
|
||||
return self.create_error_response(str(e))
|
||||
|
||||
def get_connection_status(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get WordPress.com connection status"""
|
||||
self.log_analytics_request(user_id, "get_connection_status")
|
||||
|
||||
try:
|
||||
wp_connection = self.wordpress_service.get_connection_status(user_id)
|
||||
return {
|
||||
'connected': wp_connection.get('connected', False),
|
||||
'sites_count': wp_connection.get('total_sites', 0),
|
||||
'sites': wp_connection.get('sites', []),
|
||||
'error': None
|
||||
}
|
||||
except Exception as e:
|
||||
self.log_analytics_error(user_id, "get_connection_status", e)
|
||||
return {
|
||||
'connected': False,
|
||||
'sites_count': 0,
|
||||
'sites': [],
|
||||
'error': str(e)
|
||||
}
|
||||
Reference in New Issue
Block a user