sixty

für React-Native-Apps

Auf dem Telefon eines anderen kannst du die Logs nicht lesen.

Eine mobile App ist eine Rendering-Schicht über einer API, fast alles, was kaputtgeht, zeigt sich also zuerst dort — und die Form dessen, was zurückkommt, zählt hier mehr als irgendwo sonst, weil ein Endpunkt, der die zehnfache Zeilenzahl zurückgibt, einen Server etwas Heap kostet und ein Telefon seinen Akku, sein Datenvolumen und irgendwann den Prozess. Sixty misst das aus der App heraus, zählt Abstürze gegen Starts statt isoliert, und vergleicht jedes Release mit dem davor.

Sechs Arten, kaputtzugehen, ohne dass etwas wirft

Keines davon wirft einen Fehler, lässt eine Anfrage scheitern oder bewegt eine Zeitmessung genug, um darauf zu alarmieren. Es ist das, was deine Nutzer sehen. Such dir eines aus.

Was es misst

Jede Anfrage, die die App macht
Wie lange sie gedauert hat, was zurückkam, wie groß es war und wie viele Zeilen es trug — an XHR instrumentiert, wo `fetch` und jeder HTTP-Client darüber am Ende landen.
Abstürze, getrennt von Fehlern
Eine nicht abgefangene Exception auf einem Server verliert eine Anfrage. Auf einem Telefon schließt sie die Anwendung, während jemand sie in der Hand hält. Das sind keine Abstufungen einer Messung, sie werden also nicht zusammen gemittelt — und `app_starts` wird als Nenner aufgezeichnet, weil „vierzehn Abstürze“ keine Zahl ist, mit der irgendjemand etwas anfangen kann.
Anfragen pro Screen, und Aufrufe pro Render
Das Babel-Plugin macht Komponenten zu Eltern-Spans, und genau das macht aus „siebzehn Anfragen“ ein „dieser Screen macht siebzehn Anfragen und machte im letzten Release drei“.
Auf welchem Screen etwas passiert ist
Ein Navigations-Tracker benennt den aktuellen Screen, ein Befund sagt also wo und nicht nur was. React Navigation wird mit zwei Props eingebunden.
Fenster, die die Sitzung überleben
In den Hintergrund zu gehen ist, wie die meisten mobilen Sitzungen enden, und weder iOS noch Android wird eine Anfrage für eine App zu Ende bringen, die nicht mehr läuft. Fenster werden vor dem Senden in den Speicher geschrieben und nur bei einem bestätigten 2xx gelöscht, das letzte, bevor jemand sein Telefon einsteckt, kommt also beim nächsten Start an — unter dem Release, das es erzeugt hat, nicht unter dem, das jetzt installiert ist.

Was es findet

Jedes davon wird gegen das Release davor verglichen, der Befund benennt also die Änderung und nicht den Tag.

Ein Screen, der angefangen hat, siebzehn Anfragen zu machen

Im letzten Release waren es drei. Jede ist schnell, keine einzelne Anfrage sieht also falsch aus und keine Zeitmessung bewegt sich genug, um aufzufallen — nur die Anzahl ist falsch, und im Mobilfunk ist die Anzahl das, was der Nutzer spürt.

Eine Render-Schleife, von außen

Eine Funktion, die weit häufiger aufgerufen wird als vorher, pro Aufruf unverändert. So sieht ein Effekt mit einer instabilen Abhängigkeit aus, wenn man den Quellcode nicht sehen kann: kein Absturz, keine fehlgeschlagene Anfrage, und ein Akku, der sich an einem Nachmittag leert.

Ein Endpunkt, der angefangen hat, alles zurückzugeben

Derselbe Aufruf, die zehnfache Zeilenzahl. Auf einem Server ist das etwas Heap und ein paar Millisekunden. Auf einem Gerät ist es das Datenvolumen der Nutzerin und irgendwann das Betriebssystem, das eine App beendet, die gewachsen ist, statt sie gewähren zu lassen.

Aufrufe, die erfolgreich sind und leer zurückkommen

Ein 200 ohne Inhalt. Der Screen rendert leer, nichts wirft, und jede serverseitige Metrik meldet das als gesund.

Sitzungen, die abgewiesen werden

Ein Anteil der Aufrufe eines Endpunkts wird mit 401 oder 403 beantwortet. Ein Token, das still aufgehört hat, sich zu erneuern, sieht genau so aus, stundenlang, bevor die erste Supportnachricht kommt.

Die App schließt sich, während jemand sie benutzt

Fatale JavaScript-Fehler, gegen App-Starts gezählt, die Zahl ist also eine Rate statt einer Strichliste — und gruppiert danach, wo im Code sie herkamen, statt nach ihrer Meldung.

Fehler, die nie einen Crash-Reporter erreichen

Die, die eine Boundary abfängt: kein Absturz, eine leere Fläche, wo ein Screen sein sollte, und ein Nutzer, der zurücktippt und es noch einmal versucht.

Wie es installiert wird

Drei Zeilen, und ein Babel-Plugin, das eine weitere ist. Metro ist Babel, dieselbe Transformation, die der Node-Agent benutzt, funktioniert hier also unverändert — sie ist es, die Anfragen pro Screen und das Render-Schleifen-Signal einbringt, und die statischen Regeln kommen mit.

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

Installiere zusätzlich @react-native-async-storage/async-storage, und der dauerhafte Ausgangspuffer funktioniert von allein — ohne ihn läuft der Agent weiterhin und verliert die Fenster, die noch ausstanden, als die App in den Hintergrund ging. Für Screen-Namen übergibst du die zwei Handler des Navigations-Trackers an deinen NavigationContainer.

Was es nicht tut

  • Keine Startzeit, keine ausgelassenen Frames, kein Ruckeln. Das ist das mobile Gegenstück zu den Web Vitals und lässt sich aus JavaScript nicht messen — dafür braucht es ein natives Modul, das auf iOS CADisplayLink oder MetricKit liest und auf Android JankStats. Die Bildrate vom JS-Thread aus zu messen würde die Meinung des JS-Threads darüber melden, und genau die ist falsch, wenn die App ruckelt.
  • Native Abstürze fehlen. Ein fehlerhafter Pointer in einem nativen Modul erreicht JavaScript nie, was hier gezählt wird, ist also ein fataler *JavaScript*-Fehler.
  • Release-Builds gruppieren richtig und lokalisieren schlecht. Hermes kompiliert das Bundle zu Bytecode, ein Frame kommt also als Offset in index.android.bundle an statt als Datei in deinem Repository. Das ist immer noch eine stabile Identität für einen Bug — die Eigenschaft, auf die es am meisten ankommt — aber es wird nicht auf eine Zeile deines Codes verlinken, bis es einen Sourcemap-Upload gibt. Ein Development-Build hat echte Pfade.
  • Der Release-Vergleich ist nicht nach Kohorten getrennt, und das ist hier der größte offene Punkt. Ein Server-Deployment ersetzt, was lief; ein mobiles Release tut das nicht. Es rollt über Tage aus, alte Versionen laufen unbegrenzt weiter, und die Nutzer, die zuerst aktualisieren, haben tendenziell neuere Geräte und bessere Verbindungen — A gegen B ist also teilweise ein Vergleich gegen eine sich verschiebende Population.
  • Es gibt keinen Proxy, also gibt es keine strukturelle IP-Privatsphäre. Der bessere Modus des Browser-Agenten hält keine Zugangsdaten und postet an dein eigenes Origin, weshalb keine Anfrage einer Besucherin je den Collector erreicht. Eine App hat kein eigenes Origin, diese Anordnung ist also schlicht nicht verfügbar, und dieser Agent trägt einen öffentlichen Schlüssel. Wenn du ohnehin eine API betreibst, richte `endpoint` darauf und leite von dort weiter, und die Eigenschaft ist exakt wiederhergestellt.

Falls du wegen einer dieser Sachen hier bist

Finde heraus, was deine letzte Änderung angerichtet hat.

Probier es aus!

Du baust etwas anderes? Sieh dir Node-Dienste oder Lovable-Apps an — diese Seiten sind anders, und was wir sehen können, auch.

Performance- und Fehler-Monitoring für React-Native-Apps — Sixty