feat: filtro de severidad (Alta/Media/Baja) en el log de errores

Se asigna sola, sin etiquetar nada a mano: backend y errores de render de React (rompen toda la
pantalla) = alta; errores JS sueltos = media; promesas rechazadas sin manejar = baja (suelen ser
fallos de red puntuales, no criticos). Nuevo filtro en el panel ademas del de origen ya existente.

Migracion 014_error_logs_severity.sql (ALTER TABLE ADD COLUMN IF NOT EXISTS). Probado en dev: las
tres severidades se asignan correctamente segun el origen, un valor invalido del frontend cae en
media, y el filtro combinado con origen funciona.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-17 14:50:36 -05:00
parent a06df86881
commit feadd2e715
14 changed files with 81 additions and 23 deletions
@@ -19,6 +19,7 @@ export class ErrorBoundary extends Component {
message: error?.message || 'Error de render',
stack: `${error?.stack || ''}\n${info?.componentStack || ''}`.slice(0, 5000),
url: window.location.href,
severity: 'alta', // rompe toda la pantalla, no solo una parte
});
}
+10
View File
@@ -87,6 +87,11 @@ export const messages = {
error_logs_clear: 'Limpiar registro',
error_logs_clear_confirm_msg: '¿Seguro que deseas borrar todo el historial de errores registrados? No se puede deshacer.',
error_logs_empty: 'No hay errores registrados.',
error_logs_source: 'Origen',
error_logs_severity: 'Severidad',
error_logs_severity_alta: 'Alta',
error_logs_severity_media: 'Media',
error_logs_severity_baja: 'Baja',
password_show: 'Mostrar clave',
password_hide: 'Ocultar clave',
common_new: 'Nuevo',
@@ -358,6 +363,11 @@ export const messages = {
error_logs_clear: 'Clear log',
error_logs_clear_confirm_msg: 'Are you sure you want to delete the entire error log history? This cannot be undone.',
error_logs_empty: 'No errors logged.',
error_logs_source: 'Source',
error_logs_severity: 'Severity',
error_logs_severity_alta: 'High',
error_logs_severity_media: 'Medium',
error_logs_severity_baja: 'Low',
password_show: 'Show password',
password_hide: 'Hide password',
common_new: 'New',
@@ -7,11 +7,14 @@ import { Button } from '../../components/Button.jsx';
import { Badge } from '../../components/Badge.jsx';
import { ComboBox } from '../../components/ComboBox.jsx';
const SEVERITY_COLOR = { alta: 'danger', media: 'warning', baja: 'success' };
export default function ErrorLogsPanel() {
const { t, pushToast, openDialog } = useUi();
const runAction = useAction();
const [source, setSource] = useState('');
const { data: logs, loading, reload } = useAsync(() => listErrorLogs({ source }), [source]);
const [severity, setSeverity] = useState('');
const { data: logs, loading, reload } = useAsync(() => listErrorLogs({ source, severity }), [source, severity]);
function confirmClear() {
openDialog({
@@ -34,7 +37,7 @@ export default function ErrorLogsPanel() {
return (
<div>
<div style={{ display: 'flex', gap: 10, marginBottom: 12, alignItems: 'center' }}>
<div style={{ display: 'flex', gap: 10, marginBottom: 12, alignItems: 'center', flexWrap: 'wrap' }}>
<div style={{ width: 160 }}>
<ComboBox
options={[
@@ -42,7 +45,18 @@ export default function ErrorLogsPanel() {
{ value: 'backend', label: 'Backend' },
{ value: 'frontend', label: 'Frontend' },
]}
value={source} onChange={setSource}
value={source} onChange={setSource} placeholder={t('error_logs_source')}
/>
</div>
<div style={{ width: 160 }}>
<ComboBox
options={[
{ value: '', label: t('common_all') },
{ value: 'alta', label: t('error_logs_severity_alta') },
{ value: 'media', label: t('error_logs_severity_media') },
{ value: 'baja', label: t('error_logs_severity_baja') },
]}
value={severity} onChange={setSeverity} placeholder={t('error_logs_severity')}
/>
</div>
<Button variant="standard" icon={Trash2} onClick={confirmClear} disabled={!logs || logs.length === 0}>
@@ -56,7 +70,8 @@ export default function ErrorLogsPanel() {
{(logs || []).map((log) => (
<div key={log.id} className="panel-card" style={{ marginBottom: 8 }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6 }}>
<Badge color={log.source === 'backend' ? 'danger' : 'warning'}>{log.source}</Badge>
<Badge color={SEVERITY_COLOR[log.severity] || 'muted'}>{t(`error_logs_severity_${log.severity}`)}</Badge>
<Badge color="muted">{log.source}</Badge>
<span className="text-muted" style={{ fontSize: '.72rem' }}>{String(log.created_at).slice(0, 16)}</span>
{log.user_name && <span className="text-muted" style={{ fontSize: '.72rem' }}>· {log.user_name}</span>}
</div>
+8 -4
View File
@@ -5,26 +5,30 @@ const MAX_REPORTS_PER_SESSION = 20;
let reportedCount = 0;
let installed = false;
function send(message, stack) {
function send(message, stack, severity) {
if (reportedCount >= MAX_REPORTS_PER_SESSION) return;
reportedCount += 1;
reportError({
message: String(message ?? 'Error desconocido').slice(0, 2000),
stack: stack ? String(stack).slice(0, 5000) : undefined,
url: window.location.href,
severity,
});
}
// Captura errores JS no manejados y promesas rechazadas sin catch en toda la app — llamar UNA
// vez al arrancar (main.jsx). Los errores de render de React los captura ErrorBoundary aparte.
// vez al arrancar (main.jsx). Los errores de render de React los captura ErrorBoundary aparte
// (severidad "alta" ahi, rompen toda la pantalla).
export function installGlobalErrorReporter() {
if (installed) return;
installed = true;
// Error JS suelto: la pagina suele seguir funcionando parcialmente.
window.addEventListener('error', (event) => {
send(event.message, event.error?.stack);
send(event.message, event.error?.stack, 'media');
});
// Promesa rechazada sin catch: casi siempre un fallo de red/API puntual, no critico.
window.addEventListener('unhandledrejection', (event) => {
const reason = event.reason;
send(reason?.message || String(reason), reason?.stack);
send(reason?.message || String(reason), reason?.stack, 'baja');
});
}