Engineering Story · 2 de julio de 2026

El bug que nunca encontramos

Cómo un problema de pagos terminó obligándonos a construir observabilidad aplicada.

ObservabilidadPagosSistemas distribuidosPHPPython

Viernes — 5:42 PM

Seis meses de trabajo finalmente habían llegado a producción.

La integración de pagos no era un feature pequeño. Tocaba directamente el corazón del ERP: el punto de venta.

El acuerdo era salir de forma gradual. Una sucursal primero. Si todo se comportaba bien, después vendrían las demás.

La prueba piloto parecía exitosa. Las transacciones fluían, los logs no mostraban nada extraño y todo se comportaba como esperábamos para un feature nuevo en producción.

Cerré mi laptop convencido de que la parte más difícil ya había quedado atrás.

El lunes por la mañana empezó a sonar mi teléfono.

Incident

Un encargado no estaba seguro de si a un cliente realmente se le había cobrado.

El banco mostraba la transacción.

Netpay mostraba la transacción.

El ERP no.

No había huella de una respuesta final. Solo sabíamos que Metrify había enviado la solicitud, que existía un nuevo folio y que la venta no se había cerrado.

En ese momento no pensé que fuera un bug.

Después de tantas pruebas previas, lo primero fue tratarlo como un incidente aislado. Algo incómodo, sí, pero aislado.

El cliente seguía esperando en mostrador. Su aplicación bancaria ya mostraba el cargo. El encargado necesitaba una respuesta.

La pregunta no era técnica.

La pregunta era operativa.

¿El cliente podía llevarse los productos?

Timeline

Viernes · 5:42 PM

Salida a producción

La integración de pagos salió a producción después de meses de desarrollo, pruebas y validaciones.

Lunes por la mañana

Primera llamada

Una sucursal reportó una venta cobrada en banco y Netpay, pero no cerrada en el ERP.

Martes · 5:00 PM

Segundo incidente

Con más sucursales operando, apareció un caso similar: cliente esperando, banco con cargo y ERP sin cierre.

Section

El despliegue gradual

flowchart LR
A["Piloto"] --> B["1 sucursal"]
B --> C["3 sucursales"]
C --> D["7 sucursales"]
D --> E["14 sucursales"]

B:::active

classDef active fill:#1f2937,stroke:#facc15,color:#ffffff

La primera semana no queríamos probar una idea en abstracto. Queríamos ver cómo se comportaba el flujo en una sucursal real, con cajeros reales, clientes reales y ventas reales.

El problema fue que los incidentes no aparecían de forma ordenada.

No fallaba siempre la misma terminal.

No fallaba siempre la misma sucursal.

No fallaba siempre el mismo punto del flujo.

De mil transacciones podían fallar una o dos.

Pero cuando fallaban, no dejaban suficiente rastro.

Section

Cuando dejó de parecer aislado

El martes instalamos terminales en más sucursales.

A las cinco de la tarde apareció otro caso.

Un cliente estaba en mostrador. El pago aparecía en su aplicación bancaria, pero el ERP seguía mostrando la venta como pendiente.

Otra vez las mismas preguntas.

¿Debía salir el cliente con la mercancía?

¿Debía el cajero cobrar otra vez?

¿O todos debían esperar?

Nadie tenía evidencia suficiente.

Solo supuestos.

Ya no estábamos buscando un bug.

Estábamos intentando saber en qué punto la transacción había dejado de dejar huella.

Con el segundo problema empezamos a revisar todo.

Logs de Apache.

Logs de PHP.

Errores fatales.

Excepciones.

Sockets.

Base de datos.

Nada explicaba el comportamiento.

No era que el sistema estuviera explotando con un error evidente. Era peor. El flujo parecía normal hasta que, en algún punto, simplemente dejábamos de ver la transacción.

Section

El flujo de pago

sequenceDiagram
autonumber
participant ERP
participant Netpay
participant Terminal
participant Banco

ERP->>Netpay: POST Sale
Netpay-->>ERP: 200 OK
Netpay->>Terminal: Enviar cobro
Terminal->>Banco: Autorización
Banco-->>Terminal: Aprobado
Terminal-->>Netpay: Resultado
Netpay-->>ERP: Webhook
ERP->>ERP: Cerrar venta

A primera vista, la arquitectura parecía directa.

Una petición. Un gateway. Una terminal. Un banco.

Pero la realidad era distinta.

Un solo pago cruzaba sistemas independientes, cada uno con sus propios tiempos, reintentos, logs, modos de falla y, al final, su propia versión de la verdad.

Engineering Decision

El problema no era simplemente que algunos pagos fallaran.

El verdadero problema era que nadie podía explicar en qué punto una transacción había desaparecido para el ERP.

Ese descubrimiento cambió el objetivo.

Section

Siete sucursales después

Con siete sucursales operando, el volumen cambió por completo.

Cada sucursal podía tener más de una terminal. Las transacciones ya no ocurrían en serie. Mientras una venta estaba enviando su solicitud, otra podía estar esperando respuesta y otra podía estar recibiendo un webhook.

El log crecía por tiempo, no por transacción.

JSONL stream · 7 sucursales · 2 terminales por sucursal

08:35:10.102   S10-T1   pre-request   oDMfPMBz...
08:35:10.448   S03-T2   pre-request   C7ai-tOW0...
08:35:11.006   S10-T1   post-response   oDMfPMBz...
08:35:11.391   S07-T1   webhook-received   L9kaP02m...
08:35:11.884   S02-T2   pre-request   aQm91ZxK...
08:35:12.120   S03-T2   post-response   C7ai-tOW0...
08:35:12.612   S06-T1   pre-request   R81mKQpl...
08:35:13.048   S14-T2   post-response   N8xw0Paa...
08:35:13.441   S01-T1   webhook-received   kP91aQx2...
08:35:13.982   S10-T1   webhook-missing   oDMfPMBz...

Eso significaba que una sola venta quedaba mezclada entre cientos de eventos de otras sucursales.

Buscar por monto, cliente, folio o terminal todavía servía en algunos casos, pero ya no era sostenible.

Había momentos en los que teníamos que entrar a datos, revisar tablas y reconstruir la venta manualmente mientras el cliente seguía esperando.

Ahí entendimos que el problema ya no era solamente tener logs.

El problema era no tener contexto.

Section

Necesitábamos otra forma de buscar

No empezamos con un dashboard.

No empezamos con alertas.

No empezamos con una plataforma completa.

Empezamos con algo más pequeño: cada transacción necesitaba una identidad.

Así nació el correlation_id.

No era el héroe de la historia. Era una herramienta de apoyo.

La idea importante era otra: necesitábamos observabilidad aplicada al flujo real de pagos.

Section

La primera evidencia

Evidencia cruda de la transacción

post-response · solicitud de pago aceptada

{
"stage": "post-response",
"method": "POST",
"endpoint": "/gateway/integration-service/transactions/sale",
"http_status": 200,
"timestamp_iso": "2025-10-06T08:39:30-06:00",
"correlation_id": "rTAOeW0rc80QdKWaaHMdk",
"response": {
  "code": "00",
  "message": "Mensaje enviado exitosamente"
},
"latency_ms": 1639.24
}

Desde ese momento ya podíamos identificar mejor cada transacción.

Todavía era manual. Muchas veces era descargar el log, abrirlo, usar Cmd + F y buscar el correlation_id.

Pero ya teníamos algo que antes no existía.

Un punto de partida.

Correlation ID

rTAOeW0rc80QdKWaaHMdk

Stage

post-response

HTTP Status

200 OK

Latency

1639.24 ms

Observed result

El gateway aceptó la solicitud, pero eso no demostraba que el ERP hubiera recibido la confirmación final del pago.

La diferencia importante

Netpay aceptó la solicitud y regresó 200 OK.

Pero el ERP seguía sin confirmación final del pago.

La petición no había fallado.

El estado de la transacción se había vuelto ambiguo.

Failure Mode

Pago ambiguo

La petición fue aceptada por el gateway, pero el ERP nunca recibió el estado final necesario para cerrar la venta.

Operativamente, esto era peor que una falla limpia.

Una petición fallida se puede reintentar.

Un pago ambiguo se tiene que investigar.

Section

Cuando una transacción empezó a tener recorrido

Correlation Journey

rTAOeW0rc80QdKWaaHMdk

POST Request

El ERP envió la solicitud de pago.

Gateway Response

El gateway regresó HTTP 200.

Terminal

El cliente completó el pago.

Webhook

El callback nunca fue recibido.

ERP Update

La venta permaneció pendiente.

El correlation_id no estuvo en todos los sistemas externos.

Pero sí estuvo donde más lo necesitábamos: en nuestra evidencia.

Nos permitió seguir una transacción desde que salía del ERP, comparar sus estados y saber en qué momento dejaba de avanzar para nosotros.

Section

La primera herramienta

Con catorce sucursales operando, buscar manualmente dejó de tener sentido.

Los eventos llegaban mezclados. Las transacciones eran paralelas. El archivo JSONL seguía creciendo.

Necesitábamos una herramienta que recibiera un correlation_id, encontrara todos los eventos relacionados y reconstruyera el recorrido completo.

Así nació el primer analizador.

python analyze.py logs.jsonl —cid rTAOeW0rc80QdKWaaHMdk

Searching…

Found 8 events

Last Stage: POST Response

Webhook: NOT FOUND

Diagnosis: Gateway accepted request. No callback was received.

Metric

Tiempo promedio de investigación

Before

30 min

After

30 seg

Treinta minutos no era la mejora real.

La mejora real era la confianza.

Por primera vez podíamos responderle a una sucursal con evidencia.

Ya no empezábamos preguntando si era la red, la terminal, el ERP o el proveedor.

Empezábamos siguiendo la transacción.

Section

Lo que realmente construimos

Durante semanas pensé que estábamos estabilizando una integración de pagos.

No era eso.

Estábamos construyendo observabilidad.

La pasarela de pagos fue solamente el problema que nos obligó a hacerlo.

Hoy, cuando una sucursal llama preguntando qué ocurrió con una transacción, ya no empezamos adivinando.

Empezamos trazando.

Y cuando trabajas con dinero, eso cambia todo.

Section

La herramienta

La herramienta terminó publicada como Open Source después de que el problema dejó de ser urgente.

No nació para GitHub.

Nació para producción.

Para que el analizador pudiera reconstruir una transacción, cada evento debía contener, como mínimo:

Con esos datos era posible reconstruir, en segundos, el recorrido completo de una transacción.

La implementación completa se encuentra disponible como proyecto Open Source.

GitHub

Payment Correlation Analyzer

Herramienta Open Source para analizar archivos JSONL, reconstruir transacciones mediante Correlation ID y generar diagnósticos automáticamente.

Analizador JSONL
Reconstrucción por Correlation ID
Línea de tiempo de eventos
Diagnóstico automático
Ejemplos reales de logs
View repository →

La recomendación no es específica para Netpay.

Lo que realmente aprendimos

En sistemas donde circula dinero, el objetivo no es eliminar todos los errores.

El objetivo es que ningún error vuelva a ser un misterio.

Ese día dejamos de construir solamente un ERP.

Empezamos a construir observabilidad.

Section

Temas aprendidos

Este incidente terminó convirtiéndose en la base de varios temas que hoy forman parte de cómo construimos Metrify.

Observabilidad aplicada

Ver el comportamiento real del sistema sin depender de suposiciones.

Correlation ID

Seguir una transacción entre distintos eventos, logs y estados.

Trazabilidad

Reconstruir el recorrido completo de una operación crítica.

JSONL

Registrar eventos estructurados en orden cronológico y fáciles de analizar.

Debugging en producción

Investigar bajo presión usando evidencia, no intuición.

Sistemas distribuidos

Aceptar que ningún sistema posee por sí solo toda la verdad.

Cada uno de estos temas merece su propio artículo.

Pero todos nacieron del mismo problema: necesitábamos dejar de adivinar.