Files
colibre/src/api/tracking.py
T
2026-05-13 14:45:26 +02:00

111 lines
2.9 KiB
Python

import os
import queue
import threading
from typing import Optional
import httpx
from src.api import tokens_db
from src.utils import logger
_STOP_SENTINEL = object()
_queue: Optional[queue.Queue] = None
_worker_thread: Optional[threading.Thread] = None
def _worker_loop(q: queue.Queue, db_path: str) -> None:
while True:
item = q.get()
try:
if item is _STOP_SENTINEL:
return
kind, payload = item
if kind == "counter":
token_id = payload
try:
tokens_db.increment_usage(db_path, token_id)
except Exception: # noqa: BLE001
logger.warning(
"tracking: échec increment_usage token_id=%s",
token_id,
exc_info=True,
)
elif kind == "matomo":
try:
_post_matomo(**payload)
except Exception: # noqa: BLE001
logger.warning("tracking: échec envoi Matomo", exc_info=True)
finally:
q.task_done()
def _post_matomo(url: str, params: dict) -> None:
"""POST fire-and-forget vers la Tracking API Matomo. Mockable en test."""
httpx.post(url, data=params, timeout=5.0)
def enqueue_matomo_event(
token_id: int,
path: str,
query_string: str,
status_code: int,
user_agent: str,
) -> None:
if _queue is None:
return
if os.getenv("MATOMO_TRACKING_ENABLED", "false").lower() != "true":
return
url = os.getenv("MATOMO_URL")
site_id = os.getenv("MATOMO_SITE_ID")
if not url or not site_id:
return
full_url = f"https://decp.info{path}"
if query_string:
full_url += f"?{query_string}"
params = {
"idsite": site_id,
"rec": "1",
"url": full_url,
"action_name": f"API {path}",
"uid": f"token-{token_id}",
"dimension1": str(token_id),
"dimension2": str(status_code),
"ua": user_agent,
}
_queue.put(("matomo", {"url": url, "params": params}))
def start_worker(db_path: str) -> None:
global _queue, _worker_thread
if _worker_thread is not None and _worker_thread.is_alive():
return
_queue = queue.Queue()
_worker_thread = threading.Thread(
target=_worker_loop, args=(_queue, db_path), daemon=True
)
_worker_thread.start()
def stop_worker() -> None:
global _worker_thread, _queue
if _worker_thread is None:
return
_queue.put(_STOP_SENTINEL)
_worker_thread.join(timeout=2.0)
_worker_thread = None
_queue = None
def enqueue_counter_update(token_id: int) -> None:
if _queue is None:
return # tracking désactivé (tests par ex.)
_queue.put(("counter", token_id))
def flush(timeout: float = 2.0) -> None:
"""Attend que la queue soit drainée. Utile en test."""
q = _queue
if q is None:
return
q.join()