mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 23:50:12 +01:00
265 lines
7.8 KiB
Python
265 lines
7.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any, Dict, Mapping, Optional, Sequence
|
|
|
|
from fastapi import Request
|
|
|
|
__all__ = [
|
|
"log_commander_page_view",
|
|
"log_commander_create_deck",
|
|
"log_partner_suggestions_generated",
|
|
"log_partner_suggestion_selected",
|
|
"log_include_exclude_toggle",
|
|
"log_frontend_event",
|
|
]
|
|
|
|
_LOGGER = logging.getLogger("web.commander_browser")
|
|
_PARTNER_LOGGER = logging.getLogger("web.partner_suggestions")
|
|
_MUST_HAVE_LOGGER = logging.getLogger("web.must_haves")
|
|
_FRONTEND_LOGGER = logging.getLogger("web.frontend_events")
|
|
|
|
|
|
def _emit(logger: logging.Logger, payload: Dict[str, Any]) -> None:
|
|
try:
|
|
logger.info(json.dumps(payload, separators=(",", ":"), ensure_ascii=False))
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _request_id(request: Request) -> str | None:
|
|
try:
|
|
rid = getattr(request.state, "request_id", None)
|
|
if rid:
|
|
return str(rid)
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
|
|
def _client_ip(request: Request) -> str | None:
|
|
try:
|
|
client = getattr(request, "client", None)
|
|
if client and getattr(client, "host", None):
|
|
return str(client.host)
|
|
forwarded = request.headers.get("X-Forwarded-For")
|
|
if forwarded:
|
|
return forwarded.split(",")[0].strip()
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
|
|
def _query_snapshot(request: Request) -> Dict[str, Any]:
|
|
snapshot: Dict[str, Any] = {}
|
|
try:
|
|
params = request.query_params
|
|
items = params.multi_items() if hasattr(params, "multi_items") else params.items()
|
|
for key, value in items:
|
|
key = str(key)
|
|
value = str(value)
|
|
if key in snapshot:
|
|
existing = snapshot[key]
|
|
if isinstance(existing, list):
|
|
existing.append(value)
|
|
else:
|
|
snapshot[key] = [existing, value]
|
|
else:
|
|
snapshot[key] = value
|
|
except Exception:
|
|
return {}
|
|
return snapshot
|
|
|
|
|
|
def log_commander_page_view(
|
|
request: Request,
|
|
*,
|
|
page: int,
|
|
result_total: int,
|
|
result_count: int,
|
|
is_htmx: bool,
|
|
) -> None:
|
|
payload: Dict[str, Any] = {
|
|
"event": "commander_browser.page_view",
|
|
"request_id": _request_id(request),
|
|
"path": str(request.url.path),
|
|
"query": _query_snapshot(request),
|
|
"page": int(page),
|
|
"result_total": int(result_total),
|
|
"result_count": int(result_count),
|
|
"is_htmx": bool(is_htmx),
|
|
"client_ip": _client_ip(request),
|
|
}
|
|
_emit(_LOGGER, payload)
|
|
|
|
|
|
def log_commander_create_deck(
|
|
request: Request,
|
|
*,
|
|
commander: str,
|
|
return_url: str | None,
|
|
) -> None:
|
|
payload: Dict[str, Any] = {
|
|
"event": "commander_browser.create_deck",
|
|
"request_id": _request_id(request),
|
|
"path": str(request.url.path),
|
|
"query": _query_snapshot(request),
|
|
"commander": commander,
|
|
"has_return": bool(return_url),
|
|
"return_url": return_url,
|
|
"client_ip": _client_ip(request),
|
|
}
|
|
_emit(_LOGGER, payload)
|
|
|
|
|
|
def _extract_dataset_metadata(metadata: Mapping[str, Any] | None) -> Dict[str, Any]:
|
|
if not isinstance(metadata, Mapping):
|
|
return {}
|
|
snapshot: Dict[str, Any] = {}
|
|
for key in ("dataset_version", "generated_at", "record_count", "entry_count", "build_id"):
|
|
if key in metadata:
|
|
snapshot[key] = metadata[key]
|
|
if not snapshot:
|
|
# Fall back to a small subset to avoid logging the full metadata document.
|
|
for key, value in list(metadata.items())[:5]:
|
|
snapshot[key] = value
|
|
return snapshot
|
|
|
|
|
|
def log_partner_suggestions_generated(
|
|
request: Request,
|
|
*,
|
|
commander_display: str,
|
|
commander_canonical: str,
|
|
include_modes: Sequence[str] | None,
|
|
available_modes: Sequence[str],
|
|
total: int,
|
|
mode_counts: Mapping[str, int],
|
|
visible_count: int,
|
|
hidden_count: int,
|
|
limit_per_mode: int,
|
|
visible_limit: int,
|
|
include_hidden: bool,
|
|
refresh_requested: bool,
|
|
dataset_metadata: Mapping[str, Any] | None = None,
|
|
) -> None:
|
|
payload: Dict[str, Any] = {
|
|
"event": "partner_suggestions.generated",
|
|
"request_id": _request_id(request),
|
|
"path": str(request.url.path),
|
|
"query": _query_snapshot(request),
|
|
"commander": {
|
|
"display": commander_display,
|
|
"canonical": commander_canonical,
|
|
},
|
|
"limits": {
|
|
"per_mode": int(limit_per_mode),
|
|
"visible": int(visible_limit),
|
|
"include_hidden": bool(include_hidden),
|
|
},
|
|
"result": {
|
|
"total": int(total),
|
|
"visible_count": int(visible_count),
|
|
"hidden_count": int(hidden_count),
|
|
"available_modes": list(available_modes),
|
|
"mode_counts": {str(key): int(value) for key, value in mode_counts.items()},
|
|
"metadata": _extract_dataset_metadata(dataset_metadata),
|
|
},
|
|
"filters": {
|
|
"include_modes": [str(mode) for mode in (include_modes or [])],
|
|
"refresh": bool(refresh_requested),
|
|
},
|
|
"client_ip": _client_ip(request),
|
|
}
|
|
_emit(_PARTNER_LOGGER, payload)
|
|
|
|
|
|
def log_partner_suggestion_selected(
|
|
request: Request,
|
|
*,
|
|
commander: str,
|
|
scope: str | None,
|
|
partner_enabled: bool,
|
|
auto_opt_out: bool,
|
|
auto_assigned: bool,
|
|
selection_source: Optional[str],
|
|
secondary_candidate: str | None,
|
|
background_candidate: str | None,
|
|
resolved_secondary: str | None,
|
|
resolved_background: str | None,
|
|
partner_mode: str | None,
|
|
has_preview: bool,
|
|
warnings: Sequence[str] | None,
|
|
error: str | None,
|
|
) -> None:
|
|
payload: Dict[str, Any] = {
|
|
"event": "partner_suggestions.selected",
|
|
"request_id": _request_id(request),
|
|
"path": str(request.url.path),
|
|
"scope": scope or "",
|
|
"commander": commander,
|
|
"partner_enabled": bool(partner_enabled),
|
|
"auto_opt_out": bool(auto_opt_out),
|
|
"auto_assigned": bool(auto_assigned),
|
|
"selection_source": (selection_source or "") or None,
|
|
"inputs": {
|
|
"secondary_candidate": secondary_candidate,
|
|
"background_candidate": background_candidate,
|
|
},
|
|
"resolved": {
|
|
"partner_mode": partner_mode,
|
|
"secondary": resolved_secondary,
|
|
"background": resolved_background,
|
|
},
|
|
"preview_available": bool(has_preview),
|
|
"warnings_count": len(warnings or []),
|
|
"has_error": bool(error),
|
|
"error": error,
|
|
"client_ip": _client_ip(request),
|
|
}
|
|
if warnings:
|
|
payload["warnings"] = list(warnings)
|
|
_emit(_PARTNER_LOGGER, payload)
|
|
|
|
|
|
def log_include_exclude_toggle(
|
|
request: Request,
|
|
*,
|
|
card_name: str,
|
|
action: str,
|
|
enabled: bool,
|
|
include_count: int,
|
|
exclude_count: int,
|
|
) -> None:
|
|
payload: Dict[str, Any] = {
|
|
"event": "must_haves.toggle",
|
|
"request_id": _request_id(request),
|
|
"path": str(request.url.path),
|
|
"card": card_name,
|
|
"list": action,
|
|
"enabled": bool(enabled),
|
|
"include_count": int(include_count),
|
|
"exclude_count": int(exclude_count),
|
|
"client_ip": _client_ip(request),
|
|
}
|
|
_emit(_MUST_HAVE_LOGGER, payload)
|
|
|
|
|
|
def log_frontend_event(
|
|
request: Request,
|
|
event: str,
|
|
data: Mapping[str, Any] | None,
|
|
) -> None:
|
|
snapshot: Dict[str, Any] = {}
|
|
if isinstance(data, Mapping):
|
|
snapshot = {str(k): data[k] for k in data}
|
|
payload: Dict[str, Any] = {
|
|
"event": f"frontend.{event}",
|
|
"request_id": _request_id(request),
|
|
"path": str(request.url.path),
|
|
"data": snapshot,
|
|
"referer": request.headers.get("referer"),
|
|
"client_ip": _client_ip(request),
|
|
}
|
|
_emit(_FRONTEND_LOGGER, payload)
|