feat: frontend error capture — diagnostic instantané pour Hermes
- app.js: intercepte window.onerror + unhandledrejection Envoie automatiquement au backend via POST /api/frontend-error Déduplication, throttling 1/sec, max 50 erreurs buffer local window.__flowdeck_errors accessible en console debug - api.py: 2 nouvelles routes POST /api/frontend-error — reçoit erreurs JS, déduplique, logge GET /api/frontend-errors?clear=true — Hermes lit les erreurs - csrf.py: exemption /api/frontend-error du CSRF Usage Hermes après chaque déploiement: curl -s http://localhost:8080/api/frontend-errors | jq . → Voir TOUTES les erreurs JS en temps réel
This commit is contained in:
@@ -16,7 +16,7 @@ class CSRFMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
|
||||
SAFE_METHODS = {"GET", "HEAD", "OPTIONS"}
|
||||
EXCLUDED_PATHS = {"/api/webhook", "/api/v1", "/auth/callback", "/auth/register", "/auth/local-login", "/api/user", "/board/api/pages", "/board/api/favorites", "/api/workspace", "/api/local-workspace", "/api/settings", "/db/", "/workspace"}
|
||||
EXCLUDED_PATHS = {"/api/webhook", "/api/v1", "/auth/callback", "/auth/register", "/auth/local-login", "/api/user", "/board/api/pages", "/board/api/favorites", "/api/workspace", "/api/local-workspace", "/api/settings", "/db/", "/workspace", "/api/frontend-error"}
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
# Webhook receiver, OAuth callback, and internal API are exempt
|
||||
|
||||
@@ -623,3 +623,55 @@ async def get_collaborators(owner: str, repo: str):
|
||||
return {"collaborators": collaborators}
|
||||
except Exception as e:
|
||||
return {"collaborators": [], "error": str(e)}
|
||||
|
||||
|
||||
# ── Frontend Error Capture ───────────────────────────────────
|
||||
# Hermes diagnostique le frontend en appelant GET /api/frontend-errors
|
||||
|
||||
_frontend_errors: list[dict] = []
|
||||
_MAX_STORED_ERRORS = 100
|
||||
|
||||
|
||||
@router.post("/frontend-error")
|
||||
async def capture_frontend_error(request: Request):
|
||||
"""Reçoit les erreurs JS du navigateur. Appelé automatiquement par app.js."""
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return {"status": "ignored", "reason": "invalid json"}
|
||||
|
||||
msg = body.get("message", "")
|
||||
err_type = body.get("type", "error")
|
||||
source = body.get("source", "")
|
||||
line = body.get("line", 0)
|
||||
|
||||
# Dédupliquer les erreurs identiques consécutives
|
||||
if _frontend_errors and _frontend_errors[-1].get("message") == msg:
|
||||
_frontend_errors[-1]["count"] = _frontend_errors[-1].get("count", 1) + 1
|
||||
_frontend_errors[-1]["time"] = body.get("time", "")
|
||||
else:
|
||||
body["count"] = 1
|
||||
_frontend_errors.append(body)
|
||||
|
||||
while len(_frontend_errors) > _MAX_STORED_ERRORS:
|
||||
_frontend_errors.pop(0)
|
||||
|
||||
if err_type == "error":
|
||||
logger.warning("Frontend JS error: %s (%s:%s)", msg, source, line)
|
||||
else:
|
||||
logger.warning("Frontend unhandled rejection: %s", msg)
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/frontend-errors")
|
||||
async def get_frontend_errors(request: Request, clear: bool = True):
|
||||
"""Retourne les erreurs frontend récentes. Hermes appelle cette route."""
|
||||
errors = list(_frontend_errors)
|
||||
if clear:
|
||||
_frontend_errors.clear()
|
||||
return {
|
||||
"errors": errors,
|
||||
"count": len(errors),
|
||||
"cleared": clear,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,59 @@
|
||||
// FlowDeck Notion UI — Client-side logic
|
||||
// Alpine.js + SortableJS + HTMX + Mobile support
|
||||
|
||||
// ── Frontend Error Capture ──────────────────────────────────
|
||||
// Intercepte TOUTES les erreurs JS et les envoie au backend.
|
||||
// Hermes peut les consulter via GET /api/frontend-errors.
|
||||
(function() {
|
||||
'use strict';
|
||||
const MAX_ERRORS = 50;
|
||||
var _fd_errors = [];
|
||||
var _fd_throttle = 0;
|
||||
|
||||
function _fd_send(error) {
|
||||
_fd_errors.push(error);
|
||||
if (_fd_errors.length > MAX_ERRORS) _fd_errors.shift();
|
||||
// Throttle: max 1 POST per second
|
||||
var now = Date.now();
|
||||
if (now - _fd_throttle < 1000) return;
|
||||
_fd_throttle = now;
|
||||
try {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/frontend-error', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send(JSON.stringify(error));
|
||||
} catch(e) { /* fail silently */ }
|
||||
}
|
||||
|
||||
window.addEventListener('error', function(e) {
|
||||
_fd_send({
|
||||
type: 'error',
|
||||
message: e.message || 'Unknown error',
|
||||
source: e.filename || '',
|
||||
line: e.lineno || 0,
|
||||
col: e.colno || 0,
|
||||
stack: (e.error && e.error.stack) ? String(e.error.stack).substring(0, 2000) : '',
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent.substring(0, 200),
|
||||
time: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', function(e) {
|
||||
_fd_send({
|
||||
type: 'unhandledrejection',
|
||||
message: e.reason ? (e.reason.message || String(e.reason)) : 'Promise rejected',
|
||||
stack: (e.reason && e.reason.stack) ? String(e.reason.stack).substring(0, 2000) : '',
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent.substring(0, 200),
|
||||
time: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Expose pour le debug console
|
||||
window.__flowdeck_errors = _fd_errors;
|
||||
})();
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user