Bing Analytics and Insights added, background jobs added, database setup updated, environment setup updated, frontend updated, backend updated.
Onboarding Manager and Router Manager refactored, analytics and background jobs added, database setup updated, environment setup updated, frontend updated, backend updated. Critical onboarding database migration implemented.
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}")
|
||||
265
backend/services/analytics/handlers/bing_handler.py
Normal file
265
backend/services/analytics/handlers/bing_handler.py
Normal file
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
connection_status = self.bing_service.get_connection_status(user_id)
|
||||
|
||||
if not connection_status.get('connected'):
|
||||
return self.create_error_response('Bing Webmaster not connected')
|
||||
|
||||
# Get the first connected site token info
|
||||
token_sites = connection_status.get('sites', [])
|
||||
if not token_sites:
|
||||
return self.create_error_response('No Bing Webmaster sites found')
|
||||
|
||||
# Get the first token's access token
|
||||
token_info = token_sites[0]
|
||||
access_token = token_info.get('access_token')
|
||||
|
||||
# Get the actual site URLs from Bing API when needed for analytics
|
||||
# Check cache first for sites data
|
||||
cached_sites = analytics_cache.get('bing_sites', user_id)
|
||||
if cached_sites:
|
||||
logger.info(f"Using cached Bing sites for analytics for user {user_id}")
|
||||
sites = cached_sites
|
||||
else:
|
||||
# Fetch sites from API and cache them
|
||||
logger.info(f"Fetching fresh Bing sites for analytics for user {user_id}")
|
||||
sites = self.bing_service.get_user_sites(user_id)
|
||||
if not sites:
|
||||
return self.create_error_response('No site URLs found in Bing Webmaster API')
|
||||
|
||||
# Cache the sites for future use
|
||||
analytics_cache.set('bing_sites', user_id, sites, 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')
|
||||
|
||||
# Get actual query stats for the first site using the Bing service
|
||||
query_stats = await self._get_query_stats(user_id, sites)
|
||||
|
||||
# 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'
|
||||
}
|
||||
|
||||
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