duyduc
Fix HF retrieval source dependency closure
14cd803
Raw
History Blame Contribute Delete
4.08 kB
import logging
from datetime import datetime
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from src.services.db import UsageEvent, get_session, new_id
logger = logging.getLogger(__name__)
class UsageTrackingService:
"""Usage event recording and querying.
CRITICAL DESIGN RULE:
- record_event_safe() opens its OWN short-lived DB session.
It never commits, rollbacks, or closes the caller's request-scoped session.
If the insert fails, it logs the error and returns silently — the main
user request is never broken by a tracking failure.
- Counting/query methods (count_user_events, count_user_events_grouped)
use the injected request session (self.db) for read-only queries. This is
safe because they do not modify state.
"""
def __init__(self, db: Session):
self.db = db # request-scoped session, used for reads only
@staticmethod
def record_event_safe(
*,
event_type: str,
user_id: str | None = None,
organization_id: str | None = None,
guest_session_id: str | None = None,
source: str | None = None,
metadata: dict | None = None,
status: str = "success",
) -> None:
"""Insert a usage event in an INDEPENDENT session.
Opens a new short-lived Session, inserts, commits, closes.
On any failure: logs the error, does NOT raise, does NOT touch
the caller's transaction.
"""
try:
gen = get_session()
session = next(gen)
try:
event = UsageEvent(
id=new_id("evt"),
user_id=user_id,
organization_id=organization_id,
guest_session_id=guest_session_id,
event_type=event_type,
status=status,
source=source,
metadata_json=metadata,
)
session.add(event)
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
except Exception:
logger.exception("Usage tracking failed (event_type=%s)", event_type)
def count_user_events(
self,
user_id: str,
event_type: str,
*,
start_at: datetime | None = None,
end_before: datetime | None = None,
) -> int:
"""Count successful events for a single user and type."""
stmt = select(func.count(UsageEvent.id)).where(
UsageEvent.user_id == user_id,
UsageEvent.event_type == event_type,
UsageEvent.status == "success",
)
if start_at is not None:
stmt = stmt.where(UsageEvent.created_at >= start_at)
if end_before is not None:
stmt = stmt.where(UsageEvent.created_at < end_before)
return int(self.db.scalar(stmt) or 0)
def count_user_events_grouped(
self,
user_id: str,
event_types: list[str],
*,
start_at: datetime | None = None,
end_before: datetime | None = None,
) -> dict[str, int]:
"""Count events grouped by event_type for a single user.
Only counts status == 'success'.
"""
stmt = (
select(UsageEvent.event_type, func.count(UsageEvent.id))
.where(
UsageEvent.user_id == user_id,
UsageEvent.event_type.in_(event_types),
UsageEvent.status == "success",
)
.group_by(UsageEvent.event_type)
)
if start_at is not None:
stmt = stmt.where(UsageEvent.created_at >= start_at)
if end_before is not None:
stmt = stmt.where(UsageEvent.created_at < end_before)
rows = self.db.execute(stmt).all()
result = {et: 0 for et in event_types}
for event_type_val, count in rows:
result[event_type_val] = int(count)
return result