Saltar al contenido principal

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,
"recipients": ["[email protected]"],
"email_min_severity": "medium",
"repeat_email_minutes": 60,
"max_emails_per_hour": 30,
"retention_days": 90
}
CampoQué hace
enabledfalse apaga el guardado y los correos, sin desplegar nada
recipientsCorreos que reciben el aviso (máximo 10). Vacío: se guarda pero no se envía
email_min_severitycritical, high, medium o low. Por debajo se guarda sin correo
repeat_email_minutesCada cuánto se vuelve a avisar de un error que se sigue repitiendo
max_emails_per_hourTope de correos por hora y por instancia del backend
retention_daysDí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 details y hint, 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ó (su User-Agent), no el node del servidor.
  • La versión (release): el commit corto del build del frontend. Lo pasa deploy.sh como NEXT_PUBLIC_APP_VERSION al construir la imagen. Si un error trae un release anterior 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 marcada truncated. 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:

SeveridadCuándo
criticalCualquier 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
highGuardar algo en conversaciones, contactos o agenda. Una de esas pantallas caída. Una excepción sin capturar. Un permiso denegado (42501)
mediumLecturas, errores del tiempo real, pantallas caídas de otros módulos. Todo lo que llega sin sesión tiene aquí su techo
lowCortes 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​

  1. Pon tu correo en recipients y comprueba que SMTP_* esté configurado en el backend.
  2. Provoca un error. Por ejemplo, POST /api/errors/report con {"message":"prueba","kind":"global","source":"browser"} y el Bearer de tu sesión.
  3. Revisa la fila en platform_error_events y el correo. Si el correo no llega, busca en el log del backend aviso de error sin enviar o no se pudo enviar el aviso de error.