Saltar a contenido

Observabilidad de errores — diagnóstico y estado deseado

Fecha: 2026-07-28 Alcance: backend/src/instrument.ts, backend/src/common/filters/*, backend/src/common/sentry/*, backend/src/common/sap-client/*, backend/src/modules/*/**.service.ts, frontend/src/utils/omsBackend/omsBackend.ts, stack de infraestructura (GlitchTip, logs). Documento hermano: plan-implementacion.md — mapa técnico de trabajo, fase por fase.


1. Tesis

Cuando un usuario reporta un error del OMS, el equipo busca en GlitchTip y no lo encuentra, o encuentra un evento genérico sin contexto. No es un problema de GlitchTip: la instrumentación del backend está bien montada en su esqueleto (SDK inicializado antes de Nest, filtro global, interceptor de scope con usuario y sucursal), pero tres decisiones puntuales hacen que:

  1. Los errores que los usuarios efectivamente reportan (errores de negocio de SAP, status 4xx) se descartan antes de enviarse a GlitchTip.
  2. El ID de referencia que ve el usuario no existe como dato buscable en GlitchTip ni en ningún otro lado.
  3. El contexto rico (payload enviado a SAP, respuesta de SAP, duración) se queda en la consola y el evento de GlitchTip llega desnudo y mal agrupado.

A esto se suma un faltante estructural: no hay logs estructurados ni agregados — no existe forma de reconstruir "todo lo que pasó en el request X".

2. Cómo funciona hoy la cadena de captura

Usuario → Frontend (Axios) → Backend NestJS → SAP Service Layer
                                  │
                 ┌────────────────┼──────────────────────┐
                 │                │                      │
        SentryScopeInterceptor  SapErrorFilter      Logger (consola)
        (user, branch,          (@SentryExceptionCaptured   (texto plano,
         request_id tag)         → GlitchTip, filtrado       stdout, sin
                                 por beforeSend)             agregación)
  • Init: backend/src/instrument.ts — SDK @sentry/nestjs se inicializa como primera línea de main.ts. Correcto.
  • Scope: backend/src/common/sentry/sentry-scope.interceptor.ts — setea user, tags branch, position, request_id. Correcto en intención.
  • Filtro: backend/src/common/filters/sap-error.filter.ts — catch-all global; transforma AxiosError de SAP en SapBusinessException, responde JSON seguro con referenceId.
  • Cliente SAP: backend/src/common/sap-client/sap-client.module.ts — interceptores que loguean cada request/response a SAP con duración, y en error el body de respuesta y el body enviado.
  • Servicios: ~90 catch que envuelven el error de SAP en BadRequestException con el mensaje interpolado a mano.

3. Los problemas, con evidencia

# Problema Dónde Consecuencia
P1 beforeSend descarta todo status < 500. Los servicios envuelven los errores de SAP en BadRequestException (400) y SapBusinessException usa 400 por defecto. instrument.ts:24-31, inventory.service.ts:161 (patrón ×90 en modules/), sap-business.exception.ts:44 Los errores de negocio de SAP —los que los usuarios reportan— nunca llegan a GlitchTip. Se busca donde el error, por diseño, no está.
P2 Dos IDs de correlación desconectados. El interceptor genera request_id (tag en GlitchTip); el filtro genera un referenceId distinto con Math.random() que es el que ve el usuario en el JSON de error. Ninguno se devuelve como header ni viaja desde el frontend. sentry-scope.interceptor.ts:24-25, sap-error.filter.ts:170-172 El ID que el usuario podría reportar no es buscable en GlitchTip; el ID buscable nunca se le muestra. No hay hilo reporte → evento → logs.
P3 El evento llega crudo y mal agrupado. @SentryExceptionCaptured() captura el AxiosError original ("Request failed with status code 400"), sin mensaje real de SAP, sin payload, sin fingerprint. GlitchTip agrupa peor que Sentry: todos los errores SAP caen en 1-2 issues genéricos por status code. sap-error.filter.ts:37 Imposible distinguir "stock insuficiente" de "cliente bloqueado" en el listado de issues. Triaje manual leyendo eventos uno por uno.
P4 El contexto rico muere en stdout. El sap-client loguea el mensaje real de SAP, el body de respuesta y el body enviado — pero vía Logger de Nest (escribe a process.stdout directo; Sentry no lo captura como breadcrumb). sap-client.module.ts:216-242 La información que resolvería el diagnóstico existe, pero solo en la consola del contenedor, sin búsqueda ni retención.
P5 El wrapping manual pierde la causa. new BadRequestException(msg) sin { cause } descarta stack y error original. Patrón repetido ~90 veces con interpolación manual de error.response?.data?.error?.message?.value. inventory.service.ts:159-164 y ~90 sitios más Aun si el evento llegara a GlitchTip, no tendría el stack real. Además: 90 lugares para mantener el mismo boilerplate.
P6 Errores tragados en silencio. Catches que solo hacen logger.warn y continúan; schedulers y listeners (Bancard QR, notificaciones) capturan sus propios errores sin reportar a GlitchTip. inventory.service.ts:97-103, bancard-qr.cancel.scheduler.ts, notifications/*.listener.ts Fallas de background jobs invisibles salvo lectura de consola en vivo.
P7 Sin logs estructurados ni agregación. Logger default de Nest, texto plano, sin request_id automático, retención = vida del contenedor. todo el backend No se puede responder "¿qué pasó en el request abc-123?" ni "¿cuántas veces falló X esta semana?".
P8 Sin alertas. GlitchTip soporta alertas por issue nuevo / umbral; no están configuradas hacia ningún canal del equipo. infra GlitchTip El equipo se entera por el usuario, nunca antes.
P9 Frontend y backend no correlacionan. El frontend tiene su propio GlitchTip (instrumentation-client.ts, sentry.server.config.ts) pero no comparte ningún ID con el backend; el interceptor Axios (omsBackend.ts:78) no envía X-Request-Id. frontend/src/utils/omsBackend/omsBackend.ts Un error visto en el frontend no se puede unir con su contraparte backend.

4. Estado deseado

Principio rector: de cualquier reporte de usuario al diagnóstico completo en menos de 2 minutos, con un solo dato: el ID de referencia.

Dimensión Hoy Deseado
Cobertura Solo 5xx y excepciones no controladas llegan a GlitchTip Todo error que un usuario percibe llega a GlitchTip (errores de negocio SAP como warning, 5xx como error). Se filtra solo ruido real: validación de DTOs, 401/403/404.
Correlación 2 IDs desconectados, ninguno cierra el círculo Un solo request_id: nace en el frontend (o en el edge), viaja en header X-Request-Id, es tag en ambos GlitchTip (front y back), aparece en el JSON de error, en el header de respuesta, en el toast que ve el usuario, y en cada línea de log del request.
Contexto por evento AxiosError genérico sin payload Cada evento lleva: mensaje real de SAP, código SAP, endpoint SAP llamado, body enviado (truncado/redactado), body de respuesta, duración, usuario, sucursal.
Agrupación 1-2 issues genéricos por status code Fingerprint por errorCode + ruta → un issue por tipo de problema real.
Causa raíz Stack perdido al envolver Cadena de causas preservada ({ cause }); los servicios lanzan SapBusinessException.fromSapError() en vez de formatear a mano.
Background jobs Errores invisibles Helper captureJobError() en schedulers/listeners con tag job:<nombre>.
Logs Texto plano, efímero JSON estructurado (pino) con request_id automático, agregado en Grafana Loki, retención ≥ 30 días, buscable por request_id/usuario/endpoint.
Alertas Ninguna Issue nuevo o pico de frecuencia → notificación al canal del equipo.
UX del error Mensaje + ref que no sirve para buscar Toast/pantalla de error muestra Ref: <request_id> con botón "copiar detalles". El usuario reporta con el ID; soporte pega el ID en GlitchTip o Loki y ve todo.

5. Qué NO está en alcance (por ahora)

  • Tracing distribuido / performance (tracesSampleRate > 0): se evalúa como segunda etapa, cuando la captura de errores esté sana. La duración de llamadas SAP ya se loguea y pasará a Loki, que cubre el 80 % de la necesidad.
  • Migrar de GlitchTip a Sentry SaaS: no hace falta. Los problemas son de instrumentación, no de la herramienta. El fingerprint manual compensa la agrupación más débil de GlitchTip.
  • Reemplazar el patrón de manejo de errores de los 90 catch de una sola vez: se hace incremental, módulo por módulo, empezando por los de mayor tráfico de reportes (ver plan).

6. Resultado esperado

  • Un reporte de usuario ("me dio error, ref abc-123") se resuelve buscando el tag request_id:abc-123 en GlitchTip → evento con contexto SAP completo → si hace falta más, misma búsqueda en Loki reconstruye el request entero.
  • El equipo detecta errores nuevos por alerta antes de que lleguen 3 reportes.
  • Métrica de éxito propuesta: tiempo mediano de diagnóstico (reporte → causa identificada) y % de reportes con evento encontrado en GlitchTip (hoy ≈ bajo; objetivo > 95 %).