"""Author RAG — Analytics Service. Event tracking, session aggregation, and MaxMind geo-location. Per implementation plan features #43-50. """ import hashlib from datetime import datetime, timezone from typing import Any import structlog from sqlalchemy.ext.asyncio import AsyncSession logger = structlog.get_logger(__name__) def fingerprint(ip: str, user_agent: str) -> str: """SHA-256 fingerprint from IP + User-Agent. No PII stored. Args: ip: Client IP address. user_agent: HTTP User-Agent header. Returns: 32-character hex fingerprint. """ combined = f"{ip}|{user_agent}" return hashlib.sha256(combined.encode()).hexdigest()[:32] async def track_event( db: AsyncSession, author_id: str, event_type: str, session_id: str, book_id: str | None = None, metadata: dict[str, Any] | None = None, ) -> None: """Persist an analytics event. Event types: chat_start, message, buy_link_click, rating, session_end. Args: db: Database session. author_id: Author UUID. event_type: Event category string. session_id: Reader session UUID. book_id: Optional book UUID. metadata: Additional event data. """ try: from app.models.analytics import AnalyticsEvent event = AnalyticsEvent( author_id=author_id, event_type=event_type, session_id=session_id, book_id=book_id, meta=metadata or {}, created_at=datetime.now(timezone.utc), ) db.add(event) await db.commit() except Exception as e: logger.warning("Failed to track event", event_type=event_type, error=str(e)) async def get_geo_country(ip: str) -> str | None: """Look up country for an IP via MaxMind GeoLite2. Returns None if GeoIP database is not available. """ try: import geoip2.database from app.config import get_settings cfg = get_settings() with geoip2.database.Reader(cfg.GEO_DB_PATH) as reader: response = reader.country(ip) return response.country.iso_code except Exception: return None