2026年5月のある朝、Google Play Console の Android Vitals を開いたとき、クラッシュ件数のグラフが想定外の山を描いていました。
Beautiful HD Wallpapers の Android 版(累計 5,000 万 DL 超)で、v2.0.0 のリリースから 28 日間で 50 名以上のユーザーが RecyclerView.setAdapter() 周りで IndexOutOfBoundsException を経験していたのです。段階公開で 100% に到達した後に気づいたこのクラッシュは、対応に時間がかかり、多くのユーザーに迷惑をかけました。
その反省から構築したのが、Claude Code を使った Play Store 段階公開の自動監視システム です。
段階公開(Staged Rollout)は Google Play が提供する機能で、アップデートを全ユーザーに一度に届けるのではなく、5% → 10% → 25% → 50% → 100% のように段階的に展開できます。問題が起きた場合は途中で止めてロールバックできる、個人開発者にとって非常に心強い機能です。しかし、監視を手動で行っていると見逃しが起きます。私がそれを身をもって経験しました。
なぜ手動監視では不十分なのか
12年間、個人でアプリ開発を続けてきた中で、リリースのたびに Play Console をこまめに確認するよう心がけてきました。しかし現実には、アプリ開発と並行してアート活動やブログ運営もしているため、1日に何度も確認する時間は取れません。
v2.0.0 のケースでは、問題の本質は監視の頻度ではなく、見るべき指標と判断基準が曖昧だった ことにありました。「クラッシュ件数が増えている気がする」という定性的な感覚ではなく、「Crash-free Users が 99.7% を下回ったら段階公開を一時停止する」という定量的な基準が必要だったのです。
Claude Code はこの課題に対して、2つの役割を担います。
Play Console から取得したデータの定量的な分析 — 数値を見て「これは止めるべきか続けるべきか」を判定する
自然言語でのレポート生成 — 何が起きているかを、次のリリース時にも参照できる形で記録する
システムの全体構成
Play Console(手動エクスポート または API)
↓
crash_report.py(データ取得・整形)
↓
Claude API(claude-sonnet-4-6)
↓
Go/No-Go 判定 + レポート生成
↓
通知(Slack または メール)
今回構築したシステムは、Play Console の Android Vitals から取得したデータを Python スクリプトで整形し、Claude API に渡して分析させるシンプルな構成です。Play Console の正式 API(Google Play Developer API)を使う方法と、CSV エクスポートを使う方法の両方に対応しています。
Step 1: Android Vitals データの取得
Play Console の Android Vitals では、以下の指標をアプリごとにモニタリングできます。
Crash-free Users Rate : クラッシュが発生しなかったユーザーの割合
ANR-free Users Rate : ANR(Application Not Responding)が発生しなかったユーザーの割合
クラッシュ件数 : 期間中の総クラッシュ数
Google Play Developer API を使う場合、androidpublisher.v3 の reviews エンドポイントではなく、vitals.crashrate.get を使います。
まず認証の準備です。Google Cloud Console でサービスアカウントを作成し、Play Console に連携させます。
# google_play_vitals.py
# Google Play Developer API から Crash-free Rate を取得する
from google.oauth2 import service_account
from googleapiclient.discovery import build
from datetime import datetime, timedelta
import json
PACKAGE_NAME = "net.dolice.beautifulwallpapers"
SERVICE_ACCOUNT_FILE = "service_account.json"
SCOPES = [ "https://www.googleapis.com/auth/androidpublisher" ]
def get_crash_metrics (days_back: int = 7 ) -> dict :
"""指定期間のクラッシュ指標を取得する"""
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE , scopes = SCOPES
)
service = build( "androidpublisher" , "v3" , credentials = credentials)
end_date = datetime.now()
start_date = end_date - timedelta( days = days_back)
# Crash-free Users Rate を取得
request = service.vitals().crashrate().get(
packageName = PACKAGE_NAME ,
# メトリクス: CRASH_RATE = 全ユーザー中クラッシュが発生したユーザーの割合
# つまり Crash-free Rate = 1 - crash_rate
)
result = request.execute()
return result
def parse_crash_free_rate (vitals_data: dict ) -> float :
"""API レスポンスから Crash-free Users Rate を計算する"""
# crash_rate が返ってくる(クラッシュが発生したユーザーの割合)
# Crash-free Rate = 100 - crash_rate_percentage
crash_rate = vitals_data.get( "crashRate" , {}).get( "rate" , 0 )
crash_free_rate = ( 1 - crash_rate) * 100
return round (crash_free_rate, 3 )
CSV エクスポートを使う場合は、Play Console の「Android Vitals」→「クラッシュと ANR」から期間を選択してダウンロードした CSV を読み込む方法が簡単です。
# csv_reader.py
# Play Console から手動エクスポートした CSV を読み込む(API不使用の場合)
import csv
from pathlib import Path
def read_vitals_csv (csv_path: str ) -> list[ dict ]:
"""Play Console の Android Vitals CSV を読み込む"""
rows = []
with open (csv_path, encoding = "utf-8-sig" ) as f:
reader = csv.DictReader(f)
for row in reader:
rows.append({
"date" : row.get( "Date" , "" ),
"crash_free_rate" : float (row.get( "Crash-free users (%)" , 0 )),
"anr_free_rate" : float (row.get( "ANR-free users (%)" , 0 )),
"crash_count" : int (row.get( "Crashes" , 0 )),
"version" : row.get( "App version" , "" )
})
return rows
Step 2: Claude Code でのスクリプト作成と改善
ここから Claude Code が本格的に活躍します。
最初のスクリプトを書いたとき、Claude Code に以下のように依頼しました。
「Play Console のデータを受け取って、段階公開を続けるべきか止めるべきかを判定する Python スクリプトを書いてほしい。Crash-free Users が 99.7% を下回ったら警告、99.5% を下回ったら即時停止推奨とします。ANR は 99.8% を基準にする」
Claude Code が生成したスクリプトのベースは動作しましたが、実際のデータを当てはめてみると、いくつかの課題が出てきました。サンプルサイズが小さい初期段階(5%公開時)では、少数のクラッシュでも率が大きく振れる という問題です。
5% 公開時に 1 件クラッシュが発生すると、ユーザー数が少ないため Crash-free Rate が著しく低く見える一方、これは実際には深刻ではない可能性があります。この問題を Claude Code に伝えると、信頼区間を考慮した判定ロジック を提案してくれました。
# go_no_go_analyzer.py
# 段階公開の Go/No-Go 判定ロジック
import math
from dataclasses import dataclass
from enum import Enum
class Decision ( Enum ):
PROCEED = "PROCEED" # 次のステップへ進む
WATCH = "WATCH" # 様子見(警戒)
PAUSE = "PAUSE" # 一時停止
ROLLBACK = "ROLLBACK" # ロールバック推奨
@dataclass
class RolloutMetrics :
crash_free_rate: float # 例: 99.8 (%)
anr_free_rate: float # 例: 99.9 (%)
crash_count: int # 期間中の総クラッシュ数
active_users: int # 対象ユーザー数
rollout_percentage: int # 現在の公開割合 (5, 25, 50, 100)
days_elapsed: int # 公開からの経過日数
def wilson_lower_bound (successes: int , total: int , z: float = 1.96 ) -> float :
"""
Wilson スコア区間の下限を計算する
クラッシュなしユーザーを「成功」として信頼区間を推定する
z=1.96 は 95% 信頼区間に相当する
"""
if total == 0 :
return 0.0
p_hat = successes / total
denominator = 1 + z ** 2 / total
center = p_hat + z ** 2 / ( 2 * total)
margin = z * math.sqrt(p_hat * ( 1 - p_hat) / total + z ** 2 / ( 4 * total ** 2 ))
return (center - margin) / denominator * 100
def analyze_rollout (metrics: RolloutMetrics) -> tuple[Decision, str ]:
"""
段階公開の Go/No-Go を判定する
Returns: (判定結果, 理由の説明)
"""
# サンプルサイズの計算(Crash-free Users 数)
crash_free_users = int (metrics.active_users * metrics.crash_free_rate / 100 )
# Wilson 下限(最悪ケースの推定)
wilson_lower = wilson_lower_bound(crash_free_users, metrics.active_users)
# 段階ごとの最小経過日数(十分なサンプルを確保するため)
min_days = { 5 : 1 , 25 : 2 , 50 : 3 , 100 : 5 }.get(metrics.rollout_percentage, 3 )
reasons = []
# 1. 絶対的な閾値チェック
if metrics.crash_free_rate < 99.5 :
return Decision. ROLLBACK , f "Crash-free Users が { metrics.crash_free_rate :.3f } % — 即時ロールバック推奨(基準: 99.5% 未満)"
if metrics.anr_free_rate < 99.8 :
return Decision. ROLLBACK , f "ANR-free Users が { metrics.anr_free_rate :.3f } % — ロールバック推奨(基準: 99.8% 未満)"
# 2. Wilson 下限による信頼区間チェック
if wilson_lower < 99.5 :
reasons.append( f "Wilson 下限 { wilson_lower :.3f } % が警戒ゾーン(サンプルサイズ: { metrics.active_users :, } )" )
# 3. 観測期間が不十分な場合
if metrics.days_elapsed < min_days:
reasons.append( f "観測期間 { metrics.days_elapsed } 日(推奨: { min_days } 日以上)" )
# 4. 警告ゾーン
if metrics.crash_free_rate < 99.7 :
reasons.append( f "Crash-free Users { metrics.crash_free_rate :.3f } % — 警告ゾーン(基準: 99.7%)" )
if reasons:
return Decision. WATCH , "、" .join(reasons)
return Decision. PROCEED , f "すべての指標が基準を満たしています(Crash-free: { metrics.crash_free_rate :.3f } %、ANR-free: { metrics.anr_free_rate :.3f } %)"
このロジックの要点は、Wilson スコア区間の下限 を使っている部分です。5% 公開時は対象ユーザーが少なく、統計的な信頼性が低いため、観測値そのものではなく「最悪ケースの推定値」で判断します。これにより、初期段階での過剰反応を防ぎながら、本当に問題がある場合は早期に検知できます。
Step 3: Claude API によるレポート生成
数値だけでは「次のリリース時に何を改善すべきか」が残りません。Claude API を使って、段階公開の経緯を自然言語でドキュメント化します。
# report_generator.py
# Claude API を使って段階公開レポートを生成する
import anthropic
from datetime import datetime
client = anthropic.Anthropic() # ANTHROPIC_API_KEY は環境変数から
def generate_rollout_report (
app_version: str ,
metrics_history: list[ dict ],
decisions: list[ tuple ],
final_outcome: str
) -> str :
"""
段階公開の履歴から運用レポートを生成する
metrics_history: 各段階での指標データのリスト
decisions: 各段階での判定結果と理由のリスト
"""
# プロンプト構築
history_text = " \n " .join([
f "- { d[ 'rollout_pct' ] } % 公開( { d[ 'days_elapsed' ] } 日経過): "
f "Crash-free { d[ 'crash_free_rate' ] :.3f } %, ANR-free { d[ 'anr_free_rate' ] :.3f } %, "
f "クラッシュ { d[ 'crash_count' ] } 件 → { decisions[i][ 0 ].value } "
for i, d in enumerate (metrics_history)
])
prompt = f """以下は Android アプリ(バージョン { app_version } )の段階公開モニタリング記録です。
## 段階別データ
{ history_text }
## 最終結果
{ final_outcome }
この記録を元に、以下の観点で簡潔なレポートを日本語で作成してください:
1. 今回のリリースで特筆すべき指標の動き(あった場合)
2. Go/No-Go 判定が「WATCH」または「PAUSE」になった原因(あった場合)
3. 次のリリースに向けた改善提案(具体的に1〜2点)
文体は「〜でした」「〜でしょう」の敬体で、技術的に正確に書いてください。
余計な前置きは不要です。直接内容から始めてください。"""
message = client.messages.create(
model = "claude-sonnet-4-6" ,
max_tokens = 1024 ,
messages = [
{ "role" : "user" , "content" : prompt}
]
)
return message.content[ 0 ].text
def save_report (version: str , report: str ) -> None :
"""レポートをファイルに保存する"""
timestamp = datetime.now().strftime( "%Y%m %d _%H%M" )
filename = f "rollout_report_ { version } _ { timestamp } .md"
with open ( f "reports/ { filename } " , "w" , encoding = "utf-8" ) as f:
f.write( f "# Play Store 段階公開レポート: { version }\n\n " )
f.write( f "生成日時: { datetime.now().strftime( '%Y-%m- %d %H:%M JST' ) }\n\n " )
f.write(report)
print ( f "✅ レポート保存: reports/ { filename } " )
Step 4: Claude Code でのシステム統合
個々のスクリプトができたところで、Claude Code を使って全体を統合し、実際に動かせる状態にします。ここで Claude Code が特に役立つのは、エラーハンドリングの網羅 です。
段階公開の監視システムは、長期間(リリースから 2〜4 週間)動き続けます。この間に API のレート制限、認証トークンの期限切れ、Play Console の一時的なデータ欠損など、さまざまな例外が発生し得ます。
Claude Code に「本番運用に耐えるエラーハンドリングを追加して」と依頼すると、以下のような統合スクリプトを提案してくれました。
# monitor.py
# 段階公開モニタリングのメインスクリプト(統合版)
import time
import logging
from pathlib import Path
from typing import Optional
from dataclasses import dataclass
logging.basicConfig(
level = logging. INFO ,
format = " %(asctime)s [ %(levelname)s ] %(message)s " ,
handlers = [
logging.FileHandler( "monitor.log" ),
logging.StreamHandler()
]
)
logger = logging.getLogger( __name__ )
@dataclass
class MonitorConfig :
package_name: str
check_interval_hours: int = 6 # 6時間ごとにチェック
rollout_percentage: int = 5 # 現在の公開割合
crash_free_warning: float = 99.7 # 警告閾値
crash_free_stop: float = 99.5 # 停止閾値
anr_free_stop: float = 99.8 # ANR 停止閾値
notification_webhook: Optional[ str ] = None # Slack webhook URL
def run_monitor (config: MonitorConfig) -> None :
"""
段階公開モニタリングのメインループ
エラー時は指数バックオフでリトライする
"""
retry_count = 0
max_retries = 3
while True :
try :
logger.info( f "[ { config.package_name } ] 監視チェック開始 ( { config.rollout_percentage } % 公開中)" )
# データ取得(CSV または API)
metrics = fetch_current_metrics(config)
if metrics is None :
logger.warning( "メトリクス取得失敗 — データ欠損の可能性。次回チェックまで待機" )
time.sleep(config.check_interval_hours * 3600 )
continue
# Go/No-Go 判定
decision, reason = analyze_rollout(metrics)
logger.info( f "判定: { decision.value } — { reason } " )
# 通知送信(PAUSE/ROLLBACK の場合は必ず)
if decision in (Decision. PAUSE , Decision. ROLLBACK ):
send_alert(
config = config,
decision = decision,
metrics = metrics,
reason = reason
)
if decision == Decision. ROLLBACK :
logger.critical( "🚨 ロールバック推奨 — 即時対応が必要です" )
# ロールバック推奨時はモニタリングを停止して手動対応を待つ
break
retry_count = 0 # 成功したらリトライカウントをリセット
except Exception as e:
retry_count += 1
wait_seconds = min ( 300 * ( 2 ** retry_count), 3600 ) # 最大1時間
logger.error( f "エラー発生 (試行 { retry_count } / { max_retries } ): { e } " )
if retry_count >= max_retries:
logger.critical( "最大リトライ回数に達しました。手動確認が必要です。" )
send_alert( config = config, decision = Decision. PAUSE ,
metrics = None , reason = f "監視システムエラー: { e } " )
break
logger.info( f " { wait_seconds } 秒後に再試行します" )
time.sleep(wait_seconds)
continue
# 次のチェックまで待機
logger.info( f "次のチェックまで { config.check_interval_hours } 時間待機" )
time.sleep(config.check_interval_hours * 3600 )
def send_alert (config: MonitorConfig, decision: Decision,
metrics: Optional[RolloutMetrics], reason: str ) -> None :
"""Slack または標準出力にアラートを送信する"""
if not config.notification_webhook:
print ( f "⚠️ アラート: { decision.value } — { reason } " )
return
import requests
emoji = { "PROCEED" : "✅" , "WATCH" : "👀" , "PAUSE" : "⏸️" , "ROLLBACK" : "🚨" }.get(decision.value, "ℹ️" )
message = {
"text" : f " { emoji } *Play Store 段階公開アラート*" ,
"blocks" : [
{
"type" : "section" ,
"text" : {
"type" : "mrkdwn" ,
"text" : f "* { emoji } { decision.value } * — { config.package_name }\n{ reason } "
}
}
]
}
if metrics:
message[ "blocks" ].append({
"type" : "section" ,
"fields" : [
{ "type" : "mrkdwn" , "text" : f "*Crash-free* \n{ metrics.crash_free_rate :.3f } %" },
{ "type" : "mrkdwn" , "text" : f "*ANR-free* \n{ metrics.anr_free_rate :.3f } %" },
{ "type" : "mrkdwn" , "text" : f "*公開割合* \n{ metrics.rollout_percentage } %" },
{ "type" : "mrkdwn" , "text" : f "*クラッシュ数* \n{ metrics.crash_count :, } " }
]
})
try :
response = requests.post(config.notification_webhook, json = message, timeout = 10 )
response.raise_for_status()
except Exception as e:
logger.error( f "通知送信失敗: { e } " )
実際の v2.1.0 リリースでの適用結果
このシステムを使って Beautiful HD Wallpapers Android v2.1.0 をリリースしました。
v2.1.0 の主な変更は、前述の RecyclerView クラッシュ修正(防御的コピーによる根本対応)と、Glide 5.0.5 × AGP 9.x 環境での Java 8 Supplier NoClassDefFoundError の対応(coreLibraryDesugaringEnabled true の追加)です。後者は Android 6.0.1 以下のユーザーで発生していたもので、v2.0.0 のリリース後に報告が相次いでいました。
段階公開の経緯は以下の通りです。
5% 公開(Day 1〜2)
Crash-free Users: 99.94%、ANR-free Users: 100%。Wilson 下限は 99.71%(サンプル小のため)。判定: WATCH。観測期間が 1 日のため、基準の 2 日を満たしていない状態でしたが、数値は良好でした。
25% 公開(Day 3〜5)
Crash-free Users: 99.89%、ANR-free Users: 99.97%。クラッシュ件数は 3 件。Wilson 下限: 99.82%。判定: PROCEED。
50% 公開(Day 6〜9)
Crash-free Users: 99.91%、ANR-free Users: 99.98%。クラッシュ件数は 7 件。判定: PROCEED。この時点で Claude API によるレポート生成を実行し、「Android 6.0.1 ユーザーのクラッシュが v2.0.0 から消えていることを確認」というコメントが自動生成されました。
100% 公開(Day 10〜)
Crash-free Users: 99.88%(28 日間平均)。v2.0.0 の同期間の値は 99.61% でした。
v2.0.0 で 28 日間に 50 件超あったクラッシュが、v2.1.0 では段階公開を通じて早期に異常値を捕捉できる体制を作ったことで、100% 公開後も安定した状態を維持できました。
Claude Code が特に役立った場面
今回の開発を通じて、Claude Code が特に力を発揮したのは以下の 3 点でした。
Wilson スコア区間の実装提案
「サンプルサイズが小さいとき、観測値だけでは判断できない」という問題提起に対して、Wilson スコア区間を使った解決策を提案してくれました。私自身はこの手法を知っていましたが、Python での実装を一から書くよりも、Claude Code に「95% 信頼区間の下限を計算する関数を書いて」と依頼した方が、エッジケース(total=0 のとき)まで考慮したコードを素早く得られました。
指数バックオフのエラーハンドリング
長期稼働するモニタリングスクリプトにとって、エラー時の挙動設計は重要です。最初に書いたスクリプトには try-except はありましたが、リトライ戦略がありませんでした。Claude Code に「本番運用を想定したエラーハンドリングを追加して」と依頼したことで、指数バックオフと最大リトライ数の実装が加わりました。
レポートのプロンプト設計
Claude API を呼ぶプロンプトの最適化は、試行錯誤が必要な作業です。最初のバージョンでは、生成されたレポートが冗長で、次のリリース時に参照する気になりませんでした。Claude Code に「もっと端的に、改善提案に重点を置いたプロンプトに書き換えて」と伝えると、プロンプト自体を改善してくれました。AIにAIのためのプロンプトを書かせるという構図ですが、これは実際に有効です。
導入に向けた注意点
このシステムを導入する際に、いくつか気をつけた方がよい点があります。
Play Console のデータには遅延がある
Android Vitals のデータは、リアルタイムではなく数時間〜24時間の遅延があります。特に 5% 公開の初期段階では、データが十分に蓄積されるまで待つ必要があります。days_elapsed の最小値を段階ごとに設定しているのは、この遅延を考慮したためです。
Crash-free Rate の定義を把握する
Google の Crash-free Rate は「期間中に 1 度もクラッシュを経験しなかったユーザーの割合」です。1 人のユーザーが 10 回クラッシュしても「1 件」としてカウントされます。この定義を理解した上で閾値を設定することをお勧めします。
全バージョンの指標を比較する
閾値の設定は、過去のリリースのデータを元に行うのが確実です。アプリによって「正常な Crash-free Rate」は異なります。Beautiful HD Wallpapers では 99.8% 以上が通常の状態ですが、別のアプリでは 99.5% が通常値かもしれません。
段階公開の割合をどう決めるか
5% → 25% → 50% → 100% という割合は、私が過去のリリースで何度か調整を経て落ち着いた設定です。
Google Play が推奨するデフォルトは 10% → 20% → 50% → 100% ですが、累計 5,000 万 DL のアプリでは 10% でも数万人に届いてしまいます。問題が起きてから対応するまでの時間を短くしたいなら、最初の段階はできるだけ小さくする方が安全です。
5% を選んだ理由は、モニタリングの観点からも理にかなっています。5% という小さな窓でも、ユーザー数が多いアプリなら数千〜数万人のサンプルを確保できます。一方、ユーザー数が少ないアプリ(数万 DAU 以下)なら、5% では Wilson 下限の信頼性が低くなりすぎるため、10〜15% 程度から始めた方がよい場合もあります。
経過日数の設定
各段階に最小経過日数を設けている理由は、Play Console のデータ遅延と、ユーザーの使用パターンのばらつきの両方に対応するためです。
たとえば、壁紙アプリは週末に使用率が上がる傾向があります。月曜日に 5% 公開を開始し、翌日の火曜日だけのデータで判断すると、週末に特定の機能を使うユーザー層のクラッシュを見逃す可能性があります。少なくとも平日と週末の両方のデータが含まれる期間を待つことで、使用パターンの多様性を反映した判断ができます。
v2.0.0 の失敗を分析する
v2.0.0 でなぜクラッシュが見逃されたのか、今振り返ると 3 つの要因が重なっていました。
要因 1: 視覚的な異常への鈍感化
Daily Active Users が多いアプリでは、多少のクラッシュは常に発生します。Play Console の Android Vitals グラフを見ていると、「いつも少しある」という感覚が生まれ、「今回も許容範囲だろう」という思い込みにつながります。数値の絶対値ではなく、前バージョンとの比較(Δクラッシュ数、Δ Crash-free Rate)を見ていれば、異変に早く気づけたはずです。
要因 2: クラッシュの再現難易度
RecyclerView.setAdapter() 周りの IndexOutOfBoundsException は、特定のスクロール操作のタイミングで発生するレースコンディションでした。手元の端末では再現せず、「理論上は起きるはずだが、実際に見たことはない」という状態で進めてしまいました。Claude Code に「この種のクラッシュはどういう状況で起きやすいか」を聞いていれば、防御的コピーのパターンを事前に適用できたかもしれません。
要因 3: 段階公開の「安心感」への過信
段階公開をしているから問題が起きても早めに気づけるはず、という漠然とした安心感がありました。しかし、段階公開はあくまで影響範囲を限定するツールであり、問題を自動検知するツールではありません。監視の仕組みがなければ、100% に到達するまで気づかない可能性があります。これが v2.0.0 で実際に起きたことでした。
閾値 99.7% と 99.5% の根拠
「なぜ 99.7% を警告、99.5% を停止にするのか」という疑問は、このシステムを紹介すると必ず出てきます。
これらの数値は普遍的なものではありません。私が 12 年間の個人アプリ開発で観測してきた Beautiful HD Wallpapers の正常値から設定しています。
具体的には、過去 10 バージョン分の 28 日間 Crash-free Rate の中央値が 99.84% でした。標準偏差を考慮すると、99.7% は「通常の変動範囲の下限」に相当し、これを下回ったときは何らかの原因がある可能性が高いと判断できます。99.5% は、v2.0.0 のクラッシュが最も深刻だった期間の値(99.61%)を下回る水準として設定しました。
あなたのアプリに適用する場合は、まず過去 3〜5 バージョンの Crash-free Rate の平均と標準偏差を計算してください。平均から標準偏差の 2 倍を引いた値を「警告」、3 倍を引いた値を「停止」とするのが、統計的に意味のある設定の出発点になります。
Claude Code との実際のやり取りから学んだこと
このシステムを Claude Code と対話しながら作る中で、いくつかの気づきがありました。
問いの立て方が答えの質を決める
最初に「段階公開の監視スクリプトを作って」と依頼したとき、Claude Code が生成したコードは動作しましたが、実運用上の課題(サンプルサイズ問題、データ遅延)が考慮されていませんでした。「5% 公開直後のようにサンプルサイズが小さい場合、単純な閾値チェックで何が問題になるか」という問いを追加したことで、Wilson 信頼区間の提案につながりました。
Claude Code は課題を正確に提示すれば、想定以上の深さで回答してくれます。ただし、課題を曖昧に伝えると、表面的には動くが実運用には耐えないコードが返ってくることがあります。これは Claude Code の限界というよりも、依頼の質の問題です。
コードの「なぜ」を確認する習慣
Wilson 下限の実装を受け取ったとき、私は「これは正しい実装か」を自分で確認しました。数式を Wikipedia で調べ、境界値(total=0)での挙動をテストしました。Claude Code が生成するコードを信頼することと、それを検証することは別の話です。特にリリース判断に使うロジックは、生成されたコードをそのまま使わず、自分で理解した上で本番に入れることをお勧めします。
プロンプトを Claude Code に改善させる
レポート生成プロンプトの改善は、「このプロンプトで生成されたレポートが長すぎて読まありません。もっと端的で、次リリースに向けた改善提案に重点を置いたプロンプトに書き直して」という依頼から始まりました。AI のプロンプトを AI に改善させるのは、一見奇妙に思えます。しかし、「どういう出力が欲しいか」を明確に言語化できるなら、プロンプトの最適化も Claude Code の得意な仕事です。
実運用で気づいた細かな注意点
このシステムを数週間動かしてみて、当初は想定していなかった細かな課題がいくつか出てきました。
CSV エクスポートのファイル名の変化
Play Console から CSV をエクスポートすると、ファイル名にタイムスタンプが含まれます。自動でファイルを読み込むスクリプトを書く場合は、ファイル名をハードコードせず glob でパターンマッチングする設計にしておかないと、毎回ファイル名を更新する手間が生じます。
import glob
def find_latest_vitals_csv (download_dir: str ) -> str | None :
pattern = f " { download_dir } /crashrate_*.csv"
files = sorted (glob.glob(pattern))
return files[ - 1 ] if files else None
単一バージョンと複数バージョンのデータ混在
Play Console の Crash-free Rate は、デフォルトで複数のアプリバージョンを合算して表示します。段階公開中は旧バージョン(v2.0.0)と新バージョン(v2.1.0)が同時に稼働しているため、バージョンを絞り込んだデータを取得するのが原則です。Android Vitals の「アプリのバージョン」フィルターを必ず設定してエクスポートするか、API 側でバージョン指定のパラメータを渡してください。バージョンを混在させたまま閾値判定をすると、旧バージョンのクラッシュが新バージョンの評価を歪めます。
クラッシュ種別の把握
Crash-free Rate が下がったとき、そのクラッシュが新機能に起因するものか、それとも元々存在していた古いバグが露出しただけなのかを区別できるようにしておくべきでしょう。Android Vitals の「クラッシュの詳細」からスタックトレースを確認し、変更差分と照合する作業は、自動化ではなく人間が行う必要があります。このシステムはあくまで「アラートを上げる」役割であり、原因の分析は開発者の判断が不可欠です。自動化できるのは「数値の監視と通知」であり、「コードを直す判断」は人間の仕事として残ります。
まず手元のアプリの過去 3〜5 バージョン分の Crash-free Rate を Play Console で確認することから始めてみてください。その数値がシステム設計の起点になります。統計的な根拠のある閾値があれば、判断の迷いが消え、リリースに対する構えが少し変わるはずです。
個人開発を 12 年続ける中で、リリースのたびに緊張する気持ちは今も変わりません。ただ、数値で判断できる仕組みを作ることで、その緊張が「不安」ではなく「準備」に変わる感覚があります。同じようにアプリを作り続けている方の参考になれば幸いです。実際のリリースで試した感想や、自分のアプリでの閾値の設定について、気づいたことがあればぜひ共有してみてください。