Lo que el tiempo se llevó

oct. 05, 2021

El 5 de Agosto me llegó un mensaje de un cliente con el que trabajamos en una aplicación móvil. El mensaje era más o menos así:

Tomás! Tenemos un caso de un usuario que lleva usando la app por varios meses. Hoy comenzó con problemas. Se le queda pegado cuando trata de entrar. Lo raro es que probó en otro teléfono y entró sin problemas

...un misterio. Teníamos un par de teorías: quizás el teléfono no tenía espacio de almacenamiento para cargar los datos de la app, o quizás la conexión del usuario estaba mala y la app se pegaba por eso. Ambos errores los manejamos, pero uno nunca sabe. Lo bueno del caso: era sólo un usuario y pudo usar la app desde otro dispositivo.

Era sólo un usuario, ¿cierto? … ¡¿cierto?!

Como un virus

Cuatro días después me escriben nuevamente: “Tomás! ¡Tenemos dos casos nuevos con el mismo problema!”. Chupalla… la cosa se puso complicada. Que fuera un caso repetido significaba que no era simplemente un problema del teléfono.

Un par de días después aparecieron algunos casos más, pero seguían siendo pocos. No parecía haber algún factor común entre los afectados, el misterioso “virus” parecía seleccionar aleatoriamente a sus víctimas, afectando a usuarios de iOS y Android por igual.

Lo más extraño fue que, de repente, los primeros afectados dejaron de tener problemas. Así, como si nada. Y en el mismo orden en que se fueron “infectando” se fueron “sanando”. Otro misterio...

Siendo incapaces de replicar el misterioso (y maldito) bug, no pudimos hacer nada para resolverlo. Los días pasaron y eventualmente no hubo más casos nuevos. Todos los infectados se sanaron; y vivieron felices por siempre… hasta el sábado 28 de Agosto.

El súper brote

Ese día sábado me desperté y me había llegado un mensaje del cliente. Chuta, ¿mensaje un sábado? Probablemente era algo grave, ya que tenemos políticas estrictas de no trabajar los fines de semana. El mensaje era algo así:

Tomás! El virus llegó hasta mí, y otros que usaron la app hoy. Debes encontrar la cura!

Ya, quizás no fue tan dramático, pero efectivamente era un problema grave. La gran mayoría de los usuarios utiliza la app en días hábiles, y que le haya pasado a muchos usuarios ese día sábado significaba que el lunes quedaría la escoba.

Lo bueno, dentro de todo, era que por fin pudimos replicar el error. Lo malo: teníamos sólo un par de días para resolverlo.

A lo Sherlock Holmes

Teniendo ahora una forma de replicar el error, no quedó otra que lanzarnos a explorar y recolectar pistas, para entender el origen del problema.

La primera pista que obtuvimos rápidamente, fue que el problema estaba asociado a la zona horaria. La app funciona con una agenda de actividades que están fijadas en ciertos horarios, mostrando las actividades de los próximos ocho días. En ese momento estábamos en GMT-4, y si cambiábamos a otra zona horaria, la app cargaba sin problemas.

Dado que la app carga las actividades de los próximos ocho días. Nos dimos cuenta que ese día (sábado 28 de agosto), estábamos a ocho días del domingo 5 de septiembre: primer día del nuevo horario de verano. Otra pista más.

Un paso natural a seguir era replicar este problema en nuestro entorno de desarrollo para poder debuggear el flujo en que se generaban estas fechas. En Platanus trabajamos con Expo para el desarrollo de aplicaciones móviles, y Expo (y React Native) viene con una herramienta que te permite debuggear código JavaScript de manera remota. Es decir, puedo correr la app en mi smartphone y usar las herramientas de debugging que tengo en mi pc para ver qué está pasando.

Pero el malvado virus tenía una última sorpresa para mí. Al correr la app de este modo, el problema no aparecía… ¡¿Qué?!

Console.log al rescate

Si bien no podía usar el debugger ni el browser para capturar el output de la aplicación, todavía me quedaba el log de la consola que corre la app de expo. Este funciona incluso sin tener el modo “Debug Remote JS” activado. Así que tuve que recurrir al clásico console.log para poder ver qué estaba pasando.

Con esto, pude comprobar que la app se pegaba al generar la agenda de los próximos días. Para trabajar con fechas, en esta app usamos una librería de JavaScript llamada date-fns. En particular, se pegaba con un método llamado eachDayOfInterval, que permite generar un arreglo de fechas en un intervalo, pasándole una fecha de inicio y una fecha de término. Para resolver el problema, reemplacé este método por una implementación propia, y todo se resolvió.

Fin

Na, es broma. Para qué te iba a contar la mansa historia con todos esos detalles si lo iba a dejar hasta ahí. Vamos ahora de adelante hacia atrás, para entender qué pasó.


El malvado eachDayOfInterval

El principal problema estaba en el método eachDayOfInterval de date-fns. Este método parte creando con la fecha de inicio que uno le entrega. Toma esta fecha y le cambia la hora a las 00:00. Esta fecha es guardada en una variable currentDate. Luego, el método guarda esta fecha en un arreglo, suma un día a la fecha actual y asigna la hora de ese día a las 00:00. Este proceso se repite hasta llegar a la fecha de término. Acá te dejó un extracto del código usado por ese método.

while (currentDate.getTime() <= endTime) {
  dates.push(toDate(currentDate))
  currentDate.setDate(currentDate.getDate() + 1)
  currentDate.setHours(0, 0, 0, 0)
}

Esta implementación tiene un conflicto con el cambio de hora que tuvimos en Chile. En el horario de verano, el día 5 de septiembre parte a las 01:00. No existen las 00:00 de ese día. Al correr la línea currentDate.setHours(0, 0, 0, 0), el intérprete estaba seteando la hora a las 00:00 del día anterior. Así, la app entraba en un loop infinito y dejaba de responder.

El método no pasaba del 4 de septiembre.

Para solucionarlo, reimplementé esta parte del método usando un for para iterar en un rango fijo de números (de 0 a 8), y así evitar depender de que el currentDate efectivamente avanzara un día. Aquí te dejo un extracto del código que escribí para evitar el problema anterior.

for (let index = 0; index <= 8; index++) {
  dates.push(addDays(currentTime, index));
}
addDays es otro método de date-fns.

El (casi tan) malvado modo Debug Remote JS

El código JavaScript es ejecutado en un entorno de ejecución (o en inglés: runtime environment). Ya sea que desarrolles para web o móvil, este entorno puede ser distinto. En el caso de los navegadores más usados, el runtime environment de Chrome se llama V8, el de Safari se llama JavaScriptCore y el de Firefox se llama SpiderMonkey.

Volviendo a React Native, ocurre que el runtime environment es diferente si uno está en modo Debug Remote JS. Entonces, al correr la app sin debugger, es decir, como la corren los usuarios en producción, usamos JavaScriptCore, mientras que al correr la app con debugger, estoy usando V8.

¿Por qué es un problema? Dado que la librería date-fns utiliza implementaciones nativas del objeto Date, algunas diferencias en cómo los entornos de ejecución interpretan y construyen este objeto causaban que V8 no tuviera problemas con el método eachDayOfInterval, pero JavaScriptCore sí los tuviera.

El misterioso paciente cero

Sólo nos queda resolver el caso del usuario que presentó el problema el 5 de Agosto, y de los demás que lo siguieron antes del fatídico sábado 28. Para entenderlo, primero tenemos que conocer un poco la historia del cambio de hora en Chile durante los últimos años.

El año 2015 en Chile se suprimió el horario de invierno, y se mantuvo el horario de verano durante todo el año (GMT-3). El año 2016 se retomó el horario de invierno (GMT-4), determinando que se volvería al horario de verano el segundo sábado de Agosto. Esta legislación se mantuvo hasta el año 2019, cuando se determinó que el horario de verano volvería el primer sábado de septiembre. Es decir, dentro de los últimos 7 años, hemos tenido tres reglas distintas para determinar la zona horaria en Chile.

¿Entonces qué pasa en mi teléfono? Todo teléfono viene con un set de reglas sobre las zonas horarias, las cuales son usadas por otras librerías para determinar fechas y horas. Estas reglas las define y mantiene el manufacturador del equipo. Normalmente, si tu celular no está tan viejo, puedes seguir recibiendo actualizaciones, las cuales pueden incluir actualizaciones de las reglas de zonas horarias.

Uniendo los dos puntos anteriores, puede darse el caso de que un usuario tenga un equipo antiguo, que ya no reciba actualizaciones, y que haya quedado con las reglas de zona horaria previas al 2019. En estos casos, los celulares tenían la regla de que el cambio de hora era el segundo sábado de agosto, el 11. Mi teoría es que los usuarios que presentaron los primeros casos estaban en esta situación, y se resolvió automáticamente una vez que pasó el 11 de Agosto porque ya no se trataba de generar la fecha que causaba problemas.


Y así fue como el cambio de hora me quitó más que sólo una hora… incluso antes de quitármela realmente.

¡Genial! Te has suscrito con éxito.
¡Genial! Ahora, completa el checkout para tener acceso completo.
¡Bienvenido de nuevo! Has iniciado sesión con éxito.
Éxito! Su cuenta está totalmente activada, ahora tienes acceso a todo el contenido.
Calendar vector created by pch.vector - www.freepik.com