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.
454 lines
16 KiB
Python
454 lines
16 KiB
Python
"""
|
|
Bing Analytics Storage API Routes
|
|
|
|
Provides endpoints for accessing stored Bing analytics data,
|
|
historical trends, and performance analysis.
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
|
|
from typing import Optional, Dict, Any, List
|
|
from datetime import datetime, timedelta
|
|
from loguru import logger
|
|
import os
|
|
import json
|
|
from sqlalchemy import and_
|
|
|
|
from services.bing_analytics_storage_service import BingAnalyticsStorageService
|
|
from middleware.auth_middleware import get_current_user
|
|
|
|
router = APIRouter(prefix="/bing-analytics", tags=["Bing Analytics Storage"])
|
|
|
|
# Initialize storage service
|
|
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./bing_analytics.db')
|
|
storage_service = BingAnalyticsStorageService(DATABASE_URL)
|
|
|
|
|
|
@router.post("/collect-data")
|
|
async def collect_bing_data(
|
|
background_tasks: BackgroundTasks,
|
|
site_url: str = Query(..., description="Site URL to collect data for"),
|
|
days_back: int = Query(30, description="Number of days back to collect data"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Collect and store Bing analytics data for a site.
|
|
This endpoint triggers data collection from Bing API and stores it in the database.
|
|
"""
|
|
try:
|
|
user_id = current_user.get("user_id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="User not authenticated")
|
|
|
|
logger.info(f"Starting Bing data collection for user {user_id}, site: {site_url}")
|
|
|
|
# Run data collection in background
|
|
background_tasks.add_task(
|
|
storage_service.collect_and_store_data,
|
|
user_id=user_id,
|
|
site_url=site_url,
|
|
days_back=days_back
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Bing data collection started for {site_url}",
|
|
"site_url": site_url,
|
|
"days_back": days_back,
|
|
"status": "collecting"
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error starting Bing data collection: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
|
|
|
|
|
@router.get("/summary")
|
|
async def get_analytics_summary(
|
|
site_url: str = Query(..., description="Site URL to get analytics for"),
|
|
days: int = Query(30, description="Number of days for analytics summary"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get comprehensive analytics summary for a site over a specified period.
|
|
"""
|
|
try:
|
|
user_id = current_user.get("user_id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="User not authenticated")
|
|
|
|
logger.info(f"Getting analytics summary for user {user_id}, site: {site_url}, days: {days}")
|
|
|
|
summary = storage_service.get_analytics_summary(
|
|
user_id=user_id,
|
|
site_url=site_url,
|
|
days=days
|
|
)
|
|
|
|
if 'error' in summary:
|
|
raise HTTPException(status_code=404, detail=summary['error'])
|
|
|
|
return {
|
|
"success": True,
|
|
"data": summary,
|
|
"site_url": site_url,
|
|
"period_days": days
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting analytics summary: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
|
|
|
|
|
@router.get("/daily-metrics")
|
|
async def get_daily_metrics(
|
|
site_url: str = Query(..., description="Site URL to get daily metrics for"),
|
|
days: int = Query(30, description="Number of days to retrieve"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get daily metrics for a site over a specified period.
|
|
"""
|
|
try:
|
|
user_id = current_user.get("user_id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="User not authenticated")
|
|
|
|
logger.info(f"Getting daily metrics for user {user_id}, site: {site_url}, days: {days}")
|
|
|
|
db = storage_service._get_db_session()
|
|
|
|
# Calculate date range
|
|
end_date = datetime.now()
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Get daily metrics
|
|
daily_metrics = db.query(storage_service.BingDailyMetrics).filter(
|
|
and_(
|
|
storage_service.BingDailyMetrics.user_id == user_id,
|
|
storage_service.BingDailyMetrics.site_url == site_url,
|
|
storage_service.BingDailyMetrics.metric_date >= start_date,
|
|
storage_service.BingDailyMetrics.metric_date <= end_date
|
|
)
|
|
).order_by(storage_service.BingDailyMetrics.metric_date).all()
|
|
|
|
db.close()
|
|
|
|
# Format response
|
|
metrics_data = []
|
|
for metric in daily_metrics:
|
|
metrics_data.append({
|
|
"date": metric.metric_date.isoformat(),
|
|
"total_clicks": metric.total_clicks,
|
|
"total_impressions": metric.total_impressions,
|
|
"total_queries": metric.total_queries,
|
|
"avg_ctr": metric.avg_ctr,
|
|
"avg_position": metric.avg_position,
|
|
"clicks_change": metric.clicks_change,
|
|
"impressions_change": metric.impressions_change,
|
|
"ctr_change": metric.ctr_change,
|
|
"top_queries": json.loads(metric.top_queries) if metric.top_queries else [],
|
|
"collected_at": metric.collected_at.isoformat()
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"data": metrics_data,
|
|
"site_url": site_url,
|
|
"period_days": days,
|
|
"metrics_count": len(metrics_data)
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting daily metrics: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
|
|
|
|
|
@router.get("/top-queries")
|
|
async def get_top_queries(
|
|
site_url: str = Query(..., description="Site URL to get top queries for"),
|
|
days: int = Query(30, description="Number of days to analyze"),
|
|
limit: int = Query(50, description="Number of top queries to return"),
|
|
sort_by: str = Query("clicks", description="Sort by: clicks, impressions, or ctr"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get top performing queries for a site over a specified period.
|
|
"""
|
|
try:
|
|
user_id = current_user.get("user_id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="User not authenticated")
|
|
|
|
if sort_by not in ["clicks", "impressions", "ctr"]:
|
|
raise HTTPException(status_code=400, detail="sort_by must be 'clicks', 'impressions', or 'ctr'")
|
|
|
|
logger.info(f"Getting top queries for user {user_id}, site: {site_url}, sort_by: {sort_by}")
|
|
|
|
db = storage_service._get_db_session()
|
|
|
|
# Calculate date range
|
|
end_date = datetime.now()
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Get raw query data
|
|
query_stats = db.query(storage_service.BingQueryStats).filter(
|
|
and_(
|
|
storage_service.BingQueryStats.user_id == user_id,
|
|
storage_service.BingQueryStats.site_url == site_url,
|
|
storage_service.BingQueryStats.query_date >= start_date,
|
|
storage_service.BingQueryStats.query_date <= end_date
|
|
)
|
|
).all()
|
|
|
|
db.close()
|
|
|
|
if not query_stats:
|
|
return {
|
|
"success": True,
|
|
"data": [],
|
|
"message": "No query data found for the specified period"
|
|
}
|
|
|
|
# Aggregate queries
|
|
query_aggregates = {}
|
|
for stat in query_stats:
|
|
query = stat.query
|
|
if query not in query_aggregates:
|
|
query_aggregates[query] = {
|
|
"query": query,
|
|
"total_clicks": 0,
|
|
"total_impressions": 0,
|
|
"avg_ctr": 0,
|
|
"avg_position": 0,
|
|
"days_appeared": 0,
|
|
"category": stat.category,
|
|
"is_brand": stat.is_brand_query
|
|
}
|
|
|
|
query_aggregates[query]["total_clicks"] += stat.clicks
|
|
query_aggregates[query]["total_impressions"] += stat.impressions
|
|
query_aggregates[query]["days_appeared"] += 1
|
|
|
|
# Calculate weighted average position
|
|
if stat.avg_click_position > 0:
|
|
query_aggregates[query]["avg_position"] = (
|
|
query_aggregates[query]["avg_position"] * (query_aggregates[query]["days_appeared"] - 1) +
|
|
stat.avg_click_position
|
|
) / query_aggregates[query]["days_appeared"]
|
|
|
|
# Calculate CTR for each query
|
|
for query_data in query_aggregates.values():
|
|
query_data["avg_ctr"] = (
|
|
query_data["total_clicks"] / query_data["total_impressions"] * 100
|
|
) if query_data["total_impressions"] > 0 else 0
|
|
|
|
# Sort and limit results
|
|
sorted_queries = sorted(
|
|
list(query_aggregates.values()),
|
|
key=lambda x: x[f"total_{sort_by}"],
|
|
reverse=True
|
|
)[:limit]
|
|
|
|
return {
|
|
"success": True,
|
|
"data": sorted_queries,
|
|
"site_url": site_url,
|
|
"period_days": days,
|
|
"sort_by": sort_by,
|
|
"total_queries": len(query_aggregates),
|
|
"returned_queries": len(sorted_queries)
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting top queries: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
|
|
|
|
|
@router.get("/query-details")
|
|
async def get_query_details(
|
|
site_url: str = Query(..., description="Site URL"),
|
|
query: str = Query(..., description="Specific query to analyze"),
|
|
days: int = Query(30, description="Number of days to analyze"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get detailed performance data for a specific query.
|
|
"""
|
|
try:
|
|
user_id = current_user.get("user_id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="User not authenticated")
|
|
|
|
logger.info(f"Getting query details for user {user_id}, query: {query}")
|
|
|
|
db = storage_service._get_db_session()
|
|
|
|
# Calculate date range
|
|
end_date = datetime.now()
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Get query stats
|
|
query_stats = db.query(storage_service.BingQueryStats).filter(
|
|
and_(
|
|
storage_service.BingQueryStats.user_id == user_id,
|
|
storage_service.BingQueryStats.site_url == site_url,
|
|
storage_service.BingQueryStats.query == query,
|
|
storage_service.BingQueryStats.query_date >= start_date,
|
|
storage_service.BingQueryStats.query_date <= end_date
|
|
)
|
|
).order_by(storage_service.BingQueryStats.query_date).all()
|
|
|
|
db.close()
|
|
|
|
if not query_stats:
|
|
return {
|
|
"success": True,
|
|
"data": None,
|
|
"message": f"No data found for query: {query}"
|
|
}
|
|
|
|
# Calculate summary statistics
|
|
total_clicks = sum(stat.clicks for stat in query_stats)
|
|
total_impressions = sum(stat.impressions for stat in query_stats)
|
|
avg_ctr = (total_clicks / total_impressions * 100) if total_impressions > 0 else 0
|
|
avg_position = sum(stat.avg_click_position for stat in query_stats if stat.avg_click_position > 0) / len([stat for stat in query_stats if stat.avg_click_position > 0]) if any(stat.avg_click_position > 0 for stat in query_stats) else 0
|
|
|
|
# Daily performance data
|
|
daily_data = []
|
|
for stat in query_stats:
|
|
daily_data.append({
|
|
"date": stat.query_date.isoformat(),
|
|
"clicks": stat.clicks,
|
|
"impressions": stat.impressions,
|
|
"ctr": stat.ctr,
|
|
"avg_click_position": stat.avg_click_position,
|
|
"avg_impression_position": stat.avg_impression_position
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"query": query,
|
|
"period_days": days,
|
|
"total_clicks": total_clicks,
|
|
"total_impressions": total_impressions,
|
|
"avg_ctr": round(avg_ctr, 2),
|
|
"avg_position": round(avg_position, 2),
|
|
"days_appeared": len(query_stats),
|
|
"category": query_stats[0].category,
|
|
"is_brand_query": query_stats[0].is_brand_query,
|
|
"daily_performance": daily_data
|
|
}
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting query details: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
|
|
|
|
|
@router.get("/sites")
|
|
async def get_user_sites(
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Get list of sites with stored Bing analytics data.
|
|
"""
|
|
try:
|
|
user_id = current_user.get("user_id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="User not authenticated")
|
|
|
|
logger.info(f"Getting user sites for user {user_id}")
|
|
|
|
db = storage_service._get_db_session()
|
|
|
|
# Get unique sites for the user
|
|
sites = db.query(storage_service.BingDailyMetrics.site_url).filter(
|
|
storage_service.BingDailyMetrics.user_id == user_id
|
|
).distinct().all()
|
|
|
|
db.close()
|
|
|
|
sites_data = []
|
|
for site_tuple in sites:
|
|
site_url = site_tuple[0]
|
|
|
|
# Get latest metrics for each site
|
|
summary = storage_service.get_analytics_summary(user_id, site_url, 7)
|
|
|
|
sites_data.append({
|
|
"site_url": site_url,
|
|
"latest_summary": summary if 'error' not in summary else None,
|
|
"has_data": 'error' not in summary
|
|
})
|
|
|
|
return {
|
|
"success": True,
|
|
"data": sites_data,
|
|
"total_sites": len(sites_data)
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting user sites: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
|
|
|
|
|
@router.post("/generate-daily-metrics")
|
|
async def generate_daily_metrics(
|
|
background_tasks: BackgroundTasks,
|
|
site_url: str = Query(..., description="Site URL to generate metrics for"),
|
|
target_date: Optional[str] = Query(None, description="Target date (YYYY-MM-DD), defaults to yesterday"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Generate daily metrics for a specific date from stored raw data.
|
|
"""
|
|
try:
|
|
user_id = current_user.get("user_id")
|
|
if not user_id:
|
|
raise HTTPException(status_code=401, detail="User not authenticated")
|
|
|
|
# Parse target date
|
|
if target_date:
|
|
try:
|
|
target_dt = datetime.strptime(target_date, '%Y-%m-%d')
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD")
|
|
else:
|
|
target_dt = None
|
|
|
|
logger.info(f"Generating daily metrics for user {user_id}, site: {site_url}, date: {target_dt}")
|
|
|
|
# Run in background
|
|
background_tasks.add_task(
|
|
storage_service.generate_daily_metrics,
|
|
user_id=user_id,
|
|
site_url=site_url,
|
|
target_date=target_dt
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"Daily metrics generation started for {site_url}",
|
|
"site_url": site_url,
|
|
"target_date": target_dt.isoformat() if target_dt else "yesterday"
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error generating daily metrics: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|