"""Diagnostics structurés de la GUI V6 (E2/E3) — RGPD strict. On n'émet QUE des métadonnées techniques liste-blanche : type d'exception (nom de classe), catégorie d'erreur d'un ensemble fermé, statut, ordinal, durée. JAMAIS de nom/chemin/texte de document, ni de message d'exception brut. L'envoi est non bloquant : un échec réseau n'interrompt jamais le traitement. Patron : gui_v6/usage_telemetry.py (télémétrie d'usage). """ from __future__ import annotations import json import uuid from pathlib import Path from typing import Any, Callable, Iterable, Optional # Clés autorisées par item de diagnostic (filtre RGPD appliqué à la construction). _ALLOWED_ITEM_KEYS = {"ordinal", "status", "error_type", "error_code", "duration_ms"} REPORT_PATH = "/api/v1/diagnostics/report" def new_run_id() -> str: return uuid.uuid4().hex def items_from_summary(summary: Any) -> list[dict]: """Extrait les items de diagnostic (RGPD-safe) d'un ``RunSummary``. Ne lit que les attributs autorisés ; aucun nom/chemin/message n'est lu. """ items: list[dict] = [] for item in getattr(summary, "documents", None) or []: items.append( { "ordinal": getattr(item, "ordinal", 0), "status": getattr(item, "status", "success"), "error_type": getattr(item, "error_type", None), "error_code": getattr(item, "error_code", None), "duration_ms": getattr(item, "duration_ms", None), } ) return items def build_diagnostics_payload( *, run_id: str, app_name: str, app_version: Optional[str], license_ref: Optional[str], machine_id: Optional[str], duration_ms: Optional[int], items: Iterable[dict], ) -> dict: """Construit le payload diagnostic. Chaque item est filtré aux seules clés autorisées → aucun nom/chemin/message ne peut fuir, même fourni par erreur.""" clean_items: list[dict] = [] succeeded = failed = 0 for raw in items: it = {k: raw[k] for k in _ALLOWED_ITEM_KEYS if k in raw} status = it.get("status") if status == "success": succeeded += 1 elif status == "failed": failed += 1 clean_items.append(it) return { "run_id": run_id, "license_ref": license_ref, "machine_id": machine_id, "app_name": app_name, "app_version": app_version, "duration_ms": duration_ms, "document_count": len(clean_items), "succeeded_count": succeeded, "failed_count": failed, "items": clean_items, }