Los peores bugs de emap no dieron ni un error
Repasando el registro de sesiones de emap encontré un patrón: los fallos que más tiempo me costaron no lanzaron ninguna excepción. Un service worker que dejó de repartir despliegues, una API caída devolviendo 200, props que la librería descarta sin avisar y el bug que tuve que arreglar dos veces.
Llevo un registro de sesiones de emap desde el primer día. Al releerlo entero me encontré con algo incómodo: los bugs que más tiempo me han costado no rompieron nada. Ni excepción, ni pantalla roja, ni una línea en los logs. El sistema seguía funcionando, solo que peor, y nadie avisó.
El service worker que se quedó con una versión de hace seis días
Desplegaba, verificaba en local, todo bien. Y a quien ya hubiera entrado en la web una vez, no le llegaba nada.
El service worker servía el shell —HTML, JS y CSS— cache-first, y solo se refrescaba al subir el número de VERSION. Esa constante llevaba clavada en la v14 seis días: cada deploy salía correcto y se quedaba en el servidor sin que ningún visitante recurrente lo viera.
Lo que lo hacía perfecto como trampa: solo lo sufre quien ya te ha visitado antes. Yo verificaba cada despliegue en una pestaña limpia, y una visita nueva siempre recibe lo último. El único perfil afectado —el que vuelve— era justo el que yo nunca probaba.
Lo arreglé de raíz en vez de subir el número a mano: red primero, y la caché solo como respaldo.
// Antes: si estaba en caché, la red no se consultaba nunca.
event.respondWith(caches.match(event.request).then((c) => c || fetch(event.request)));
// Ahora: la red manda, y la caché es el plan B.
event.respondWith(fetch(event.request).catch(() => caches.match(event.request)));
El 200 que mentía
Un día la app empezó a decir "Ruta no disponible en este modo". /api/route devolvía 503 en producción, pero el geocodificador iba bien.
La causa raíz no estaba en mi código: la zona DNS del dominio se había reseteado a los nameservers de aparcamiento del proveedor, al tocar la configuración del correo días antes. Vercel ya no llegaba a los motores de routing de mi VPS. El VPS estuvo sano todo el tiempo.
Pero lo interesante viene después. Al investigarlo descubrí que el servicio de búsqueda semántica llevaba días caído también, y nadie se había enterado porque respondía 200:
{ "results": [], "unavailable": true }
Un código de éxito con el fallo escondido dentro del cuerpo. Ninguna alerta salta con eso. Ningún health check basado en el código de estado lo ve.
Un servicio caído que devuelve 500 se arregla esa tarde. Uno que devuelve 200 puede estar semanas así.
La librería que ignora lo que no entiende
Durante la navegación, el marcador que ancla el mapa al hacer pan simplemente no se pintaba. Sin error en consola.
Le estaba pasando al Marker de @maplibre/maplibre-react-native dos props con los nombres de la versión anterior, que la v11 renombró:
// v10 — la v11 las descarta sin decir nada
<Marker coordinate={coord} anchor={{ x: 0.5, y: 0.5 }} />
// v11
<Marker lngLat={coord} anchor="center" />
La librería las ignora en silencio. No valida, no avisa: recibe algo que no reconoce y sigue como si no se lo hubieras pasado.
Me pasó exactamente lo mismo con belowLayerID, que en la v11 es beforeId. El resultado eran las etiquetas de calle desapareciendo bajo los edificios 3D, sin una sola pista de por qué.
El mismo patrón dos veces: una API tolerante convierte una errata en un comportamiento silencioso. Si esas props hubieran lanzado un error, habría perdido treinta segundos en vez de una tarde.
El guard que abortaba antes de empezar
Los edificios emblemáticos de Bilbao —San Mamés, el Guggenheim— no aparecían en 3D. Con un detalle rarísimo: al segundo toggle sí aparecían.
Perseguí el fetch y el GeoJSON un buen rato antes de entender que ese detalle era la respuesta entera. Añadir la capa de edificios deja el estilo devolviendo style.loaded() === false, así que el guard de entrada de la función de landmarks se salía mudo:
if (!map.isStyleLoaded()) return; // sale mudo: nadie se entera
A la segunda pasada el estilo ya estaba cargado, y por eso el segundo toggle sí funcionaba.
La salida evidente era esperar al evento idle del mapa y montarlos ahí. No sirve: durante la navegación la cámara no se queda quieta nunca, así que ese idle no llega jamás. Acabé montándolos en el .then del fetch.
El comportamiento raro no era ruido: era el síntoma que señalaba la causa.
El que arreglé dos veces
El que peor me sabe. La búsqueda por proximidad —"¿dónde hay una fuente cerca?"— medía las distancias desde el centro del mapa, no desde el GPS. Con el mapa desplazado, te mandaba a la fuente más cercana a un punto donde no estabas.
Lo arreglé. Y volvió. Necesitó una segunda ronda porque la primera vez toqué la superficie: corregí donde se veía el síntoma sin trazar el recorrido completo del dato hasta el origen.
De ahí salió la regla que sigo a rajatabla: antes de tocar código, trazar la ruta entera del dato hasta el síntoma y confirmar la causa raíz con evidencia. Parchear donde se ve el problema es cómodo, y garantiza volver.
Qué tienen en común
Ninguno se anuncia. Un servicio devuelve 200, una librería descarta una prop, un guard se sale por la puerta de atrás, una caché sirve lo de antes. El sistema no se cae: se degrada, y sigue pareciendo que funciona.
En el benchmark de búsqueda me pasó la versión más pura: heredé un umbral de abstención de otro modelo y apagó la abstención entera. Sin excepción. Una parte del sistema dejó de existir sin decírmelo.
Me han quedado tres hábitos:
Verificar en producción, y como un usuario que vuelve. Cada vez que despliego compruebo la cosa concreta contra el servidor real —curl a la ruta, o captura de la pantalla que cambió— y no siempre desde una pestaña limpia. La build en verde no es evidencia de nada.
Desconfiar del código de estado. Un 200 dice que la petición llegó, no que la respuesta sea buena. Ahora miro el cuerpo.
Tratar lo raro como información. "Al segundo toggle sí funciona" no era una curiosidad: era el bug entero, escrito en una frase.