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"}
|
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):
|
async def dispatch(self, request: Request, call_next):
|
||||||
# Webhook receiver, OAuth callback, and internal API are exempt
|
# Webhook receiver, OAuth callback, and internal API are exempt
|
||||||
|
|||||||
@@ -623,3 +623,55 @@ async def get_collaborators(owner: str, repo: str):
|
|||||||
return {"collaborators": collaborators}
|
return {"collaborators": collaborators}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"collaborators": [], "error": str(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
|
// FlowDeck Notion UI — Client-side logic
|
||||||
// Alpine.js + SortableJS + HTMX + Mobile support
|
// 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() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user