"""Client-side boot/render diagnostic telemetry. The route is intentionally public: it is used when the SPA cannot render enough UI to let an authenticated operator report the failure from inside the app. Only structural counts and short error summaries are logged; page text is not accepted or persisted. """ from __future__ import annotations import json import logging from typing import Any from fastapi import APIRouter, Request, status from pydantic import BaseModel, ConfigDict, Field router = APIRouter(tags=["diagnostics"]) logger = logging.getLogger("vignette.client_diagnostics") class ClientDiagnosticError(BaseModel): model_config = ConfigDict(extra="ignore") kind: str = "" detail: str = "" at: str | None = None class ClientDiagnosticRequest(BaseModel): model_config = ConfigDict(extra="ignore") reason: str = "unknown" path: str = "" href: str = "" asset: str = "" elapsed_ms: int | None = None body_text_length: int | None = None root_text_length: int | None = None root_child_count: int | None = None visible_nodes: int | None = None document_ready_state: str = "" viewport: str = "" user_agent: str = "" errors: list[ClientDiagnosticError] = Field(default_factory=list) def _short(value: Any, limit: int) -> str: text = "" if value is None else str(value) return text[:limit] def _bounded_int(value: int | None, upper: int = 1_000_000) -> int | None: if value is None: return None return max(0, min(int(value), upper)) def _diagnostic_log_payload(payload: ClientDiagnosticRequest, request: Request) -> dict[str, Any]: client_host = request.client.host if request.client else "" return { "reason": _short(payload.reason, 80), "path": _short(payload.path, 300), "href": _short(payload.href, 800), "asset": _short(payload.asset, 160), "elapsed_ms": _bounded_int(payload.elapsed_ms), "body_text_length": _bounded_int(payload.body_text_length), "root_text_length": _bounded_int(payload.root_text_length), "root_child_count": _bounded_int(payload.root_child_count, 50_000), "visible_nodes": _bounded_int(payload.visible_nodes, 50_000), "document_ready_state": _short(payload.document_ready_state, 40), "viewport": _short(payload.viewport, 80), "user_agent": _short(payload.user_agent, 300), "origin": _short(request.headers.get("origin"), 300), "referer": _short(request.headers.get("referer"), 800), "cf_ray": _short(request.headers.get("cf-ray"), 80), "client_host": _short(client_host, 80), "errors": [ { "kind": _short(error.kind, 80), "detail": _short(error.detail, 900), "at": _short(error.at, 80), } for error in payload.errors[:5] ], } @router.post("/client-diagnostics", status_code=status.HTTP_202_ACCEPTED) async def record_client_diagnostic( payload: ClientDiagnosticRequest, request: Request ) -> dict[str, bool]: """Log a minimal browser-side boot/render failure report.""" safe_payload = _diagnostic_log_payload(payload, request) logger.warning( "client_diagnostic %s", json.dumps(safe_payload, ensure_ascii=False, separators=(",", ":")), ) return {"ok": True}