sixty

para aplicaciones React Native

No puedes leer los registros en el teléfono de otra persona.

Una aplicación móvil es una capa de renderizado sobre una API, así que casi todo lo que se rompe aparece allí primero — y la forma de lo que vuelve importa aquí más que en ningún otro sitio, porque un endpoint que devuelve diez veces las filas le cuesta a un servidor algo de memoria y a un teléfono le cuesta su batería, sus datos y, al final, el proceso. Sixty mide eso desde dentro de la app, cuenta los cierres contra los arranques en vez de aislados, y compara cada versión con la anterior.

Seis formas de romperse sin que salte ninguna excepción

Ninguna de estas lanza un error, hace fallar una petición ni mueve un tiempo lo suficiente como para alertar. Son lo que ven tus usuarios. Elige una.

Qué mide

Cada petición que hace la app
Cuánto tardó, qué volvió, cuánto ocupaba y cuántas filas llevaba — instrumentado en XHR, que es donde acaban `fetch` y cualquier cliente HTTP por encima.
Cierres, aparte de los errores
Una excepción no capturada en un servidor pierde una petición. En un teléfono cierra la aplicación mientras alguien la tiene en la mano. No son grados de una misma medición, así que no se promedian juntos — y `app_starts` se registra como denominador, porque «catorce cierres» no es un número sobre el que nadie pueda actuar.
Peticiones por pantalla, y llamadas por render
El plugin de Babel convierte los componentes en spans padre, que es lo que convierte «diecisiete peticiones» en «esta pantalla hace diecisiete peticiones, y hacía tres en la versión anterior».
En qué pantalla pasó cada cosa
Un rastreador de navegación nombra la pantalla actual, así que un hallazgo dice dónde y no solo qué. React Navigation se conecta con dos props.
Ventanas que sobreviven a la sesión
Pasar a segundo plano es como termina la mayoría de las sesiones móviles, y nada en iOS ni en Android va a terminar una petición de una app que ya no está corriendo. Las ventanas se escriben en almacenamiento antes de enviarse y solo se borran con un 2xx confirmado, así que la última antes de que alguien se guarde el teléfono llega en el siguiente arranque — bajo la versión que la produjo, no la que hay instalada ahora.

Qué encuentra

Cada una de estas se compara con la versión anterior, así que el hallazgo nombra el cambio y no el día.

Una pantalla que empezó a hacer diecisiete peticiones

Hacía tres en la versión anterior. Cada una es rápida, así que ninguna petición individual parece mal y ningún tiempo se mueve lo suficiente como para notarlo — solo el número está mal, y en datos móviles el número es lo que la persona siente.

Un bucle de render, visto desde fuera

Una función llamada muchísimas más veces que antes, sin cambios por llamada. Eso es lo que parece un efecto con una dependencia inestable cuando no puedes ver el código fuente: sin cierre, sin petición fallida, y una batería que se vacía en una tarde.

Un endpoint que empezó a devolverlo todo

La misma llamada, diez veces las filas. En un servidor eso es algo de memoria y unos milisegundos. En un dispositivo son los datos de la persona y, al final, el sistema operativo matando una app que creció en vez de dejarla.

Llamadas que funcionan y vuelven vacías

Un 200 sin nada dentro. La pantalla se renderiza en blanco, no salta nada, y todas las métricas del lado servidor reportan esto como sano.

Sesiones siendo rechazadas

Una parte de las llamadas a un endpoint respondidas con 401 o 403. Un token que dejó de renovarse calladamente se ve exactamente así, durante horas, antes de que llegue el primer mensaje de soporte.

La app cerrándose mientras alguien la está usando

Errores fatales de JavaScript contados contra los arranques, así que el número es una tasa y no un recuento — y agrupados por el punto del código del que vinieron y no por su mensaje.

Errores que nunca llegan a un reportador de cierres

Los que captura un boundary: sin cierre, un área en blanco donde debería haber una pantalla, y una persona que vuelve atrás y lo intenta otra vez.

Cómo se instala

Tres líneas, y un plugin de Babel que es una más. Metro es Babel, así que la misma transformación que usa el agente de Node funciona aquí sin cambios — es lo que compra las peticiones por pantalla y la señal de bucle de render, y las reglas estáticas vienen con ella.

npm install @sixty-sh/react-native

// index.js — before anything else
import { init, composeRelease } from '@sixty-sh/react-native'

init({
  key: 'sixty_pk_…',
  service: 'my-app',
  // A native version alone reports every OTA bundle you push as the
  // same release, which is the one thing a comparison cannot survive.
  release: composeRelease({ version: '1.4.2', otaUpdateId }),
})

// babel.config.js
module.exports = { plugins: ['@sixty-sh/react-native/babel'] }

Instala además @react-native-async-storage/async-storage y la bandeja de salida duradera funciona sola — sin él el agente sigue corriendo y pierde las ventanas que quedaban pendientes cuando la app pasó a segundo plano. Para los nombres de pantalla, pasa los dos manejadores del rastreador de navegación a tu NavigationContainer.

Qué no hace

  • Sin tiempo de arranque, sin caídas de fotogramas, sin tirones. Eso es el equivalente móvil de las web vitals y no se puede medir desde JavaScript — necesita un módulo nativo leyendo CADisplayLink o MetricKit en iOS y JankStats en Android. Medir la tasa de fotogramas desde el hilo de JS reportaría la opinión del hilo de JS al respecto, que es justo el número que está mal cuando la app va a tirones.
  • Los cierres nativos no están. Un puntero mal en un módulo nativo nunca llega a JavaScript, así que lo que se cuenta aquí es un error fatal *de JavaScript*.
  • Las builds de producción agrupan bien y localizan mal. Hermes compila el bundle a bytecode, así que un frame llega como un desplazamiento dentro de index.android.bundle en vez de como un archivo de tu repositorio. Eso sigue siendo una identidad estable para un fallo — la propiedad que más importa — pero no enlazará a una línea de tu código hasta que exista la subida de sourcemaps. Una build de desarrollo tiene rutas reales.
  • La comparación entre versiones no está por cohortes, y este es el asunto abierto más grande de aquí. Un despliegue de servidor sustituye lo que estaba corriendo; una versión móvil no. Se despliega a lo largo de días, las versiones viejas siguen corriendo indefinidamente, y quienes actualizan primero tienden a tener dispositivos más nuevos y mejores conexiones — así que A contra B es en parte una comparación contra una población que se mueve.
  • No hay proxy, así que no hay privacidad de IP estructural. El modo mejor del agente de navegador no lleva ninguna credencial y publica en tu propio origen, que es por lo que ninguna petición de un visitante llega jamás al colector. Una app no tiene origen propio, así que ese arreglo sencillamente no está disponible y este agente lleva una clave pública. Si ya tienes una API, apunta `endpoint` a ella y haz de proxy desde ahí y la propiedad se restaura exactamente.

Si estás aquí por una de estas

Descubre qué hizo tu último cambio.

¡Pruébalo!

¿Construyes otra cosa? Mira servicios Node o aplicaciones Lovable — esas páginas son distintas, y también lo es lo que podemos ver.

Monitorización de rendimiento y errores para aplicaciones React Native — Sixty