titusgiap's picture
Claude Opus 4.8
feat(analytics): tab "Quản lý thời lượng" theo dõi người dùng cho quản trị
00d2c12
Raw History Blame Contribute Delete
2.39 kB
"""Ngữ cảnh theo dõi đi kèm mỗi request (user / session / task / feature).
VÌ SAO LÀ HEADER, KHÔNG PHẢI BODY:
Job được tạo ở ~15 endpoint với ~15 model Pydantic khác nhau, lại có cả
endpoint nhận multipart (upload). Thêm 6 trường vào từng model là sửa rải
rác và rất dễ sót. Frontend gắn header `X-TNT-*` một chỗ duy nhất (bản vá
`fetch` trong `lib/track.js`), backend đọc một chỗ duy nhất ở đây, và
`jobs.create()` tự nhặt — endpoint không phải biết gì về tracking.
Dùng ContextVar nên an toàn với cả endpoint `async def` lẫn `def` (Starlette
chạy endpoint đồng bộ trong threadpool có sao chép context).
"""
from __future__ import annotations
from contextvars import ContextVar
_ctx: ContextVar[dict] = ContextVar("tnt_track_ctx", default={})
HEADERS = {
"user_id": "x-tnt-user",
"session_id": "x-tnt-session",
"task_id": "x-tnt-task",
"feature": "x-tnt-feature",
"phase": "x-tnt-phase",
"attempt": "x-tnt-attempt",
}
def get_ctx() -> dict:
return _ctx.get() or {}
class TrackContextMiddleware:
"""ASGI middleware thuần — CỐ Ý không dùng BaseHTTPMiddleware.
BaseHTTPMiddleware bọc response vào một luồng trung gian; app này phục vụ
file video lớn bằng FileResponse nên không đáng đánh đổi. Ở đây chỉ đọc vài
header rồi đặt ContextVar, không đụng gì tới request/response.
"""
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope.get("type") != "http":
return await self.app(scope, receive, send)
raw = {}
for k, v in scope.get("headers") or ():
key = k.decode("latin-1").lower()
if key.startswith("x-tnt-"):
raw[key] = v.decode("latin-1", "ignore")[:120]
ctx = {k: raw.get(v, "") for k, v in HEADERS.items()}
try:
ctx["attempt"] = int(ctx["attempt"]) if ctx["attempt"] else None
except (TypeError, ValueError):
ctx["attempt"] = None
ctx["user_id"] = ctx["user_id"].strip().lower()
token = _ctx.set(ctx if ctx["user_id"] else {})
try:
await self.app(scope, receive, send)
finally:
_ctx.reset(token)