sixty

for React Native apps

You cannot read the logs on somebody else’s phone.

A mobile app is a rendering layer over an API, so nearly everything that breaks shows up there first — and the shape of what comes back matters more here than anywhere else, because an endpoint that returns ten times the rows costs a server some heap and costs a phone its battery, its data allowance, and eventually the process. Sixty measures that from inside the app, counts crashes against launches rather than in isolation, and compares each release against the one before it.

Six ways it breaks without anything throwing

None of these raise an error, fail a request, or move a timing enough to alert on. They are what your users see. Pick one.

What it measures

Every request the app makes
How long it took, what came back, how big it was and how many rows it carried — instrumented at XHR, which is where `fetch` and every HTTP client above it end up.
Crashes, apart from errors
An uncaught exception on a server loses one request. On a phone it closes the application while somebody is holding it. Those are not degrees of one measurement, so they are not averaged together — and `app_starts` is recorded as the denominator, because "fourteen crashes" is not a number anyone can act on.
Requests per screen, and calls per render
The Babel plugin makes components parent spans, which is what turns "seventeen requests" into "this screen makes seventeen requests, and made three last release".
Which screen anything happened on
A navigation tracker names the current screen, so a finding says where rather than only what. React Navigation wires in with two props.
Windows that outlive the session
Backgrounding is how most mobile sessions end, and nothing on iOS or Android will finish a request for an app that is no longer running. Windows are written to storage before they are sent and cleared only on a confirmed 2xx, so the last one before somebody pockets their phone arrives at the next launch — under the release that produced it, not the one now installed.

What it finds

Every one of these is compared against the release before it, so the finding names the change rather than the day.

A screen that started making seventeen requests

It made three last release. Each is fast, so no single request looks wrong and no timing moves enough to notice — only the count is wrong, and on cellular the count is what the user feels.

A render loop, from the outside

A function called far more often than it was, unchanged per call. That is what an effect with an unstable dependency looks like when you cannot see the source: no crash, no failed request, and a battery that empties in an afternoon.

An endpoint that started returning everything

The same call, ten times the rows. On a server that is some heap and a few milliseconds. On a device it is the user's data allowance and, eventually, the OS killing an app that grew rather than letting it.

Calls that succeed and come back empty

A 200 with nothing in it. The screen renders blank, nothing throws, and every server-side metric reports this as healthy.

Sessions being refused

A share of calls to one endpoint answered 401 or 403. A token that quietly stopped refreshing looks exactly like this, for hours, before the first support message arrives.

The app closing while somebody is using it

Fatal JavaScript errors counted against app starts, so the number is a rate rather than a tally — and grouped by where in the code they came from rather than by their message.

Errors that never reach a crash reporter

The ones a boundary catches: no crash, a blank area where a screen should be, and a user who taps back and tries again.

How it installs

Three lines, and a Babel plugin that is one more. Metro is Babel, so the same transform the Node agent uses works here unchanged — it is what buys requests-per-screen and the render-loop signal, and the static rules come with it.

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'] }

Install @react-native-async-storage/async-storage as well and the durable outbox works on its own — without it the agent still runs and loses the windows that were owed when the app was backgrounded. For screen names, pass the navigation tracker's two handlers to your NavigationContainer.

What it does not do

  • No launch time, no frame drops, no jank. Those are the mobile equivalent of web vitals and they cannot be measured from JavaScript — they need a native module reading CADisplayLink or MetricKit on iOS and JankStats on Android. Measuring the frame rate from the JS thread would report the JS thread's opinion of it, which is exactly the number that is wrong when the app is janking.
  • Native crashes are absent. A bad pointer in a native module never reaches JavaScript, so what is counted here is a fatal *JavaScript* error.
  • Release builds group correctly and locate badly. Hermes compiles the bundle to bytecode, so a frame arrives as an offset into index.android.bundle rather than a file in your repository. That is still one stable identity for one bug — the property that matters most — but it will not link to a line of your code until sourcemap upload exists. A development build has real paths.
  • Release comparison is not cohorted, and this is the largest open item here. A server deploy replaces what was running; a mobile release does not. It rolls out over days, old versions run indefinitely, and the users who update first skew toward newer devices and better connections — so A-versus-B is partly a comparison against a shifting population.
  • There is no proxy, so there is no structural IP privacy. The browser agent's better mode holds no credential and posts to your own origin, which is why no request from a visitor ever reaches the collector. An app has no origin of its own, so that arrangement is simply unavailable and this agent carries a public key. If you already run an API, point `endpoint` at it and proxy from there and the property is restored exactly.

If one of these is why you are here

Find out what your last change did.

Try it!

Building something else? See Node services or Lovable apps — those pages are different, and so is what we can see.

Performance and error monitoring for React Native apps — Sixty