from fastapi import APIRouter, Depends, HTTPException, status, Request, Query, Body from sqlalchemy.orm import Session from sqlalchemy import func, desc from typing import Optional, List, Dict, Any from datetime import datetime, timedelta, timezone import json from app.db.base import get_db from app.models.analytics import VisitorLog, AnalyticsEvent from app.models.user import User, UserRole from app.api.auth import get_current_user, get_optional_current_user from app.services.metadata_service import parse_user_agent, get_geoip_info router = APIRouter(prefix="/analytics", tags=["Analytics"]) def check_admin(user_id: int, db: Session): user = db.query(User).filter(User.id == user_id).first() if not user or user.role != UserRole.ADMIN: raise HTTPException(status_code=403, detail="Not authorized") return user @router.post("/track") async def track_event( request: Request, payload: Dict[str, Any] = Body(...), db: Session = Depends(get_db), current_user_id: Optional[int] = Depends(get_optional_current_user) ): """ Track a visitor event. Creates or updates a visitor log and logs the event. Payload: { session_id, event_type, page_url, page_title, event_data, client_metadata } """ # 0. Skip recording for Admin users to keep analytics clean if current_user_id: user = db.query(User).filter(User.id == current_user_id).first() if user and user.role == UserRole.ADMIN: return {"isSuccess": True, "message": "Admin event skipped"} print(f">>> [ANALYTICS] Received tracking event: {payload.get('event_type')} for {payload.get('page_url')}") session_id = payload.get("session_id") event_type = payload.get("event_type", "PAGE_VIEW") # 1. Identify or Create Visitor visitor = None if session_id: visitor = db.query(VisitorLog).filter(VisitorLog.session_id == session_id).first() # Fallback to IP if no session_id (e.g. tracking disabled or first load) ip_address = request.client.host if request.client else None user_agent = request.headers.get("User-Agent") if not visitor: ua_info = parse_user_agent(user_agent) location_data_raw = await get_geoip_info(ip_address) location_data = json.loads(location_data_raw) if location_data_raw else None visitor = VisitorLog( session_id=session_id, user_id=current_user_id, ip_address=ip_address, user_agent=user_agent, browser=ua_info["browser"], os=ua_info["os"], device_type=ua_info["device_type"], location_data=location_data, client_metadata=payload.get("client_metadata") ) db.add(visitor) db.flush() # Get visitor.id else: # Update last seen and potentially user_id if they just logged in visitor.last_seen = datetime.now(timezone.utc) if current_user_id and not visitor.user_id: visitor.user_id = current_user_id # Update client metadata if provided if payload.get("client_metadata"): visitor.client_metadata = payload.get("client_metadata") # 2. Log Event event = AnalyticsEvent( visitor_id=visitor.id, event_type=event_type, page_url=payload.get("page_url"), page_title=payload.get("page_title"), event_data=payload.get("event_data") ) db.add(event) try: db.commit() except Exception as e: db.rollback() # Log error or silence it to not break frontend print(f">>> [ANALYTICS] Insert error: {e}") return {"isSuccess": False} return {"isSuccess": True} @router.get("/admin/stats") async def get_analytics_stats( days: int = Query(7, ge=1, le=90), start_date: Optional[str] = Query(None), end_date: Optional[str] = Query(None), category_id: Optional[int] = Query(None), current_user_id: int = Depends(get_current_user), db: Session = Depends(get_db) ): check_admin(current_user_id, db) # 0. Date Range Logic now = datetime.now(timezone.utc) if start_date and end_date: try: since = datetime.fromisoformat(start_date.replace('Z', '+00:00')) until = datetime.fromisoformat(end_date.replace('Z', '+00:00')) # Calculate effective days for trends days = (until - since).days + 1 if days < 1: days = 1 if days > 90: days = 90 except: since = now - timedelta(days=days) until = now else: since = now - timedelta(days=days) until = now # 1. Total unique visitors (in period) total_visitors = db.query(func.count(VisitorLog.id)).filter(VisitorLog.last_seen >= since, VisitorLog.last_seen <= until).scalar() or 0 # 2. Total events (in period) total_events = db.query(func.count(AnalyticsEvent.id)).filter(AnalyticsEvent.created_at >= since, AnalyticsEvent.created_at <= until).scalar() or 0 # 3. Active Now (last 5 minutes) active_since = now - timedelta(minutes=5) active_now = db.query(func.count(VisitorLog.id)).filter(VisitorLog.last_seen >= active_since).scalar() or 0 # 4. Device breakdown devices = db.query( VisitorLog.device_type, func.count(VisitorLog.id) ).filter(VisitorLog.last_seen >= since, VisitorLog.last_seen <= until).group_by(VisitorLog.device_type).all() raw_devices = {d[0]: d[1] for d in devices} device_stats = { "desktop": raw_devices.get("Desktop", 0), "mobile": raw_devices.get("Mobile", 0), "tablet": raw_devices.get("Tablet", 0) } # 5. Daily Trends trends = [] # We'll calculate for each day in the range for i in range(days - 1, -1, -1): day_start = (now - timedelta(days=i)).replace(hour=0, minute=0, second=0, microsecond=0) day_end = day_start + timedelta(days=1) day_visitors = db.query(func.count(VisitorLog.id)).filter( VisitorLog.last_seen >= day_start, VisitorLog.last_seen < day_end ).scalar() or 0 day_events = db.query(func.count(AnalyticsEvent.id)).filter( AnalyticsEvent.created_at >= day_start, AnalyticsEvent.created_at < day_end ).scalar() or 0 trends.append({ "date": day_start.strftime("%Y-%m-%d"), "visitors": day_visitors, "events": day_events }) # 6. Top pages top_pages = db.query( AnalyticsEvent.page_url, func.count(AnalyticsEvent.id) ).filter( AnalyticsEvent.created_at >= since, AnalyticsEvent.created_at <= until, AnalyticsEvent.event_type == "PAGE_VIEW" ).group_by(AnalyticsEvent.page_url).order_by(desc(func.count(AnalyticsEvent.id))).limit(10).all() pages_data = [{"url": p[0], "views": p[1]} for p in top_pages] # 7. Top Viewed Products # Logic: Join with Product to allow category filtering top_prod_query = db.query( AnalyticsEvent.page_url, AnalyticsEvent.page_title, func.count(AnalyticsEvent.id) ).filter( AnalyticsEvent.created_at >= since, AnalyticsEvent.created_at <= until, AnalyticsEvent.page_url.like("%/products/%") ) if category_id: # Get category and subcategories subcats = db.query(Category.id).filter(or_(Category.id == category_id, Category.parent_id == category_id)).all() cat_ids = [c[0] for c in subcats] # We need to extract product ID from URL to join with Product table # URL pattern is usually .../products/{id} or .../products/{slug} # For simplicity, we join using LIKE since SQLite/others might not have robust regex support top_prod_query = top_prod_query.join( Product, or_( AnalyticsEvent.page_url.like("%/products/" + cast(Product.id, String)), AnalyticsEvent.page_url.like("%/products/" + Product.slug) ) ).filter(Product.category_id.in_(cat_ids)) top_products = top_prod_query.group_by( AnalyticsEvent.page_url, AnalyticsEvent.page_title ).order_by(desc(func.count(AnalyticsEvent.id))).limit(10).all() products_data = [{"url": p[0], "name": p[1], "views": p[2]} for p in top_products] # 8. Top Searches top_search_query = db.query(AnalyticsEvent.event_data).filter( AnalyticsEvent.created_at >= since, AnalyticsEvent.created_at <= until, AnalyticsEvent.event_type == "SEARCH" ) events_with_data = top_search_query.all() search_counts = {} for (event_data,) in events_with_data: if isinstance(event_data, dict) and "query" in event_data: q = event_data["query"].strip().lower() if q: search_counts[q] = search_counts.get(q, 0) + 1 elif isinstance(event_data, str): # Handle string-encoded JSON if necessary try: data = json.loads(event_data) q = data.get("query", "").strip().lower() if q: search_counts[q] = search_counts.get(q, 0) + 1 except: pass searches_data = sorted( [{"query": q, "count": c} for q, c in search_counts.items()], key=lambda x: x["count"], reverse=True )[:10] return { "isSuccess": True, "value": { "total_visitors": total_visitors, "total_events": total_events, "active_now": active_now, "device_stats": device_stats, "trends": trends, "top_pages": pages_data, "top_products": products_data, "top_searches": searches_data } } @router.get("/admin/visitors") async def get_recent_visitors( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), current_user_id: int = Depends(get_current_user), db: Session = Depends(get_db) ): check_admin(current_user_id, db) query = db.query(VisitorLog).order_by(desc(VisitorLog.last_seen)) total = query.count() visitors = query.offset((page - 1) * page_size).limit(page_size).all() results = [] for v in visitors: # Get event count event_count = db.query(func.count(AnalyticsEvent.id)).filter(AnalyticsEvent.visitor_id == v.id).scalar() or 0 results.append({ "id": v.id, "ip": v.ip_address, "browser": v.browser, "os": v.os, "device": v.device_type, "location": v.location_data, "last_seen": v.last_seen.isoformat(), "event_count": event_count, "user": {"name": v.user.name, "email": v.user.email} if v.user else None }) return { "isSuccess": True, "value": { "visitors": results, "total": total, "page": page, "page_size": page_size } } @router.get("/admin/events") async def get_analytics_events( page: int = Query(1, ge=1), page_size: int = Query(30, ge=1, le=100), event_type: Optional[str] = Query(None), visitor_id: Optional[int] = Query(None), start_date: Optional[str] = Query(None), end_date: Optional[str] = Query(None), current_user_id: int = Depends(get_current_user), db: Session = Depends(get_db) ): check_admin(current_user_id, db) query = db.query(AnalyticsEvent).order_by(desc(AnalyticsEvent.created_at)) # Date filtering if start_date: try: since = datetime.fromisoformat(start_date.replace('Z', '+00:00')) query = query.filter(AnalyticsEvent.created_at >= since) except: pass if end_date: try: until = datetime.fromisoformat(end_date.replace('Z', '+00:00')) query = query.filter(AnalyticsEvent.created_at <= until) except: pass if event_type: query = query.filter(AnalyticsEvent.event_type == event_type) if visitor_id: query = query.filter(AnalyticsEvent.visitor_id == visitor_id) total = query.count() events = query.offset((page - 1) * page_size).limit(page_size).all() results = [] for e in events: results.append({ "id": e.id, "event_type": e.event_type, "page_url": e.page_url, "page_title": e.page_title, "event_data": e.event_data, "created_at": e.created_at.isoformat(), "visitor_id": e.visitor_id, "visitor_ip": e.visitor.ip_address if e.visitor else None, "visitor_location": e.visitor.location_data if e.visitor else None, "user": {"name": e.visitor.user.name, "email": e.visitor.user.email} if e.visitor and e.visitor.user else None }) return { "isSuccess": True, "value": { "events": results, "total": total, "page": page, "page_size": page_size } }