Spaces:
Running
Running
| """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 | |