sixty

React Native のアプリ向け

他人の端末のログは読めません。

モバイルアプリは API の上に乗った描画層なので、壊れるものはほとんどまずそこに現れます — そして返ってくるものの形は、ここではどこよりも重要です。10倍の行数を返すエンドポイントは、サーバーにとってはヒープ少々ですが、端末にとってはバッテリーであり、通信量であり、最後にはプロセスそのものだからです。Sixty はそれをアプリの内側から測り、クラッシュを単独ではなく起動回数に対して数え、リリースごとに1つ前と比較します。

何も throw されずに壊れる6つのかたち

どれもエラーを出さず、リクエストを失敗させず、アラートを鳴らすほど時間も動かしません。これが、あなたのユーザーが見ているものです。1つ選んでください。

何を測るか

アプリが出すすべてのリクエスト
どれだけ時間がかかり、何が返り、どれだけ大きく、何行運んだか — XHR の層で計測します。`fetch` も、その上のどんな HTTP クライアントも、最後はそこに行き着くからです。
クラッシュを、エラーとは別に
サーバー上の未捕捉例外は、リクエストを1つ失います。端末上では、誰かが手に持っているアプリを閉じます。これは1つの計測の程度の差ではないので、平均には混ぜません — そして `app_starts` を分母として記録します。「14回のクラッシュ」は、誰も行動を決められない数だからです。
画面あたりのリクエスト数と、レンダーあたりの呼び出し数
Babel プラグインがコンポーネントを親スパンにします。それが「17件のリクエスト」を「この画面は17件のリクエストを出す。前のリリースでは3件だった」に変えます。
何がどの画面で起きたか
ナビゲーションのトラッカーが現在の画面に名前を付けるので、検出結果は「何が」だけでなく「どこで」を言えます。React Navigation は2つの props で組み込めます。
セッションより長生きするウィンドウ
バックグラウンドに回ることは、モバイルのセッションが終わる最も一般的な形であり、iOS も Android も、もう動いていないアプリのためにリクエストを完了させてはくれません。ウィンドウは送信前にストレージへ書き、確認された 2xx でのみ消します。だから誰かが端末をポケットに入れる直前の最後の1つは、次の起動時に届きます — いま入っているリリースではなく、それを生んだリリースの下に。

何を見つけるか

どれも1つ前のリリースと比較されます。だから検出結果は、日付ではなく変更を名指しします。

17件のリクエストを出すようになった画面

前のリリースでは3件でした。どれも速いので、1件として見ればおかしなところはなく、気づくほど時間も動きません — おかしいのは回数だけで、モバイル回線ではその回数こそ、利用者が体感するものです。

外側から見たレンダーのループ

1回あたりは変わらないのに、以前よりはるかに多く呼ばれている関数。ソースが見えないときに、不安定な依存を持つ副作用はこう見えます。クラッシュもなく、失敗したリクエストもなく、ただ午後のうちに空になるバッテリー。

すべてを返し始めたエンドポイント

同じ呼び出しで、10倍の行数。サーバー上ならヒープ少々と数ミリ秒です。端末上では利用者の通信量であり、最後には、大きくなったアプリを放置せずに OS が終了させることです。

成功して、空で返ってくる呼び出し

中身のない 200。画面は空のまま描画され、何も throw されず、サーバー側のあらゆる指標はこれを健全として報告します。

拒否されているセッション

あるエンドポイントへの呼び出しのうち一定の割合が 401 か 403 で返る。静かに更新されなくなったトークンは、最初のサポート連絡が来るまでの何時間か、まさにこう見えます。

誰かが使っている最中にアプリが閉じる

致命的な JavaScript エラーを起動回数に対して数えるので、数字は積み上げではなく比率になります — そしてメッセージではなく、コードのどこから出たかでまとめます。

クラッシュレポータに決して届かないエラー

バウンダリが捕まえるもの。クラッシュはせず、画面があるべき場所に空の領域があり、利用者は戻ってもう一度試します。

導入方法

3行と、もう1行の Babel プラグイン。Metro は Babel なので、Node のエージェントが使うのと同じ変換がそのまま動きます — 画面あたりのリクエスト数とレンダーループのシグナルは、それが買ってくれるものであり、静的な検査も一緒に付いてきます。

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

@react-native-async-storage/async-storage も入れると、永続的な送信キューが自動で働きます — なくてもエージェントは動きますが、アプリがバックグラウンドに回ったときに残っていたウィンドウを失います。画面名については、ナビゲーショントラッカーの2つのハンドラを NavigationContainer に渡してください。

できないこと

  • 起動時間もフレーム落ちもカクつきもありません。それらはウェブ vitals のモバイル版であり、JavaScript からは測れません — iOS では CADisplayLink か MetricKit、Android では JankStats を読むネイティブモジュールが必要です。JS スレッドからフレームレートを測れば、JS スレッドから見たフレームレートを報告することになり、それはアプリがカクついているときにちょうど間違っている数値です。
  • ネイティブのクラッシュはありません。ネイティブモジュールの不正なポインタは JavaScript まで届かないので、ここで数えているのは致命的な *JavaScript* エラーです。
  • リリースビルドはまとめ方は正しく、位置の特定は苦手です。Hermes はバンドルをバイトコードにコンパイルするので、フレームはリポジトリ内のファイルではなく index.android.bundle 内のオフセットとして届きます。それでも1つのバグに対する安定した identity ではあり — それが最も重要な性質です — ソースマップのアップロードができるまで、あなたのコードの行にはリンクしません。開発ビルドなら本物のパスがあります。
  • リリース間の比較はコホート分けされておらず、ここでの最大の未解決項目です。サーバーのデプロイは動いていたものを置き換えますが、モバイルのリリースは置き換えません。何日もかけて配信され、古いバージョンはいつまでも動き続け、先に更新する人ほど新しい端末と良い回線を持つ傾向があります — つまり A と B の比較は、部分的には動いている母集団との比較になります。
  • プロキシがないので、構造的な IP のプライバシーもありません。ブラウザエージェントのより良いモードは資格情報を持たず、あなた自身のオリジンに送ります。だから訪問者からのリクエストがコレクタに届くことは一度もありません。アプリには自分のオリジンがないので、その仕組みはそもそも使えず、このエージェントは公開鍵を持ちます。すでに API を運用しているなら、`endpoint` をそこへ向けて中継すれば、この性質はそっくり戻ります。

これが理由でここに来たのなら

最後の変更が何をしたのか、確かめてください。

試してみる

別のものを作っていますか? Node のサービス、Lovable のアプリ のページもあります — 見えるものが違うので、内容も違います。

React Native アプリ向けのパフォーマンスとエラーの監視 — Sixty