個人でアプリを長く運用していて、いちばん怖い瞬間は新バージョンを公開した直後の数時間だと感じています。私自身、累計 5,000 万ダウンロードのアプリ事業を運営してきた中で、夜に出した更新が朝起きるとレビュー欄に「起動しない」「データが消えた」と並んでいる経験を何度かしました。Rork で作ったアプリも例外ではありません。AI が生成したコードは賢いのですが、どんな実装でも 100% の端末・ロケール・ネットワーク環境で動く保証はないからです。
この記事は、その恐怖を仕組みで減らすための運用設計をまとめたものです。具体的には、iOS の Phased Release、Android の Staged Rollout、そして EAS Update による OTA ホットフィックスを「三層の安全網」として組み合わせ、不具合の影響を全ユーザーから 1〜10% に抑え、復旧時間を 24 時間から数時間まで短縮する方法を解説します。
公式ドキュメントを読むだけでは見えてこない実用上の落とし穴 — 「Phased Release 中に新たに修正版を提出するとどうなるか」「Native クラッシュは OTA で直せない理由」「Expedited Review が通る申請文の書き方」 — を、私自身が現場で得た判断軸とともに具体的なコード・スクリプトで示します。
「壊さない本番運用」が必要な理由 — 個人開発者ほど『初日の影響』が致命的になる
法人のアプリ事業なら、リリース直後に不具合が出てもサポートチームが対応してくれます。けれど個人開発の場合、不具合報告のメールに返信できるのは私たち本人だけで、その間にもストアレビューには「最新版で開けません」が並んでいきます。星 1 のレビューが 10 件付くと、その後数ヶ月にわたって新規ダウンロード数が落ち込むことを、私はデータで何度も見てきました。
つまり個人開発者にとって、本番運用の目標は「バグを 0 にすること」ではなく、「バグが出たときに被害をどれだけ小さくできるか」です。これは設計思想の転換でもあります。完璧なリリースを目指すよりも、未完成の状態でも安全に出せる仕組みを作るほうが、結果として長く運用できます。
宮大工だった両祖父から私が受け取った「丁寧に作る」という感覚は、コードを完璧にすることではなく、壊れたときに直せる構造で組むことだと、運用を重ねるうちに気づきました。本番運用における段階的リリースは、まさにその構造化の一つです。
三層の安全網の全体像
具体的な実装に入る前に、この記事で組み立てる「三層の安全網」を俯瞰しておきます。
第 1 層: ストア側の段階配信 : iOS Phased Release(7 日間で 1% → 100%)と Android Staged Rollout(任意の % 設定)を必ず有効にし、初日に全ユーザーへ配信されることを物理的に防ぎます。
第 2 層: 監視と判断の自動化 : Sentry や Firebase Crashlytics でクラッシュ率を継続監視し、しきい値を超えたら GitHub Actions が Slack / Discord に通知。さらに人間の判断が必要なときに必要な情報をすべて添えてアラートします。
第 3 層: 復旧経路の使い分け : JS / TypeScript 層の不具合は EAS Update で OTA 配信して即座に修正、Native 層のクラッシュやポリシー違反は Expedited Review で短時間の再審査を申請します。最悪の場合のロールバック手順も用意しておきます。
この三層を全て準備するのに、最初は半日ほどかかります。けれど一度作れば、それ以降のリリースは「ボタンを押す → 数値を眺める → 必要なら止める」というシンプルな運用になります。
第 1 層: iOS Phased Release を正しく使う
Phased Release は App Store Connect の機能で、有効にすると新バージョンが 7 日かけて自動的に段階配信されます。配信比率は公式ドキュメントに以下のとおり明記されています。
Day 1: 1%
Day 2: 2%
Day 3: 5%
Day 4: 10%
Day 5: 20%
Day 6: 50%
Day 7: 100%
注目してほしいのは Day 1 の 1% です。仮にあなたのアプリの DAU が 10 万人なら、初日の影響は最大 1,000 人。これだけでもストアレビューが荒れる事故を大幅に減らせます。
公式が説明していない落とし穴 1: 「アップデート」のみが対象
Phased Release は「既存ユーザーへのアップデート配信」のみに適用される機能です。新規ダウンロードのユーザーには初日から最新版が配信されます 。つまり Phased Release を有効にしていても、検索広告などで新規流入が多いアプリは、初日からそれなりの数のユーザーが新バージョンを使うことになります。
私はこの仕様を知らずに「Phased Release だから初日は安全」と油断したことがあり、新規ユーザー側で起きていたクラッシュに気づくのが半日遅れたことがあります。新規流入数が多いアプリほど、後述する第 2 層の監視を厳しめに設定する必要があります。
公式が説明していない落とし穴 2: Phased Release 中の修正版提出
Phased Release 中に重大な不具合に気付き、修正版を提出した場合、新バージョンが審査通過した時点で 進行中の Phased Release は自動的にキャンセル されます。そしてさらに重要なのは、新バージョンの Phased Release は最初の 1% から再スタート することです。
これは正しい挙動なのですが、現場で混乱しやすい点があります。修正版が承認されても 1% にしか配信されないため、すでに古い問題版を使っている残り 99% のユーザーには即座には届かないということです。だから、致命的な不具合のときは Phased Release だけでなく App Store Connect で「ユーザーへの配信を一時停止」を併用し、さらに OTA ホットフィックスで JS 層を修正する三段構えが必要になります。
Phased Release を有効にする手順
App Store Connect の Web UI からは以下の手順で設定します。
App Store Connect → 自分のアプリ → 「App Store」タブ
配信したいバージョンを選択
「Phased Release for Automatic Updates」セクションで「Release update over a 7-day period using phased release」にチェック
「Save」 → 「Submit for Review」
私は毎回手動でチェックを入れる運用を避けるため、Rork × Fastlane × EAS リリース自動化完全ガイド でも紹介している fastlane の deliver で Phased Release のオプションも含めて一括自動化しています。
第 1 層: Android Staged Rollout を正しく使う
Android の Staged Rollout は、iOS よりさらに細かく制御できます。Play Console から任意のパーセンテージ(1%、5%、10%、25%、50%、100% など)を指定して、好きなタイミングで次の段階に進められます。
私の運用では、最初は 5% でリリースし、24 時間問題がなければ 25%、さらに 24 時間後に 50%、72 時間後に 100% という流れを基本にしています。iOS の固定 7 日とは違い、自分の生活リズムに合わせて段階を進められるのが Android の利点です。
公式が説明していない落とし穴: ロールバックは「次のバージョン」でしか実現できない
Android では「現在配信中のバージョンを過去のバージョンに戻す」という操作は基本的にできません。Staged Rollout 中であっても、すでに新バージョンを受け取ったユーザーを古いバージョンに戻すには、古いバージョンと同じ機能を持つ新しい version code のビルド を作って提出する必要があります。
これは EAS Build や Rork の自動ビルドを使っている場合、expo-updates のロールバック機能と組み合わせるか、Git の以前のコミットからビルドし直して新しい version code として再提出することになります。手順を一度シミュレーションしておかないと、本番で焦って間違えるので、私は新しいアプリをリリースするたびに「ロールバック練習」を一度だけ行うようにしています。
第 2 層: クラッシュ率を自動監視する Sentry セットアップ
段階配信を有効にしても、配信中に不具合に気づけなければ意味がありません。私の運用では、Sentry の SDK をアプリに組み込み、リリースバージョンごとのクラッシュフリー率(Crash-Free Sessions Rate)を継続監視しています。
Rork アプリへの Sentry 組み込み(最小構成)
// app/_layout.tsx または index.ts で初期化する
import * as Sentry from "@sentry/react-native" ;
import Constants from "expo-constants" ;
Sentry. init ({
dsn: process.env. EXPO_PUBLIC_SENTRY_DSN ,
// リリース識別子に「アプリのバージョン@ビルド番号」を入れることで
// Phased Release の進行に応じたクラッシュ率を追跡できる
release: `${ Constants . expoConfig ?. name }@${ Constants . expoConfig ?. version }+${ Constants . expoConfig ?. ios ?. buildNumber ?? "unknown"}` ,
// 本番のみ送信する(開発時の実験的なクラッシュは除外)
enabled: ! __DEV__,
// 取り込みすぎないようサンプリング
tracesSampleRate: 0.1 ,
// PII を含めない(個人開発者向けプライバシー設計の基本)
sendDefaultPii: false ,
});
// expo-router を使っている場合のルーティング計装
import { useNavigationContainerRef } from "expo-router" ;
const ref = useNavigationContainerRef ();
Sentry.ReactNavigationInstrumentation;
release の指定が肝心です。@sentry/react-native のデフォルトでは Native 側のビルド番号を自動で取得してくれますが、Rork で書き出した Expo プロジェクトでは Constants.expoConfig 経由で明示しておくと、ダッシュボードで「v1.4.2 はクラッシュフリー 99.8%、v1.4.3 は 97.2%」と一目で比較できるようになります。これがないと、Phased Release 中に「どのバージョンの数値か」が混ざって判断ミスにつながります。
しきい値を超えたら GitHub Actions が Slack に通知する
Sentry のダッシュボードを四六時中見るのは現実的ではありません。私は GitHub Actions で 15 分ごとにクラッシュフリー率を取得し、しきい値を割り込んだら Slack に通知する仕組みを作っています。
# .github/workflows/sentry-crash-rate-monitor.yml
name : Sentry Crash-Free Rate Monitor
on :
schedule :
- cron : "*/15 * * * *" # 15 分ごとに実行
workflow_dispatch :
jobs :
check :
runs-on : ubuntu-latest
steps :
- name : Fetch crash-free sessions rate (latest release)
id : rate
env :
SENTRY_AUTH_TOKEN : ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG : ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT : ${{ secrets.SENTRY_PROJECT }}
run : |
# 直近のリリース 1 件についてセッションベースのクラッシュフリー率を取得
RESPONSE=$(curl -s -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
"https://sentry.io/api/0/organizations/$SENTRY_ORG/sessions/?project=$SENTRY_PROJECT&field=sum(session)&field=crash_free_rate(session)&groupBy=release&interval=1h&statsPeriod=2h")
# 最新リリースの crash_free_rate を抽出
RATE=$(echo "$RESPONSE" | jq -r '.groups[0].totals."crash_free_rate(session)"')
echo "crash_free_rate=$RATE" >> $GITHUB_OUTPUT
echo "Current crash-free rate: $RATE"
- name : Notify if below threshold
if : ${{ steps.rate.outputs.crash_free_rate != '' && steps.rate.outputs.crash_free_rate < 0.995 }}
env :
SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK_URL }}
run : |
curl -X POST -H "Content-type: application/json" \
--data "{\"text\":\":rotating_light: Crash-free rate dropped to ${{ steps.rate.outputs.crash_free_rate }}\\nCheck Sentry: https://sentry.io/organizations/$SENTRY_ORG/projects/$SENTRY_PROJECT/\"}" \
"$SLACK_WEBHOOK_URL"
このしきい値(ここでは 99.5% / 0.995)は、自分のアプリの平常値を測ってから決めます。私のアプリでは平常時 99.8〜99.9% で動いているので、99.5% を切ったら異常と判断する基準にしています。一度サンプル運用して、誤検知が多すぎないかを 1 週間チェックしてからこの値を確定させると確実です。
第 2 層: 「停止すべきか」を判断する 5 つの数値ライン
通知を受け取ってから、Phased Release を停止するか継続するかを判断するときに、私が使っている数値の目安です。あくまで個人開発・小〜中規模アプリの基準ですが、判断のスタートラインとしては有効だと考えています。
クラッシュフリー率が平常値から 0.3 ポイント以上下落 : 即停止判断。たとえば平常 99.8% が 99.5% を下回ったらアラート、99.0% を下回ったら停止。
同一スタックトレースのクラッシュが 1 時間で 50 件以上 : 即停止。新バージョン起因の確率が高い。
App Store の星 1 レビューが 24 時間で 5 件以上、かつ全て新バージョン由来 : ストア印象悪化の防衛策として停止検討。
DAU が前週比で 10% 以上下落 : 起動できないユーザーがいる可能性。要調査。
特定の OS バージョンに偏ったクラッシュ : 停止せず、その OS 向けの対策を別途実施することが多い。
数値は「目安」であって絶対的な基準ではありません。重要なのは「アラートが来たときに何を見て、どう判断するか」のフローを事前に決めておくことです。即興で判断すると、夜中に焦って間違った決定を下しがちです。
第 3 層: 復旧経路の使い分け — OTA か Expedited Review か
クラッシュが見つかったとき、最初に判断すべきは「これは JS / TypeScript レイヤーの問題か、Native レイヤーの問題か」です。これによって使える復旧手段がまったく違います。
JS / TypeScript レイヤーの問題 → EAS Update で OTA 配信
Rork の出力は基本的に React Native + Expo なので、expo-updates が組み込まれていれば JavaScript バンドルとアセットのみを差し替える OTA 配信が使えます。実装の前提や設定方法は EAS Update で Rork アプリを審査なしで即時修正する実装ガイド にまとめてありますので、未実装の方はそちらを先に整えてください。EAS Update を使った最小フローは以下のとおりです。
# 1. 修正をコミット
git add .
git commit -m "fix: handle null user in profile screen"
# 2. EAS Update を発行(production チャネル)
eas update --branch production --message "Hotfix: profile crash on null user"
# 3. (任意)特定バージョンのみに配信したい場合は runtime version を指定
# app.json の runtimeVersion が一致するクライアントだけが受け取る
ここで重要なのは「Native コードを変更する修正は OTA では配信できない 」という制約です。たとえば以下は OTA で直せません。
app.json の ios.bundleIdentifier 変更
新しい Native モジュールの追加(react-native-vision-camera を初めて入れる、など)
Info.plist の NSPhotoLibraryUsageDescription 追加
AndroidManifest.xml の Permission 変更
ネイティブ言語(Swift / Kotlin)で書かれたモジュール内の修正
これらが必要な場合は、後述する Expedited Review を使うことになります。
Native レイヤーの問題 → Expedited Review を申請する
Apple の Expedited Review は、致命的な不具合や緊急の対応が必要なときに、通常の審査よりも優先して審査してもらうことを依頼できる仕組みです。私の経験では、申請後 2〜6 時間で通ることが多いです(通常審査は 24〜48 時間)。ただし「使いすぎると次回以降通りにくくなる」という運用上の制約があるので、本当に致命的なときだけ使います。
Expedited Review が通りやすい申請文には共通点があります。私は以下のテンプレートを基にして書いています。
Subject: Expedited App Review Request — [App Name] v[X.X.X]
Hello App Review Team,
We are requesting expedited review for [App Name] (App Store Connect ID: [ID]).
Issue summary:
After releasing v[X.X.X] on [date], approximately [N]% of users on iOS [version range]
experience a crash on launch caused by [specific cause: e.g., null reference in Core Data
migration]. Crash-free rate dropped from 99.8% to 97.1% in 12 hours.
Why this needs expedited review:
- The issue affects users on launch, so they cannot use the app at all.
- Approximately [N] users are blocked daily based on our crash analytics.
- The fix is isolated to [specific file], with no other behavior changes.
Mitigation already taken:
- Phased Release was paused at [date] when crash rate exceeded threshold.
- This submission contains only the fix; no new features.
Submission details:
- Build number: [X.X.X (Y)]
- Changes from previous version: [1-2 sentences describing the diff]
Thank you for your consideration.
[Your name]
要点は 3 つです。1) 影響範囲を具体的な数値で書くこと、2) 既に Phased Release を止めていることを明示し「全ユーザーへの被害拡大は防いでいる」ことを伝えること、3) 修正内容が最小限で副作用がないことを示すこと。これだけで承認率が体感で 8 割以上になりました。
Android の場合は審査自体がほぼ即時
Android は Apple のような明示的な「Expedited Review」制度はありませんが、そもそも審査時間が通常 1〜3 時間と短いため、慌てて修正版を提出すれば数時間で配信されます。Android で焦るべきは「Staged Rollout のパーセンテージ調整」のほうで、不具合が見つかったら一度 Rollout を停止し、修正版を新しい version code で提出してから再開します。
ホットフィックス時のチェックリスト
ここまでの手順を、私が実際に紙に書いて壁に貼っているチェックリストとしてまとめておきます。
[ ] クラッシュレポートで「Native か JS か」を確認する
[ ] Sentry で影響を受けるユーザー数とバージョンを記録する
[ ] App Store Connect で Phased Release を一時停止する
[ ] Play Console で Staged Rollout のパーセンテージを下げるか停止する
[ ] 修正をローカルで再現できることを確認する
[ ] テストで「修正前は再現、修正後は再現しない」ことを確認する
[ ] JS だけの問題なら → EAS Update --branch production
[ ] Native の問題なら → ビルド番号を上げて Expedited Review 申請
[ ] 復旧後、ストアレビューに被害ユーザー向けの返信を書く
[ ] 事後分析を Notion / Issue にまとめる(次回同じ事故を防ぐため)
このリストは、深夜にアラートで起きたとき、頭が回らない状態でもなぞるだけで動けるように作っています。「事故ったら 1 から順番にやる」を体に染み込ませるのが、結局いちばん早い復旧方法です。
ロールバック戦略 — 完全に前バージョンへ戻す手順
OTA でも Expedited Review でも復旧できないほど深刻な場合(例えばデータベースのスキーマ変更で既存ユーザーのデータが壊れる、など)は、完全なロールバックを検討します。
iOS の場合、App Store Connect から「以前の承認済みバージョンを再リリース」する機能はありません。代わりに以下の手順を取ります。
Git の以前のコミット(壊れる前の状態)に戻す
version 番号を上げ、build 番号も上げる(例: 1.4.3 → 1.4.4)
「以前のバージョンと機能的に同等」とリリースノートに明記して再提出
Expedited Review を申請
Android も基本的に同じ流れですが、Play Console の「Internal app sharing」機能を使えば、ロールバック版を社内テスト用にすぐ配布して動作確認してから本番提出できるので、iOS よりも安心です。
私はこの「実質ロールバック」を年に 1 回程度やる前提で、app.json の version をバンプするスクリプトと、Git のリリースタグから自動でビルドする GitHub Actions を用意しています。本番でやるときは焦るので、平時のうちに自動化しておくのが結局いちばん安全です。
なぜ Phased Release だけでは足りないのか — 設計思想の整理
ここまで読んでくださった方は気づいたかもしれませんが、Phased Release / Staged Rollout は「初日の被害を 1〜10% に抑える」だけの機能で、自動的に問題を検知してくれるわけではありません 。
つまり段階配信は「時間を稼ぐ仕組み」であって、「判断する仕組み」ではないのです。判断は人間(あなた)がやる必要があり、そのために第 2 層の監視と第 3 層の復旧経路がセットで必要になります。三層の安全網のうちどれか一つでも欠けると、本番運用の難易度は急に上がります。
私はこの設計を「壊れない橋を作る」のではなく「壊れたとき素早く修理できる橋を作る」発想と呼んでいます。完璧を目指すよりも、不完全さを前提にしたほうが、結果として長く運用できます。
Google のサービス向けに書かれた内容ですが、SLO・エラーバジェット・ポストモーテムの考え方は、規模を問わず通用すると感じています。
よくある失敗と落とし穴
実際に運用してきた中で、私自身がやらかした失敗や、相談を受けた中で多いパターンをまとめておきます。
Phased Release を有効にしたまま緊急修正を出してしまう : 修正版も最初の 1% から再スタートするため、復旧が遅れます。緊急時は Phased Release を一時停止してから出すか、Phased Release を OFF にして提出します。
Sentry の release を未設定のまま運用 : バージョンごとのクラッシュ率が比較できなくなり、「どのバージョンが悪いのか」が分からなくなります。最初に絶対に設定してください。
OTA で Native の修正を出そうとする : 配信は成功するが、起動時の整合性チェックでクラッシュが増えます。app.json を変更したら必ずネイティブビルドからやり直します。なお OTA 配信したのに反映されない場合の典型パターンは Rork で EAS Update を配信したのに反映されない原因切り分けガイド にまとめています。
しきい値が厳しすぎて誤検知が多発 : アラートに慣れて、本物の不具合のときに見逃すことがあります。最初の 1 ヶ月は誤検知許容度を高めに(しきい値ゆるめ)して、運用しながら締めていくのがおすすめです。
ロールバック手順を本番でぶっつけ本番で試す : 一度はステージング環境かサンプルアプリで練習しておきます。本番で慌てると新規バグを生みます。
全体を振り返って:次にやること 1 つ
ここまで読んでくださった方には、まず一つだけ提案させてください。
今あなたが運用している(または運用予定の)アプリで、Phased Release / Staged Rollout が有効になっているかを確認すること。 これが有効になっていないだけで、リリース直後の被害は 100 倍違ってきます。設定自体は数クリックで終わります。
その上で、第 2 層(Sentry + アラート)と第 3 層(OTA / Expedited Review の手順書)を、週末を一日使って組み立ててみてください。三層の安全網が揃ったとき、リリース時の心拍数が驚くほど下がります。私自身、この仕組みを整えてから、深夜に「ストアレビューが荒れていないか」を確認するために起きることがなくなりました。
長い記事をここまでお読みいただき、ありがとうございました。みなさまのアプリが長く、安全に運用されていくことを願っています。