API: envoi Matomo fire-and-forget en async (#78)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,13 @@ def _track_consumption(response):
|
|||||||
token_id = getattr(g, "token_id", None)
|
token_id = getattr(g, "token_id", None)
|
||||||
if token_id is not None:
|
if token_id is not None:
|
||||||
tracking.enqueue_counter_update(token_id)
|
tracking.enqueue_counter_update(token_id)
|
||||||
|
tracking.enqueue_matomo_event(
|
||||||
|
token_id=token_id,
|
||||||
|
path=request.path,
|
||||||
|
query_string=request.query_string.decode("utf-8", errors="replace"),
|
||||||
|
status_code=response.status_code,
|
||||||
|
user_agent=request.headers.get("User-Agent", ""),
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+55
-4
@@ -1,14 +1,16 @@
|
|||||||
|
import os
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
from src.api import tokens_db
|
from src.api import tokens_db
|
||||||
from src.utils import logger
|
from src.utils import logger
|
||||||
|
|
||||||
_STOP_SENTINEL = object()
|
_STOP_SENTINEL = object()
|
||||||
_queue: Optional[queue.Queue] = None
|
_queue: Optional[queue.Queue] = None
|
||||||
_worker_thread: Optional[threading.Thread] = None
|
_worker_thread: Optional[threading.Thread] = None
|
||||||
_db_path: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
def _worker_loop(q: queue.Queue, db_path: 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,
|
token_id,
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
elif kind == "matomo":
|
||||||
|
try:
|
||||||
|
_post_matomo(**payload)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
logger.warning("tracking: échec envoi Matomo", exc_info=True)
|
||||||
finally:
|
finally:
|
||||||
q.task_done()
|
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:
|
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():
|
if _worker_thread is not None and _worker_thread.is_alive():
|
||||||
return
|
return
|
||||||
_db_path = db_path
|
|
||||||
_queue = queue.Queue()
|
_queue = queue.Queue()
|
||||||
_worker_thread = threading.Thread(
|
_worker_thread = threading.Thread(
|
||||||
target=_worker_loop, args=(_queue, db_path), daemon=True
|
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."""
|
"""Attend que la queue soit drainée. Utile en test."""
|
||||||
if _queue is None:
|
if _queue is None:
|
||||||
return
|
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)
|
||||||
|
|||||||
@@ -34,3 +34,46 @@ def test_after_request_hook_increments_counter_async(api_client, valid_token_hea
|
|||||||
|
|
||||||
rows = tokens_db.list_tokens(db_path)
|
rows = tokens_db.list_tokens(db_path)
|
||||||
assert rows[0]["count_total"] == 1
|
assert rows[0]["count_total"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_matomo_disabled_skips_call(monkeypatch, api_client, valid_token_header):
|
||||||
|
monkeypatch.setenv("MATOMO_TRACKING_ENABLED", "false")
|
||||||
|
client, _ = api_client
|
||||||
|
|
||||||
|
from src.api import tracking
|
||||||
|
|
||||||
|
called = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
tracking,
|
||||||
|
"_post_matomo",
|
||||||
|
lambda **kw: called.append(kw),
|
||||||
|
)
|
||||||
|
client.get("/api/v1/health")
|
||||||
|
tracking.flush(timeout=2.0)
|
||||||
|
assert called == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_matomo_enabled_posts_event(monkeypatch, api_client, valid_token_header):
|
||||||
|
monkeypatch.setenv("MATOMO_TRACKING_ENABLED", "true")
|
||||||
|
monkeypatch.setenv("MATOMO_URL", "https://matomo.example/matomo.php")
|
||||||
|
monkeypatch.setenv("MATOMO_SITE_ID", "42")
|
||||||
|
|
||||||
|
from src.api import tracking
|
||||||
|
|
||||||
|
captured = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
tracking,
|
||||||
|
"_post_matomo",
|
||||||
|
lambda **kw: captured.append(kw),
|
||||||
|
)
|
||||||
|
|
||||||
|
client, _ = api_client
|
||||||
|
client.get("/api/v1/schema", headers=valid_token_header)
|
||||||
|
tracking.flush(timeout=2.0)
|
||||||
|
|
||||||
|
assert len(captured) == 1
|
||||||
|
call = captured[0]
|
||||||
|
assert call["params"]["idsite"] == "42"
|
||||||
|
assert call["params"]["rec"] == "1"
|
||||||
|
assert "token-" in call["params"]["uid"]
|
||||||
|
assert call["params"]["dimension2"] == "200"
|
||||||
|
|||||||
Reference in New Issue
Block a user