API: envoi Matomo fire-and-forget en async (#78)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-05-13 14:34:52 +02:00
parent 912507b1d9
commit 985af0fdc7
3 changed files with 105 additions and 4 deletions
+55 -4
View File
@@ -1,14 +1,16 @@
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
_db_path: Optional[str] = None
def _worker_loop(q: queue.Queue, db_path: str) -> None:
@@ -28,15 +30,55 @@ def _worker_loop(q: queue.Queue, db_path: str) -> None:
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, _db_path
global _queue, _worker_thread
if _worker_thread is not None and _worker_thread.is_alive():
return
_db_path = db_path
_queue = queue.Queue()
_worker_thread = threading.Thread(
target=_worker_loop, args=(_queue, db_path), daemon=True
@@ -64,4 +106,13 @@ def flush(timeout: float = 2.0) -> None:
"""Attend que la queue soit drainée. Utile en test."""
if _queue is None:
return
_queue.join()
# Use a sentinel approach to properly honour the timeout
done_event = threading.Event()
def _wait():
_queue.join()
done_event.set()
t = threading.Thread(target=_wait, daemon=True)
t.start()
done_event.wait(timeout=timeout)