Base code
This commit is contained in:
397
backend/services/seo/dashboard_service.py
Normal file
397
backend/services/seo/dashboard_service.py
Normal file
@@ -0,0 +1,397 @@
|
||||
"""
|
||||
SEO Dashboard Service
|
||||
|
||||
Main orchestration service that coordinates data fetching from GSC, Bing,
|
||||
and other analytics sources for the SEO dashboard. Leverages existing
|
||||
OAuth connections from onboarding step 5.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from loguru import logger
|
||||
|
||||
from utils.logger_utils import get_service_logger
|
||||
from services.gsc_service import GSCService
|
||||
from services.integrations.bing_oauth import BingOAuthService
|
||||
from services.bing_analytics_storage_service import BingAnalyticsStorageService
|
||||
from services.analytics_cache_service import AnalyticsCacheService
|
||||
from services.onboarding.data_service import OnboardingDataService
|
||||
from .analytics_aggregator import AnalyticsAggregator
|
||||
from .competitive_analyzer import CompetitiveAnalyzer
|
||||
|
||||
logger = get_service_logger("seo_dashboard")
|
||||
|
||||
class SEODashboardService:
|
||||
"""Main service for SEO dashboard data orchestration."""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
"""Initialize the SEO dashboard service."""
|
||||
self.db = db
|
||||
self.gsc_service = GSCService()
|
||||
self.bing_oauth = BingOAuthService()
|
||||
self.bing_storage = BingAnalyticsStorageService("sqlite:///alwrity.db")
|
||||
self.analytics_cache = AnalyticsCacheService()
|
||||
self.user_data_service = OnboardingDataService(db)
|
||||
self.analytics_aggregator = AnalyticsAggregator()
|
||||
self.competitive_analyzer = CompetitiveAnalyzer(db)
|
||||
|
||||
async def get_platform_status(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get connection status for GSC and Bing platforms."""
|
||||
try:
|
||||
# Check GSC connection
|
||||
gsc_credentials = self.gsc_service.load_user_credentials(user_id)
|
||||
gsc_connected = gsc_credentials is not None
|
||||
|
||||
# Check Bing connection with detailed status
|
||||
bing_token_status = self.bing_oauth.get_user_token_status(user_id)
|
||||
bing_connected = bing_token_status.get('has_active_tokens', False)
|
||||
|
||||
# Get cached data for last sync info
|
||||
gsc_data = self.analytics_cache.get('gsc_analytics', user_id)
|
||||
bing_data = self.analytics_cache.get('bing_analytics', user_id)
|
||||
|
||||
return {
|
||||
"gsc": {
|
||||
"connected": gsc_connected,
|
||||
"sites": self._get_gsc_sites(user_id) if gsc_connected else [],
|
||||
"last_sync": gsc_data.get('last_updated') if gsc_data else None,
|
||||
"status": "connected" if gsc_connected else "disconnected"
|
||||
},
|
||||
"bing": {
|
||||
"connected": bing_connected,
|
||||
"sites": self._get_bing_sites(user_id) if bing_connected else [],
|
||||
"last_sync": bing_data.get('last_updated') if bing_data else None,
|
||||
"status": "connected" if bing_connected else ("expired" if bing_token_status.get('has_expired_tokens') else "disconnected"),
|
||||
"has_expired_tokens": bing_token_status.get('has_expired_tokens', False),
|
||||
"last_token_date": bing_token_status.get('last_token_date'),
|
||||
"total_tokens": bing_token_status.get('total_tokens', 0)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting platform status for user {user_id}: {e}")
|
||||
return {
|
||||
"gsc": {"connected": False, "sites": [], "last_sync": None, "status": "error"},
|
||||
"bing": {"connected": False, "sites": [], "last_sync": None, "status": "error"}
|
||||
}
|
||||
|
||||
async def get_dashboard_overview(self, user_id: str, site_url: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Get comprehensive dashboard overview with real GSC/Bing data."""
|
||||
try:
|
||||
# Get user's website URL if not provided
|
||||
if not site_url:
|
||||
# Try to get from website analysis first
|
||||
website_analysis = self.user_data_service.get_user_website_analysis(int(user_id))
|
||||
if website_analysis and website_analysis.get('website_url'):
|
||||
site_url = website_analysis['website_url']
|
||||
else:
|
||||
# Fallback: try to get from Bing sites
|
||||
bing_sites = self._get_bing_sites(user_id)
|
||||
if bing_sites:
|
||||
site_url = bing_sites[0] # Use first Bing site
|
||||
else:
|
||||
site_url = 'https://alwrity.com' # Default fallback
|
||||
|
||||
# Get platform status
|
||||
platform_status = await self.get_platform_status(user_id)
|
||||
|
||||
# Get analytics data
|
||||
gsc_data = await self.get_gsc_data(user_id, site_url)
|
||||
bing_data = await self.get_bing_data(user_id, site_url)
|
||||
|
||||
# Aggregate metrics
|
||||
summary = self.analytics_aggregator.combine_metrics(gsc_data, bing_data)
|
||||
timeseries = self.analytics_aggregator.normalize_timeseries(
|
||||
gsc_data.get("timeseries", []),
|
||||
bing_data.get("timeseries", [])
|
||||
)
|
||||
|
||||
# Get competitive insights
|
||||
competitor_insights = await self.competitive_analyzer.get_competitive_insights(user_id)
|
||||
|
||||
# Calculate health score
|
||||
health_score = self._calculate_health_score(summary, platform_status)
|
||||
|
||||
# Generate AI insights
|
||||
ai_insights = await self._generate_ai_insights(summary, timeseries, competitor_insights)
|
||||
|
||||
return {
|
||||
"website_url": site_url,
|
||||
"platforms": platform_status,
|
||||
"summary": summary,
|
||||
"timeseries": timeseries,
|
||||
"competitor_insights": competitor_insights,
|
||||
"health_score": health_score,
|
||||
"ai_insights": ai_insights,
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting dashboard overview for user {user_id}: {e}")
|
||||
raise
|
||||
|
||||
async def get_gsc_data(self, user_id: str, site_url: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Get GSC data for the specified site."""
|
||||
try:
|
||||
# Check if user has GSC credentials
|
||||
credentials = self.gsc_service.load_user_credentials(user_id)
|
||||
if not credentials:
|
||||
return {"error": "GSC not connected", "data": [], "status": "disconnected"}
|
||||
|
||||
# Try to get from cache first
|
||||
cache_key = f"gsc_analytics:{user_id}:{site_url or 'default'}"
|
||||
cached_data = self.analytics_cache.get('gsc_analytics', user_id, site_url=site_url or 'default')
|
||||
if cached_data:
|
||||
return cached_data
|
||||
|
||||
# Fetch fresh data from GSC API
|
||||
if site_url:
|
||||
gsc_data = self.gsc_service.get_search_analytics(user_id, site_url)
|
||||
else:
|
||||
# Get all sites for user
|
||||
sites = self._get_gsc_sites(user_id)
|
||||
if sites:
|
||||
gsc_data = self.gsc_service.get_search_analytics(user_id, sites[0])
|
||||
else:
|
||||
return {"error": "No GSC sites found", "data": [], "status": "disconnected"}
|
||||
|
||||
# Cache the data
|
||||
self.analytics_cache.set('gsc_analytics', user_id, gsc_data, ttl_override=3600, site_url=site_url or 'default') # 1 hour cache
|
||||
|
||||
return gsc_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting GSC data for user {user_id}: {e}")
|
||||
return {"error": str(e), "data": [], "status": "error"}
|
||||
|
||||
async def get_bing_data(self, user_id: str, site_url: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Get Bing Webmaster Tools data for the specified site."""
|
||||
try:
|
||||
# Check if user has Bing tokens
|
||||
tokens = self.bing_oauth.get_user_tokens(user_id)
|
||||
if not tokens:
|
||||
return {"error": "Bing not connected", "data": [], "status": "disconnected"}
|
||||
|
||||
# Try to get from cache first
|
||||
cache_key = f"bing_analytics:{user_id}:{site_url or 'default'}"
|
||||
cached_data = self.analytics_cache.get('bing_analytics', user_id, site_url=site_url or 'default')
|
||||
if cached_data:
|
||||
return cached_data
|
||||
|
||||
# Get data from Bing storage service
|
||||
if site_url:
|
||||
bing_data = self.bing_storage.get_analytics_summary(user_id, site_url, days=30)
|
||||
else:
|
||||
# Get all sites for user
|
||||
sites = self._get_bing_sites(user_id)
|
||||
if sites:
|
||||
logger.info(f"Using first Bing site for analysis: {sites[0]}")
|
||||
bing_data = self.bing_storage.get_analytics_summary(user_id, sites[0], days=30)
|
||||
else:
|
||||
logger.warning(f"No Bing sites found for user {user_id}")
|
||||
return {"error": "No Bing sites found", "data": [], "status": "disconnected"}
|
||||
|
||||
# Cache the data
|
||||
self.analytics_cache.set('bing_analytics', user_id, bing_data, ttl_override=3600, site_url=site_url or 'default') # 1 hour cache
|
||||
|
||||
return bing_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Bing data for user {user_id}: {e}")
|
||||
return {"error": str(e), "data": [], "status": "error"}
|
||||
|
||||
async def get_competitive_insights(self, user_id: str) -> Dict[str, Any]:
|
||||
"""Get competitive insights from onboarding step 3 data."""
|
||||
try:
|
||||
return await self.competitive_analyzer.get_competitive_insights(user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting competitive insights for user {user_id}: {e}")
|
||||
return {
|
||||
"competitor_keywords": [],
|
||||
"content_gaps": [],
|
||||
"opportunity_score": 0
|
||||
}
|
||||
|
||||
async def refresh_analytics_data(self, user_id: str, site_url: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Refresh analytics data by invalidating cache and fetching fresh data."""
|
||||
try:
|
||||
# Invalidate cache
|
||||
cache_keys = [
|
||||
f"gsc_analytics:{user_id}",
|
||||
f"bing_analytics:{user_id}",
|
||||
f"gsc_analytics:{user_id}:{site_url or 'default'}",
|
||||
f"bing_analytics:{user_id}:{site_url or 'default'}"
|
||||
]
|
||||
|
||||
for key in cache_keys:
|
||||
self.analytics_cache.delete(key)
|
||||
|
||||
# Fetch fresh data
|
||||
gsc_result = await self.get_gsc_data(user_id, site_url)
|
||||
bing_result = await self.get_bing_data(user_id, site_url)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Analytics data refreshed successfully",
|
||||
"last_updated": datetime.now().isoformat(),
|
||||
"platforms": {
|
||||
"gsc": {"status": "success" if "error" not in gsc_result else "error"},
|
||||
"bing": {"status": "success" if "error" not in bing_result else "error"}
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error refreshing analytics data for user {user_id}: {e}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to refresh analytics data: {str(e)}",
|
||||
"last_updated": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def _get_gsc_sites(self, user_id: str) -> List[str]:
|
||||
"""Get GSC sites for user."""
|
||||
try:
|
||||
credentials = self.gsc_service.load_user_credentials(user_id)
|
||||
if not credentials:
|
||||
return []
|
||||
|
||||
# This would need to be implemented in GSCService
|
||||
# For now, return empty list
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting GSC sites for user {user_id}: {e}")
|
||||
return []
|
||||
|
||||
def _get_bing_sites(self, user_id: str) -> List[str]:
|
||||
"""Get Bing sites for user."""
|
||||
try:
|
||||
# Use the existing get_user_sites method from BingOAuthService
|
||||
sites = self.bing_oauth.get_user_sites(user_id)
|
||||
if not sites:
|
||||
logger.warning(f"No Bing sites found for user {user_id}")
|
||||
return []
|
||||
|
||||
# Extract site URLs from the sites data
|
||||
site_urls = []
|
||||
for site in sites:
|
||||
if isinstance(site, dict) and site.get('url'):
|
||||
site_urls.append(site['url'])
|
||||
elif isinstance(site, str):
|
||||
site_urls.append(site)
|
||||
|
||||
logger.info(f"Found {len(site_urls)} Bing sites for user {user_id}: {site_urls}")
|
||||
return site_urls
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Bing sites for user {user_id}: {e}")
|
||||
return []
|
||||
|
||||
def _calculate_health_score(self, summary: Dict[str, Any], platform_status: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Calculate overall SEO health score."""
|
||||
try:
|
||||
score = 0
|
||||
max_score = 100
|
||||
|
||||
# Base score for connected platforms
|
||||
if platform_status.get("gsc", {}).get("connected"):
|
||||
score += 30
|
||||
if platform_status.get("bing", {}).get("connected"):
|
||||
score += 20
|
||||
|
||||
# Traffic score (0-30)
|
||||
clicks = summary.get("clicks", 0)
|
||||
if clicks > 1000:
|
||||
score += 30
|
||||
elif clicks > 500:
|
||||
score += 20
|
||||
elif clicks > 100:
|
||||
score += 10
|
||||
|
||||
# CTR score (0-20)
|
||||
ctr = summary.get("ctr", 0)
|
||||
if ctr > 0.05: # 5%
|
||||
score += 20
|
||||
elif ctr > 0.03: # 3%
|
||||
score += 15
|
||||
elif ctr > 0.01: # 1%
|
||||
score += 10
|
||||
|
||||
# Determine trend and color
|
||||
if score >= 80:
|
||||
trend = "up"
|
||||
label = "EXCELLENT"
|
||||
color = "#4CAF50"
|
||||
elif score >= 60:
|
||||
trend = "stable"
|
||||
label = "GOOD"
|
||||
color = "#2196F3"
|
||||
elif score >= 40:
|
||||
trend = "down"
|
||||
label = "NEEDS IMPROVEMENT"
|
||||
color = "#FF9800"
|
||||
else:
|
||||
trend = "down"
|
||||
label = "POOR"
|
||||
color = "#F44336"
|
||||
|
||||
return {
|
||||
"score": score,
|
||||
"change": 0, # Would need historical data to calculate
|
||||
"trend": trend,
|
||||
"label": label,
|
||||
"color": color
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating health score: {e}")
|
||||
return {
|
||||
"score": 0,
|
||||
"change": 0,
|
||||
"trend": "unknown",
|
||||
"label": "UNKNOWN",
|
||||
"color": "#9E9E9E"
|
||||
}
|
||||
|
||||
async def _generate_ai_insights(self, summary: Dict[str, Any], timeseries: List[Dict[str, Any]], competitor_insights: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Generate AI insights from analytics data."""
|
||||
try:
|
||||
insights = []
|
||||
|
||||
# Traffic insights
|
||||
clicks = summary.get("clicks", 0)
|
||||
ctr = summary.get("ctr", 0)
|
||||
|
||||
if clicks > 0 and ctr < 0.02: # Low CTR
|
||||
insights.append({
|
||||
"type": "opportunity",
|
||||
"priority": "high",
|
||||
"text": f"Your CTR is {ctr:.1%}, which is below average. Consider optimizing your meta descriptions and titles.",
|
||||
"category": "performance"
|
||||
})
|
||||
|
||||
# Competitive insights
|
||||
opportunity_score = competitor_insights.get("opportunity_score", 0)
|
||||
if opportunity_score > 70:
|
||||
insights.append({
|
||||
"type": "opportunity",
|
||||
"priority": "high",
|
||||
"text": f"High opportunity score of {opportunity_score}% - competitors are ranking for keywords you're not targeting.",
|
||||
"category": "competitive"
|
||||
})
|
||||
|
||||
# Content gaps
|
||||
content_gaps = competitor_insights.get("content_gaps", [])
|
||||
if content_gaps:
|
||||
insights.append({
|
||||
"type": "action",
|
||||
"priority": "medium",
|
||||
"text": f"Found {len(content_gaps)} content gaps. Consider creating content for these topics.",
|
||||
"category": "content"
|
||||
})
|
||||
|
||||
return insights
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating AI insights: {e}")
|
||||
return []
|
||||
Reference in New Issue
Block a user