◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り35日。API 連携を足したアプリは移行先の確認を●SDK 58 ISSUE — Android ウィジェットが定期更新されない、Stripe が静的リンクになるなど初期報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り35日。API 連携を足したアプリは移行先の確認を●SDK 58 ISSUE — Android ウィジェットが定期更新されない、Stripe が静的リンクになるなど初期報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/アプリ開発
◇ アプリ開発/2026-10-26上級

段階公開の最初の5%で見えた Crash-free 99.7%、何ユーザーから信じてよいのか

段階公開 5% の Crash-free 率は、母数が小さいほど区間が広く、進む・止めるの根拠になりません。Wilson 区間で「進む・待つ・止める」を判定する Python と、毎回見ると誤って止める確率の実測をまとめます。

段階公開4Crash-freeCrashlytics14Google Play43Wilson区間リリース判断Python個人開発228

✦ プレミアム記事

Play Console の段階公開を 5% に動かした翌朝、Crashlytics には「Crash-free users 99.76%」と出ていました。手元の基準は 99.7% です。超えているのだから 25% へ進めてよい、と指が動きかけて止まりました。母数の欄が 420 だったからです。

数字は同じでも、420 人の 99.76% と 2 万人の 99.76% は、別の情報です。前者は「たまたま 1 人しか落ちなかった」可能性を含み、後者はほぼ確定した値です。画面は両方に同じ書式で同じ顔をした数字を出します。

私は 5% → 25% → 50% → 100% の四段で出し、Crash-free users が 99.7% を下回るか ANR が 0.20% を超えたら halt(比率を戻す)と決めています。以前の記事(Android 壁紙アプリ v2.1.0 公開後の保守)に書いたとおり、v2.0.0 では二度止めました。その線引き自体は変えません。変えたのは、線と比べる「数字の信頼度」を一緒に見ることです。

ここでは、母数込みで進む・待つ・止めるを決める小さな Python スクリプトと、それを毎朝回したときに起きる意外な副作用を、手元で乱数を回して確かめた結果としてお伝えします。

同じ 99.76% でも、母数で読める範囲がこれだけ変わる

Crash-free を「クラッシュしなかったユーザーの割合」と読むと、それはユーザー数 n のうち k 人が落ちたという二項データです。観測値が同じでも、真の値の幅は n で決まります。95% の Wilson 区間(後述)で並べると次のようになります。

ユーザー数落ちた人数観測の Crash-free真の値の範囲(95%)判定(目標 99.7%)
420199.76%98.66〜99.96%待つ
2,000599.75%99.42〜99.89%待つ
5,0001299.76%99.58〜99.86%待つ
20,0004899.76%99.68〜99.82%待つ

4 行とも観測は 99.76% 前後で、目標の 99.7% を上回っています。それでも 2 万人の時点ですら、区間の下端 99.68% は目標をわずかに割っています。「超えているから進める」は、思っていたよりずっと遅い段階まで成立しません。

最初の 5% で見える数字は、おおむね「方向の目安」です。これを判定の根拠にするなら、根拠にしてよい条件を先に決めておく必要があります。

進む・待つ・止めるを、区間と目標の位置関係で決める

判定は単純です。真の値の区間が、許容ライン(99.7% なら クラッシュ率 0.3%)に対してどこにあるかを見ます。

  • 区間の上端でも許容内なら、確かに良いので進む
  • 区間の下端でも許容を超えていて、件数も偶然で済まないなら、確かに悪いので止める
  • 区間が許容ラインをまたいでいるなら、まだ決められないので待つ

以下がそのまま動くスクリプトです。標準ライブラリだけで、Crashlytics の画面に出ている「ユーザー数」と「クラッシュしたユーザー数」を手で渡します。

#!/usr/bin/env python3
"""段階公開の「進む/待つ/止める」を、母数込みで判定する。
使い方: python3 rollout_gate.py --users 420 --crashed 1 --target 99.7
標準ライブラリのみ。Crashlytics / Play Console に出ている「ユーザー数」と
「クラッシュしたユーザー数」を手で入れて使います。"""
import argparse
import math
import sys
 
Z = {90: 1.645, 95: 1.960}  # 片側 95% = 1.645 / 両側 95% = 1.960
 
 
def wilson(k: int, n: int, z: float) -> tuple[float, float]:
    """クラッシュ率 k/n の Wilson 区間 (low, high)。n が小さくても 0〜1 に収まる。"""
    if n <= 0:
        raise ValueError("users must be > 0")
    p = k / n
    z2 = z * z
    denom = 1 + z2 / n
    center = (p + z2 / (2 * n)) / denom
    half = z * math.sqrt(p * (1 - p) / n + z2 / (4 * n * n)) / denom
    return max(0.0, center - half), min(1.0, center + half)
 
 
def judge(users: int, crashed: int, target_pct: float, z: float = Z[95],
          min_crashed: int = 3) -> dict:
    allowed = 1 - target_pct / 100          # 許容するクラッシュ率 (99.7 → 0.003)
    low, high = wilson(crashed, users, z)
    if low > allowed and crashed >= min_crashed:
        verdict = "HALT"                    # 下限でも許容超え、かつ件数が偶然では済まない
    elif high <= allowed:
        verdict = "PROCEED"                 # 上限でも許容内 = 確実に良い
    else:
        verdict = "WAIT"                    # 区間が許容ラインをまたぐ = まだ決められない
    return {
        "users": users, "crashed": crashed,
        "observed_crash_free": 100 * (1 - crashed / users),
        "crash_free_low": 100 * (1 - high), "crash_free_high": 100 * (1 - low),
        "verdict": verdict,
    }
 
 
def users_needed_for_zero_crash(target_pct: float, z: float = Z[95]) -> int:
    """クラッシュ 0 件のまま PROCEED と言えるのは何ユーザー目からか。
    k=0 の Wilson 上限 = z²/(n+z²) <= allowed を n について解く。"""
    allowed = 1 - target_pct / 100
    return math.ceil(z * z * (1 / allowed - 1))
 
 
def days_to_decide(users: int, needed: int, users_per_day: float) -> int:
    """あと何日で needed ユーザーに届くか。users_per_day は「この段階で新規に
    新バージョンを開くユーザー数」の直近の実測値を入れる。"""
    if users >= needed:
        return 0
    if users_per_day <= 0:
        raise ValueError("users_per_day must be > 0")
    return math.ceil((needed - users) / users_per_day)
 
 
def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--users", type=int, required=True)
    ap.add_argument("--crashed", type=int, required=True)
    ap.add_argument("--target", type=float, default=99.7)
    ap.add_argument("--per-day", type=float, default=0,
                    help="この段階で 1 日に増える新バージョン利用ユーザー数(実測)")
    ap.add_argument("--min-crashed", type=int, default=3,
                    help="この件数に満たない間は HALT にせず WAIT にする")
    ap.add_argument("--conf", type=int, choices=sorted(Z), default=95)
    a = ap.parse_args()
    if a.crashed > a.users:
        print("crashed が users を超えています", file=sys.stderr)
        return 2
    r = judge(a.users, a.crashed, a.target, Z[a.conf], a.min_crashed)
    print(f"{r['verdict']}: {r['crashed']}/{r['users']} users crashed "
          f"(観測 {r['observed_crash_free']:.2f}% / 真の値は "
          f"{r['crash_free_low']:.2f}〜{r['crash_free_high']:.2f}% の範囲)")
    if r["verdict"] == "WAIT":
        need = users_needed_for_zero_crash(a.target, Z[a.conf])
        print(f"  目標 {a.target}% を 0 件で確かめるには約 {need} ユーザー必要")
        if a.per_day > 0:
            print(f"  今の増え方なら、あと約 {days_to_decide(a.users, need, a.per_day)} 日")
    return {"PROCEED": 0, "WAIT": 10, "HALT": 20}[r["verdict"]]
 
 
if __name__ == "__main__":
    sys.exit(main())

Wilson 区間を選んだ理由は、母数が小さく、クラッシュが 0〜数件という領域で、普通の正規近似(観測値 ± 誤差)が壊れるからです。k が 0 のとき正規近似の幅は 0 になり、「0 件だから完全に安全」という誤った自信を生みます。Wilson は k=0 でも幅を保ち、0〜1 の外にもはみ出しません。終了コードを 0・10・20 に分けているので、シェルスクリプトや CI から呼んで分岐にも使えます。

実際に動かすと、こう返ります。

$ python3 rollout_gate.py --users 420 --crashed 1
WAIT: 1/420 users crashed (観測 99.76% / 真の値は 98.66〜99.96% の範囲)
  目標 99.7% を 0 件で確かめるには約 1277 ユーザー必要
 
$ python3 rollout_gate.py --users 2000 --crashed 14
HALT: 14/2000 users crashed (観測 99.30% / 真の値は 98.83〜99.58% の範囲)
✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦5% 段階の Crash-free 率を見て、「進めてよい」と言える最小のユーザー数を自分の目標値から計算できるようになる
✦良いリリースを数字の揺れで止めてしまう前に、止める条件へ件数の下限を足して誤検知を減らせるようになる
✦「あと何日待てば判断できるか」を、直近の増え方から見積もって、朝の確認回数を減らせるようになる
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

◇ アプリ開発2026-05-29
Android 壁紙アプリ v2.1.0 公開後の保守 — Crashlytics と Claude in Chrome で回す日々の運用
Beautiful 4K/HDR Wallpapers v2.1.0 と Ukiyo-e Wallpapers v1.8.0 の段階公開後、Crashlytics 監視と新規クラッシュ対応を Claude in Chrome に任せる運用に切り替えました。RecyclerView の防御的コピーや Glide の desugar など、実機で踏んだ落とし穴を保守目線でまとめます。
◇ アプリ開発2026-09-07
アプリから外部の AI にユーザーの文章を送るとき、Play の「データの安全性」に何を書くか
外部の AI にユーザーの入力を渡すアプリで、Play のデータ安全性フォームの収集・共有・一時処理をどう区分するか。送信を一箇所に集める実装と、順序を誤ると消し漏れるマスキングの実測を添えて整理します。
◇ アプリ開発2026-08-15
TestFlight の外部テストと段階公開、個人開発ではどちらに時間を使うか
Rork で作ったアプリを他人に配るとき、外部テスターを募集する道と段階公開に受け止めさせる道の二つがあります。個人開発の限られた時間をどちらに置くかを、配布が止まる条件と事前検証すべき三領域から整理しました。