Crashlyticsのダッシュボードを開くたびに「また増えてる…」と感じていませんか。
個人でiOS/Androidアプリを運営していると、クラッシュレポートへの対応はどうしても後回しになりがちです。新機能の開発で手が埋まっているのに、毎日積み上がるレポートを一件一件読んで、再現して、修正する——この繰り返しは本当に消耗します。
私自身、2014年から個人でアプリを作り続ける中で、累計5,000万ダウンロードを超えるまでには「クラッシュ対応が怖い宿題」になっていた時期がありました。Antigravityを使い始めてから、このワークフローが根本的に変わりました。CrashlyticsのNSExceptionスタックトレースをそのままコンテキストとして渡すと、Antigravityがコードベースをスキャンしてどのファイルのどのメソッドが問題かを即座に指摘してくれます。
Firebase CrashlyticsとBigQueryの連携を準備する
まず前提として、Firebase CrashlyticsのBigQueryエクスポートを有効にしておく必要があります。Firebaseコンソールの「Crashlytics」→「BigQueryへのエクスポート」から設定できます。エクスポートが開始されると、firebase_crashlyticsデータセットにクラッシュイベントが自動で蓄積されていきます。
エクスポートの開始から最初のデータが揃うまで24〜48時間かかるため、まだ設定していない場合は先に有効化しておいてください。
設定後、以下のPythonスクリプトで直近7日間の致命的クラッシュ上位10件を取得できます。
# crash_fetcher.py
from google.cloud import bigquery
from datetime import datetime, timedelta
def get_top_crashes (project_id: str , app_id: str , days: int = 7 ) -> list[ dict ]:
"""
Crashlyticsの直近N日間の致命的クラッシュ上位10件を取得する。
app_idはFirebaseコンソールの「プロジェクト設定」>「全般」から確認できる。
例: "1:123456789012:ios:abcdef1234567890"
"""
client = bigquery.Client( project = project_id)
# BigQueryのテーブル名はapp_idの":"を"_"に置換した形式
table_id = app_id.replace( ":" , "_" )
dataset = f " { project_id } .firebase_crashlytics"
table = f " { dataset } . { table_id } "
query = f """
SELECT
issue_id,
issue_title,
issue_subtitle,
COUNT(*) AS crash_count,
ARRAY_AGG(
STRUCT(
application.display_version AS version,
device.manufacturer AS manufacturer,
device.model AS model,
os.display_version AS os_version,
ARRAY(
SELECT CONCAT(frame.file, ':', CAST(frame.line AS STRING))
FROM UNNEST(exceptions[OFFSET(0)].frames) AS frame
WHERE frame.file IS NOT NULL
LIMIT 10
) AS stack_frames
)
ORDER BY event_timestamp DESC
LIMIT 3
) AS recent_examples
FROM ` { table } *`
WHERE
_TABLE_SUFFIX BETWEEN
FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL { days } DAY))
AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
AND is_fatal = TRUE
GROUP BY issue_id, issue_title, issue_subtitle
ORDER BY crash_count DESC
LIMIT 10
"""
results = client.query(query).result()
return [ dict (row) for row in results]
if __name__ == "__main__" :
crashes = get_top_crashes(
project_id = "your-firebase-project-id" ,
app_id = "1:000000000000:ios:aaaaaaaaaaaaaaaa" # 実際のapp_idに変更
)
for crash in crashes:
print ( f "[ { crash[ 'crash_count' ] } 件] { crash[ 'issue_title' ] } " )
# 出力例:
# [247件] Fatal Exception: NSInvalidArgumentException
# [89件] Fatal Exception: EXC_BAD_ACCESS SIGSEGV
google-cloud-bigquery はpipで導入できます。Google Cloud SDKの認証は gcloud auth application-default login で通しておいてください。
クラッシュデータをAntigravityに渡すプロンプトを整形する
取得したデータをAntigravityに渡す際のポイントは「ただ貼り付けるだけ」ではなく、AIが判断しやすい構造で渡す ことです。スタックトレースの前後に文脈(件数・デバイス傾向・質問形式)を添えることで、解析精度が大きく変わります。
# crash_analyzer.py
from crash_fetcher import get_top_crashes
def format_crash_for_ai (crash: dict ) -> str :
"""
Antigravityへのプロンプトとして、Crashlyticsレポートを構造化する。
スタックトレースに件数・デバイス情報・具体的な質問を添えることが精度向上のカギ。
"""
examples = crash.get( "recent_examples" , [])
# 上位デバイス・OS情報をまとめる
devices = list ({
f " { ex[ 'manufacturer' ] } { ex[ 'model' ] } (OS { ex[ 'os_version' ] } )"
for ex in examples
if ex.get( "manufacturer" )
})[: 3 ]
# 最新クラッシュのスタックトレース(先頭10フレーム)
stack_frames = []
if examples and examples[ 0 ].get( "stack_frames" ):
stack_frames = examples[ 0 ][ "stack_frames" ]
prompt = f """
## Crashlyticsクラッシュ解析依頼
**発生件数(直近7日):** { crash[ 'crash_count' ] } 件
**エラータイプ:** { crash[ 'issue_title' ] }
**サブタイトル:** { crash.get( 'issue_subtitle' , 'N/A' ) }
**主な発生環境:** { ', ' .join(devices) if devices else '不明' }
**スタックトレース(最新クラッシュ・先頭10フレーム):**
{ chr ( 10 ).join( f ' { i + 1 } . { frame } ' for i, frame in enumerate (stack_frames)) }
**依頼事項:**
1. このクラッシュの根本原因として最も可能性の高い箇所を特定してください
2. コードベース内で修正すべきファイル・メソッドを示してください
3. 修正アプローチを3つ提案してください(優先度順)
4. 最小再現手順を推測してください
"""
return prompt
if __name__ == "__main__" :
crashes = get_top_crashes( "your-firebase-project-id" , "1:000000000000:ios:aaaaaaaaaaaaaaaa" )
# 上位3件をAntigravityに渡すプロンプトとして出力
for i, crash in enumerate (crashes[: 3 ], 1 ):
print ( f " \n{ '=' * 60 }\n ### クラッシュ # { i } " )
print (format_crash_for_ai(crash))
このスクリプトの出力をAntigravityのチャット欄に貼り付けて送信すると、Antigravityがコードベースを自動検索して原因箇所と修正案を返してくれます。
GitHub Actionsで毎朝自動解析するパイプラインを組む
毎朝自動でクラッシュレポートをSlackに届けるようにしておくと、「週明けに爆発していた」という事態を防げます。以下はGitHub Actionsでの実装例です。
# .github/workflows/crashlytics-daily-analysis.yml
name : Daily Crashlytics Analysis
on :
schedule :
- cron : '0 0 * * 1-5' # 平日の朝9時JST(= UTC 0:00)
workflow_dispatch : # 手動実行にも対応
jobs :
analyze :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- uses : actions/setup-python@v5
with :
python-version : '3.12'
- name : Install dependencies
run : pip install google-cloud-bigquery
- name : Authenticate to Google Cloud
run : |
echo '${{ secrets.GCP_SA_KEY_JSON }}' > /tmp/sa_key.json
echo "GOOGLE_APPLICATION_CREDENTIALS=/tmp/sa_key.json" >> $GITHUB_ENV
- name : Fetch and format crash report
env :
FIREBASE_PROJECT_ID : ${{ secrets.FIREBASE_PROJECT_ID }}
FIREBASE_APP_ID : ${{ secrets.FIREBASE_APP_ID }}
run : python crash_analyzer.py > /tmp/crash_report.txt
- name : Post to Slack
env :
SLACK_WEBHOOK : ${{ secrets.SLACK_CRASHLYTICS_WEBHOOK }}
run : |
# Slackの本文制限(2000文字)に合わせてカット
BODY=$(head -c 1800 /tmp/crash_report.txt)
curl -s -X POST "$SLACK_WEBHOOK" \
-H 'Content-type: application/json' \
-d "{\"text\":\"📊 *今日のCrashlyticsレポート*\n\`\`\`${BODY}\`\`\`\"}"
Slackに届くレポートをAntigravityに渡せば、コードベースの検索から修正提案まで一気通貫で対応できます。
Antigravityでクラッシュを実際に修正する
具体例として、NSInvalidArgumentException: -[__NSCFDictionary objectForKeyedSubscript:]: mutating method sent to immutable object というiOSクラッシュへの対応手順を紹介します。
AntigravityのチャットにCrashlyticsプロンプトを貼り付けると、Antigravityはコードベースをスキャンして辞書(Dictionary)への変更操作を行っている箇所を特定します。このクラッシュはAPIレスポンスのNSDictionaryをimmutableなまま変更しようとするときに発生します。
// ❌ クラッシュする実装
// APIレスポンスのJSONが NSDictionary(immutable)として返ってくる場合
func updateUserLastSeen ( response : [ String : Any ]) {
var userInfo = response[ "user" ] as! [ String : Any ]
userInfo[ "lastSeen" ] = Date ().timeIntervalSince1970 // ← ここでクラッシュ
saveToCache (userInfo)
}
// ✅ Antigravityが提案する修正
// mutableCopyを取ってから変更する
func updateUserLastSeen ( response : [ String : Any ]) {
guard let rawUserInfo = response[ "user" ] as? [ String : Any ] else { return }
var userInfo = rawUserInfo // Swiftの値型コピーはmutable
userInfo[ "lastSeen" ] = Date ().timeIntervalSince1970
saveToCache (userInfo)
}
Antigravityはこの修正パターンをコードベース全体で検索して、同様の問題を含む箇所をまとめて修正提案してくれます。「1件直す」ではなく「同じパターンを全部直す」ことができるのが、AIアシストのメリットです。
クラッシュをユーザー影響度でスコアリングして優先順位をつける
クラッシュ件数だけを見ていると、優先順位を見誤ります。私が長く運営している壁紙アプリ群でも、「件数は多いものの旧OSの少数ユーザーに偏ったクラッシュ」と「件数は中程度でも起動直後の主要導線で起きるクラッシュ」とでは、後者の方が体感品質への影響がはるかに大きいと痛感しました。件数の裏にある「何人のユーザーに、どの画面で」起きているかを見ないと、本当に直すべき一件を取りこぼしてしまいます。
そこでcrash_fetcher.pyのクエリに、ユニーク影響ユーザー数(installation_uuidの重複排除)を加えます。
# crash_fetcher.py の SELECT 句に追加し、影響ユーザー数で並び替える
COUNT( DISTINCT installation_uuid) AS affected_users,
SAFE_DIVIDE(COUNT( * ), COUNT( DISTINCT installation_uuid)) AS crashes_per_user,
# ...(既存の集計列はそのまま)
# GROUP BY のあとの ORDER BY を以下に差し替える
ORDER BY affected_users DESC , crash_count DESC
crashes_per_userが高いクラッシュは、同じユーザーが何度も踏んでいる——つまりそのユーザーにとってアプリが使い物にならなくなっているという深刻なサインです。私は「影響ユーザー数 × 主要導線かどうか」でトリアージし、件数そのものは二次的な指標として扱っています。
さらにAntigravityには、上位10件のリストをそのまま渡して「各クラッシュがどのユーザー導線で起きているか推測し、ビジネスインパクト順に並べ替えてください」と依頼できます。コードベースを読んだ上での並べ替えなので、単純な件数ソートよりも納得感のある優先順位が返ってきます。
実運用で気づいたこと
このワークフローを数ヶ月使ってみて、効果的なケースと苦手なケースが見えてきました。
導入の前後では、自分のアプリでも数値に変化が出ました。クラッシュフリーユーザー率は主要アプリで99.2%前後から99.7%台へ改善し、何より一件あたりの対応時間が大きく縮みました。以前は午後を丸ごと潰していたクラッシュ対応が、月曜の朝に上位3件をまとめて片付ける45分ほどの作業に収まっています。クラッシュはレビュー評価やリテンション、ひいてはAdMobの収益にも直結するため、解決までの時間短縮はそのまま事業の数字に効いてきます。
精度が高いケース : NSInvalidArgumentException・NSRangeException・EXC_BAD_ACCESSのように、スタックトレースに具体的なファイル名と行番号が含まれるケースです。Antigravityが「このファイルのこのメソッド」と明確に特定できるため、修正提案の精度が高めです。
苦手なケース : サードパーティSDKの内部クラッシュや、メモリ破損系のクラッシュはAntigravityでも解析が難しい場面があります。スタックトレースがSDKのバイナリフレームばかりの場合は、「SDK使用箇所のコードレビューを依頼する」方向に切り替えています。
運用リズムとしては、週1回月曜の朝に上位3件を集中して対応する スタイルが自分には合っていました。毎日確認するほど件数が多くなければ、Slackの通知を週に一度まとめてAntigravityに投げるだけで十分です。
AIクラッシュ解析で陥りやすい3つの落とし穴
このワークフローを複数のプロジェクトで回す中で、繰り返し遭遇した失敗が3つありました。
プロンプトを整形せず生のエクスポートを貼ってしまう : Crashlyticsの生データにはデバイスのメタ情報やパーセンタイル統計などのノイズが多く、そのまま渡すとコンテキスト長だけが伸びて解析精度はむしろ落ちます。format_crash_for_ai()でエラー種別・件数・スタックフレーム・具体的な質問だけに絞り込むことが、回答の精度を左右します。
コードベースの文脈を読み込ませずに質問する : Antigravityの強みは、解析時にプロジェクトを検索してくれる点にあります。プロジェクトをインデックスしていない素のセッションにスタックトレースだけ貼ると、ドキュメントのような一般論しか返ってきません。必ずプロジェクトを開いた状態のセッションで解析を依頼してください。
すべてのクラッシュを同じ緊急度で扱う : 前述のとおり、件数は粗いシグナルにすぎません。影響ユーザー数と発生する導線を併せて見ることで、本当に優先すべき一件が浮かび上がってきます。
AndroidアプリでのCrashlytics解析の違い
ここまではiOSの例で説明してきましたが、Firebase CrashlyticsはAndroidでも同じ仕組みで動きます。BigQueryのスキーマもエクスポート手順も共通です。違いはスタックトレースの形式で、Androidはファイル名:行番号ではなくJava/Kotlinのクラスパス表記になります。
システムフレームを除外してからAntigravityに渡すと、解析精度が上がります。
// Android: システムフレームを除外してからAntigravityに渡す
// CrashlyticsのBigQueryは "com.example.Class.method(File.kt:line)" 形式でフレームを返す
fun formatAndroidStack (frames: List < String >): String {
val systemPrefixes = listOf ( "android." , "java." , "kotlin." , "okhttp3." , "com.google." )
return frames
. filter { frame -> systemPrefixes. none { frame. startsWith (it) } }
. take ( 8 )
. mapIndexed { i, frame -> " ${i + 1 }. $frame " }
. joinToString ( " \n " )
}
Kotlin特有のクラッシュ——nullableな戻り値でのNullPointerException、コルーチンのCancellationException、ライフサイクル外でのLiveData購読によるIllegalStateException——は、AntigravityのKotlin理解が安定して効くケースです。iOSとAndroidとで解析品質に大きな差は感じていません。
次の一歩
まずはBigQueryエクスポートを有効にして、crash_fetcher.pyで手動実行してみてください。データが取得できたら、そのままAntigravityに貼り付けるだけで最初の解析ができます。
BigQueryエクスポートの詳細な設定手順についてはFirebase Crashlytics公式ドキュメント が参考になります。また、AntigravityでのBigQuery活用についてはFirebaseとBigQueryを連携した収益ダッシュボードの構築 でも実践的な使い方を紹介しています。クラッシュ解析と組み合わせると、品質と収益の両面をAntigravityで一元管理できるようになります。