Reporte de errores
Cada error inesperado de la app se guarda en platform_error_events y, según su severidad, se
avisa por correo con la traza. Se captura en cuatro sitios:
- en el navegador: pantallas caídas y errores sin capturar;
- en las Server Actions;
- en el servidor de Next;
- en el backend.
Decisión y motivos: ADR 0012.
Configurarlo
En /admin/settings/platform, la clave errors.reporting:
{
"enabled": true,
"email_min_severity": "medium",
"repeat_email_minutes": 60,
"max_emails_per_hour": 30,
"retention_days": 90
}
| Campo | Qué hace |
|---|---|
enabled | false apaga el guardado y los correos, sin desplegar nada |
recipients | Correos que reciben el aviso (máximo 10). Vacío: se guarda pero no se envía |
email_min_severity | critical, high, medium o low. Por debajo se guarda sin correo |
repeat_email_minutes | Cada cuánto se vuelve a avisar de un error que se sigue repitiendo |
max_emails_per_hour | Tope de correos por hora y por instancia del backend |
retention_days | Días que se guarda el histórico (mínimo 7). Lo purga el backend una vez al día |
El correo sale por el SMTP global (SMTP_HOST, SMTP_FROM_EMAIL…). Si no está configurado, los
errores se guardan igual y el backend deja un aviso en el log.
La clave errors.reporting la puede leer cualquier usuario logueado (así funciona
platform_settings). No pongas ahí nada que no sea operativo.
Qué se guarda
- La traza (hasta 8 KB), el mensaje (hasta 2 KB), el código de Postgres con su
detailsyhint, la pantalla, la acción, el usuario, el tenant y el navegador. En los errores de una Server Action, el navegador es el de quien la invocó (suUser-Agent), no elnodedel servidor. - La versión (
release): el commit corto del build del frontend. Lo pasadeploy.shcomoNEXT_PUBLIC_APP_VERSIONal construir la imagen. Si un error trae unreleaseanterior al desplegado, es una pestaña que no se recargó. - Lo que se intentaba guardar o consultar, en
context.input, tal cual: los argumentos de la Server Action (la ficha, la evolución, la cita, el contacto) o el cuerpo de la petición al backend. No se enmascara: lleva datos de pacientes (PHI). - Solo se quitan las credenciales (contraseñas, tokens, JWT, claves) y se controla el tamaño.
Cada texto se corta en 4000 caracteres y los arrays en 50 elementos. El contexto entero se
queda en ~30 KB y se recorren hasta 12 niveles de anidación (antes 6: el árbol de un documento
llegaba como
[depth]); si no cabe, queda una vista previa marcadatruncated. Los archivos solo se describen.
Por eso los destinatarios del correo tienen que ser personas autorizadas a ver historias clínicas, con un buzón del dominio de la empresa.
Severidad
La decide el backend:
| Severidad | Cuándo |
|---|---|
critical | Cualquier error de la historia clínica. La app entera caída (global-error). Un error de esquema (42703, 42P01, 42883, PGRST202/204/205), que le falla a todos |
high | Guardar algo en conversaciones, contactos o agenda. Una de esas pantallas caída. Una excepción sin capturar. Un permiso denegado (42501) |
medium | Lecturas, errores del tiempo real, pantallas caídas de otros módulos. Todo lo que llega sin sesión tiene aquí su techo |
low | Cortes de red del navegador (kind: network: Failed to fetch, Load failed) y pestañas con un build anterior al despliegue (kind: deploy_skew: Server Action … was not found, ChunkLoadError), también en la historia clínica. El resto |
Los cortes de red y el desfase de versión no son fallos del código, pero el usuario sí se entera:
el navegador le muestra «Se perdió la conexión…» o «Hay una versión nueva» con un botón
Recargar (ErrorListener). Los navegadores con el bundle anterior aún los mandan como
unhandled; el backend los reconoce por el mensaje.
Las respuestas de negocio esperadas no se reportan: lo firmado no se edita, el choque de agenda (409), la falta de sesión y el permiso de la API de agenda (401/403).
Consultar
El usuario ve un código de referencia en la pantalla de error. Es el id del evento:
select * from platform_error_events where id = '<código>';
Un renglón por error distinto, lo más reciente primero:
select * from platform_error_groups_v order by last_seen desc limit 50;
Todo lo de un tenant en la última semana:
select occurred_at, severity, module, action, message
from platform_error_events
where tenant_id = '<tenant>' and occurred_at > now() - interval '7 days'
order by occurred_at desc;
Cruzar el error del servidor de Next (con la traza real) con el de la pantalla (con el usuario):
select * from platform_error_events where context->>'digest' = '<digest>';
La tabla está cerrada al navegador: se consulta desde el SQL editor de Supabase.
Reportar desde código nuevo
En una Server Action, en la rama de error inesperado, con los argumentos de la acción en
input (nunca el cliente de Supabase):
import { reportActionError } from 'src/lib/error-reporting/report-server';
if (error) {
reportActionError('contacts.createContact', error, { kind: 'write', input: { payload } });
return null;
}
En el navegador:
import { reportClientError } from 'src/lib/error-reporting/report-client';
void reportClientError(error, { module: 'chat', action: 'chat.realtime', kind: 'realtime' });
En el backend, inyecta ErrorReportingService (el módulo es global) y llama a report(...).
Ninguna de las tres lanza ni espera. No las envuelvas en await dentro del camino de la acción.
Probar el envío
- Pon tu correo en
recipientsy comprueba queSMTP_*esté configurado en el backend. - Provoca un error. Por ejemplo,
POST /api/errors/reportcon{"message":"prueba","kind":"global","source":"browser"}y el Bearer de tu sesión. - Revisa la fila en
platform_error_eventsy el correo. Si el correo no llega, busca en el log del backendaviso de error sin enviarono se pudo enviar el aviso de error.