個人で複数のアプリを長く運用していると、毎朝の習慣がひとつ重荷になっていることに、あるときふと気づきました。Firebase Crashlytics のダッシュボードを開く時間です。前日の夜にリリースした更新がどこかで小さくこけていないかを、コーヒーをいれる前に確認します。それが3〜4分で済む朝もあれば、新しい OS の細かな挙動変化に巻き込まれて1時間以上溶ける朝もありました。
リリース後の障害対応というのは、本来は分業で吸収する仕事です。SRE がいて、運用エンジニアがいて、サポート窓口がいます。けれど個人開発者は、これらをすべて一人で抱える前提で動いています。広告収益がそれなりの規模に育った時期も、その裏側ではほとんど毎日、何かしらのクラッシュレポートを読んでいました。他の活動と並行しながら、それでも個人開発を細く長く続けていくためには、この朝のルーチンを何とかしなければならない、と静かに思い続けていました。
Claude Code を本格的に使い始めて変わったのは、クラッシュレポートを「読む対象」から「処理する対象」に置き直せたことです。本記事はその仕組みを、私自身が今でも回している運用とコード例で書きます。表面的な「ログをAIに貼り付けて聞いてみよう」という話ではなく、レポートの取得・正規化・仮説生成・該当コード特定・差分提案までを、ひとつのパイプラインとして組む方法を紹介します。
なぜ個人開発者ほど「リリース後の障害対応」が重い負荷になるのか
クラッシュレポートの対応コストは、単に「件数 × 1件あたりの調査時間」では測れません。もう少し正確に言えば、個人開発者には以下のような特殊な構造的負荷がかかっています。
ひとつは、コンテキストスイッチの代償です。新規開発でコードを書いている最中に Crashlytics の通知メールが届くと、頭の中の作業記憶を一度退避してクラッシュ調査に切り替えなくてはいけません。組織の中であれば「あとで担当者が見る」と委ねられる場面でも、個人開発では自分の中でこれを切り替えるしかありません。私の経験では、新機能の設計に集中している1時間と、クラッシュ調査に1時間使った後の1時間では、生産性が体感で3〜4倍違います。
もうひとつは、ユーザー数のスケールです。長く配信を続けたアプリは、月間アクティブで見れば数十万人規模になることもあり、しかもタブレット・スマホ・国・OS バージョンが多様に分布します。クラッシュ率が0.1%しかなくても、毎日数百件は何らかのレポートが上がってきます。手で開いて読むのは現実的ではなく、しかし「無視する」と評価が落ちて売上が削れる。この板挟みが個人開発者の日常です。
そしてもうひとつ、心理的な負荷があります。クラッシュレポートを開くということは、自分が書いたコードに穴があったことを直視する作業です。何年やっても、スタックトレースの先頭に自分のソースが乗っていると静かに胃が縮みます。これを毎朝続けるのは、技術的にも精神的にも持続可能ではありません。
ここで私が出した結論は単純です。「読む」のをやめる。代わりに、Claude Code に処理させ、自分は判断と意思決定だけに集中します。この発想の転換が、本記事の出発点です。
レポートを「Claude Code に渡せる形」に整える前処理
Claude Code に直接スタックトレースを貼り付けて聞いても、ある程度の答えは返ってきます。けれど個人開発の運用に耐える品質には届きません。理由は単純で、生のレポートはノイズが多すぎ、また同じ原因のレポートが何百件と複製されているからです。仕組み化の最初のステップは前処理です。
私が組んでいる流れはこうです。
Crashlytics または Sentry の API を叩いて、過去24時間の Issue を JSON で取得する
スタックトレースを正規化(行番号の差異を吸収)し、signature を計算する
同じ signature のレポートをマージし、発生件数・影響ユーザー数・初出時刻・最終発生時刻だけを残す
上位 N 件を「優先トリアージキュー」として JSON ファイルに書き出す
ここまでをスクリプトで自動化しておけば、Claude Code に渡すのは「整理済みの優先キュー」だけで済みます。ノイズが落ち、重複が消え、調査対象が明確になります。
実装例を示します。Sentry を使っている方を想定したスクリプトですが、Crashlytics でも考え方は同じです。
# fetch_top_issues.py
# 直近24時間のクラッシュ Issue を取得し、影響ユーザー数の多い順に上位10件を整形して出力します。
# 期待出力: triage_queue.json (Claude Code に渡す形式)
import os
import json
import time
import urllib.request
SENTRY_TOKEN = os.environ[ "SENTRY_AUTH_TOKEN" ]
ORG_SLUG = os.environ[ "SENTRY_ORG_SLUG" ]
PROJECT_SLUG = os.environ[ "SENTRY_PROJECT_SLUG" ]
def fetch_issues (hours: int = 24 , limit: int = 50 ) -> list :
url = (
f "https://sentry.io/api/0/projects/ { ORG_SLUG } / { PROJECT_SLUG } /issues/"
f "?statsPeriod= { hours } h&query=is:unresolved&sort=user&limit= { limit } "
)
req = urllib.request.Request(url, headers = { "Authorization" : f "Bearer { SENTRY_TOKEN } " })
with urllib.request.urlopen(req, timeout = 30 ) as r:
return json.loads(r.read())
def normalize (issue: dict ) -> dict :
# 末端のフレームだけを正規化キーとして使う(行番号は外す)
frames = issue.get( "metadata" , {}).get( "function" ) or ""
return {
"id" : issue[ "id" ],
"title" : issue[ "title" ],
"culprit" : issue.get( "culprit" ),
"signature" : frames.split( "(" )[ 0 ].strip(),
"users" : issue.get( "userCount" , 0 ),
"events" : issue.get( "count" , 0 ),
"first_seen" : issue.get( "firstSeen" ),
"last_seen" : issue.get( "lastSeen" ),
"permalink" : issue.get( "permalink" ),
}
if __name__ == "__main__" :
issues = fetch_issues()
triaged = [normalize(i) for i in issues]
# 影響ユーザー数の多い順
triaged.sort( key =lambda x: x[ "users" ], reverse = True )
queue = triaged[: 10 ]
with open ( "triage_queue.json" , "w" ) as f:
json.dump(queue, f, ensure_ascii = False , indent = 2 , default = str )
print ( f "wrote { len (queue) } issues to triage_queue.json at { time.strftime( '%Y-%m- %d %H:%M' ) } " )
このスクリプトを cron か GitHub Actions で1日1回流しておくだけで、毎朝 triage_queue.json がリポジトリに用意されます。Claude Code はこのファイルを起点に作業を始めればよく、ダッシュボードを開きにいく必要はありません。
ここで重要なのは、シンボリケーション(iOS の dSYM や Android の Proguard mappings の解決)はこのステップより前に済ませておくことです。Sentry も Crashlytics も、シンボル解決前のスタックトレースをそのまま渡されると Claude Code の仮説生成が大きく外れます。私はリリースパイプラインの中で fastlane upload_symbols_to_sentry を必ず叩くようにしています。
仮説生成と該当コード位置の特定
整形済みの triage_queue.json を Claude Code に読ませる際、私は単発のプロンプトではなく、3層に分けた sub-agent 構成をとっています。一段の依頼で「原因も特定して修正もして」と頼むと、思考のステップが混ざってどこで間違えたのかが追えなくなるためです。
層を分けるメリットは、各層の出力を独立して人間が確認できることにあります。私自身が「読む」作業から離脱したいのは事実ですが、AI が組み立てた論理に対しては、まだ私の判断を最後に挟む必要があります。
層の役割は次のとおりです。
層1: スタックトレース → 原因仮説(最大3つ)の列挙。コード本体は読まない
層2: 仮説 → リポジトリ内で該当ファイル・関数を特定。grep 相当の探索のみ
層3: 該当箇所 → 修正候補の差分(diff 形式)と、適用してよいかの自己判定
層1のプロンプト例は次の形にしています。
あなたは個人開発のクラッシュレポートをトリアージする補助役です。
入力: triage_queue.json の中の1 issue。
タスク: スタックトレースとタイトル、初出/最終発生時刻だけを根拠に、
最も可能性が高い原因仮説を3つ、確信度順に列挙してください。
制約:
- コード本体を読みにいかないでください。仮説の列挙までです。
- 「再現条件が不明」「ライブラリ起因の可能性」を仮説に含めてよい。
- 各仮説に「もしこれが原因ならどのファイルを見るべきか」を1行で添える。
出力形式: JSON 配列 [{rank, hypothesis, confidence, where_to_look}]
層2と層3は、層1の出力を入力として連鎖させます。Claude Code の --input-file オプションと subagent 機能を組み合わせて、一連のフローを Makefile から呼び出す形にしておくと運用が楽になります。
# Makefile (抜粋)
triage : triage_queue.json
claude code run prompts/layer1_hypothesis.md \
--input-file triage_queue.json --output hypotheses.json
claude code run prompts/layer2_locate.md \
--input-file hypotheses.json --output locations.json
claude code run prompts/layer3_diff.md \
--input-file locations.json --output proposed_diffs/
review : proposed_diffs/
@ echo "次のアクション: proposed_diffs/ を1つずつ確認して採用判断してください"
このように分割しておくと、層1だけ走らせて中身を眺める日もあれば、午前中の集中時間を切らずに make triage を裏で流して夕方の隙間時間に make review する日もあります。生活リズムに合わせて運用を変えられるのが、個人開発で何よりありがたい性質です。
修正案の生成と「適用してはいけない」見極め
層3が出してくる差分は、どんなに精度が上がっても、そのまま git apply で取り込んではいけません。私が運用の中で繰り返し見てきた「適用してはいけないケース」は、おおまかに3つに分類できます。
ひとつ目は、再現条件が不明のままパッチが当たっているケースです。Claude Code は提示されたコードと文脈の中で最も整合する変更を提案しますが、その変更が実際にクラッシュを止めるかどうかは、再現テストが書けない限り保証されません。「nil チェックを追加した」「try で囲った」といった対症療法は、本当の原因を覆い隠すだけで再発します。私は層3のプロンプトに「修正パッチに併せて、その修正の正当性を検証する最小再現テストを書いてください。テストが書けないなら採用不可と明記してください」と必ず書いています。
ふたつ目は、ライブラリ起因の可能性が残っているケースです。サードパーティ SDK の内部でクラッシュしている場合、自分のコードを変えても根本解決にはなりません。むしろ「ライブラリの想定外の使い方」を新たに作り込んでしまう恐れがあります。層1の仮説に「ライブラリ起因の可能性」が入っていたら、その時点で AI 提案の自動採用は止めて、手で SDK のリリースノートと Issue Tracker を読みにいく運用にしています。
みっつ目は、スレッド競合・タイミング依存のケースです。onCreate と onResume の競合や、メモリ警告時のキャッシュ破棄と同時に走る読み取り処理などは、AI が出すパッチでは塞ぎきれないことが多くあります。これは私のアプリ群でも繰り返し経験してきた領域で、母数が大きくなるほど、確率の低い競合も誰かが確実に踏みます。AI に修正案を作らせるよりも、まず計測(Trace や Crashlytics.log)を仕込んで状況証拠を集めるのが先です。
逆に、AI 提案をそのまま採用してよいと割り切れるケースもあります。明確な null 安全違反、配列の境界条件ミス、定数比較の誤り、JSON フィールド名のタイポ、Optional のアンラップ漏れ。これらは原因が局所的で、テストも書きやすく、副作用が少ないです。私は層3のプロンプトで「以下の5パターンに該当する場合のみ自動採用可と明記してください」と提示し、それ以外はすべて人間レビューに回るようにしています。
# 層3 プロンプト末尾に挟むルール
適用可と判定してよいのは、以下のいずれかに該当する場合のみです:
(1) null / Optional のアンラップ漏れに対する明示チェック追加
(2) 配列・コレクションの境界違反に対する事前判定追加
(3) 定数・フィールド名のタイポ修正
(4) 不要な強参照を弱参照に置き換える局所変更
(5) すでに同じパッチが他のファイルに適用された前例がある場合
それ以外は「適用不可(要人間判断)」と明記してください。
このルールを言語化しておくだけで、層3の出力は格段に扱いやすくなります。
個人開発の朝15分ルーチン設計
ここまでの仕組みを前提にすると、私の朝のルーチンは次のように整理できます。
0〜2分: コーヒーを淹れながら端末を開く。make triage の結果が来ているか確認
2〜7分: 層1の hypotheses.json を眺め、「ライブラリ起因」「スレッド競合」が含まれる issue を別キューに退避
7〜12分: 残った層3の proposed_diffs/ を1件ずつ開き、自動採用可ルールに合致するもののみ git apply
12〜15分: 採用したパッチを1コミットにまとめてリリース予約に積む
合計15分。以前の40〜60分から、確実に時間が削れています。さらに副次効果として、午前中いちばんの集中時間にコードを書く生活が戻ってきました。
ここで重要なのは、「朝15分で全部終わるように設計する」ことです。終わらせ切らない・キューに残ったものは午後に回す・特に複雑な issue は2〜3日寝かせる、というルールを決めておかないと、AI が回してくれる分だけ作業量が増え続けてしまいます。Claude Code を導入したのに前より忙しくなっては本末転倒です。デバッグ全般の進め方については Claude Code フックを使った本番デバッグの方法論 でも触れていますので、合わせて参照してみてください。
Crashlytics / Sentry それぞれの統合パターン
私のアプリ群は Crashlytics と Sentry の両方を使っています。Crashlytics は無料で個人開発との相性がよく、Sentry は Source Maps やリリーストラッキングが綺麗にまとまっていて Web 連携プロジェクトで使い分けています。それぞれの統合上の注意点を整理しておきます。
Crashlytics は公式 REST API がないため、Firebase の BigQuery エクスポート機能か、Reports の CSV エクスポートを基点にすると安定します。私は BigQuery 経由で Cloud Run のジョブから日次で Issue 集計を投げ、結果を triage_queue.json に整形しています。BigQuery のクエリは次のような形です。
-- Crashlytics の BigQuery エクスポート用クエリ
SELECT
event_id,
ANY_VALUE( blame_frame . subroutine ) AS subroutine,
ANY_VALUE( blame_frame . file ) AS file ,
COUNT ( DISTINCT user_pseudo_id) AS users,
COUNT ( * ) AS events,
MIN (event_timestamp) AS first_seen,
MAX (event_timestamp) AS last_seen
FROM `your-project.firebase_crashlytics.app_xxxxx`
WHERE event_timestamp > TIMESTAMP_SUB( CURRENT_TIMESTAMP (), INTERVAL 24 HOUR )
GROUP BY event_id
ORDER BY users DESC
LIMIT 10 ;
-- 期待出力: 直近24時間で影響ユーザー数の多い上位10件
Sentry は前述の fetch_top_issues.py がそのまま使えます。Sentry の場合は Issue 単位で permalink が付与されるので、Claude Code に渡す JSON にこの URL を含めておくと、層1の仮説生成で「タグやリリースバージョンの情報を参照したいですか?」と AI が判断しやすくなります。
両者を併用する場合は、triage_queue.json のフォーマットを揃えておくのが鍵です。私のフォーマットは以下のような共通スキーマで、ソースが Crashlytics か Sentry かは source フィールドで切り替えています。
{
"source" : "sentry" ,
"id" : "sentry-issue-12345" ,
"title" : "NullPointerException in HomeViewModel.bindUser" ,
"signature" : "HomeViewModel.bindUser" ,
"users" : 142 ,
"events" : 213 ,
"first_seen" : "2026-05-10T05:14:00Z" ,
"last_seen" : "2026-05-10T23:51:00Z" ,
"permalink" : "https://sentry.io/organizations/dolice/issues/12345/"
}
スキーマが統一されているので、層1〜層3のプロンプトはソースが何であっても同じものを使い回せます。これは小さな工夫ですが、長く運用するうえで大きく効いてきます。
私が運用で守っているルール
仕組みが回り始めたあとも、運用上のルールを言語化して守ることが、結局のところ品質を保つ最後の砦になります。私が今のところ守っているルールは次のようなものです。
リスクのある修正は2日以上寝かせる。AI が確信度高めで出してきても、夜中の自分は判断が甘くなりがち
サンプルサイズが10件未満のクラッシュは別キュー。優先度を下げて週末にまとめて見る
AI に依頼する際のコミット粒度は「1 issue = 1 コミット」を厳守。まとめると後で回帰したときに切り戻せない
リリースノートには修正した issue ID を必ず書く。ユーザーが「自分が踏んだ症状が直っているか」を確認できるように
修正内容を Notion か Obsidian に1行残す。同じ症状が3回目に出たら、その時点でアーキテクチャを疑う
これらは派手な工夫ではありませんが、長年の個人開発で繰り返し失敗してきた経験から、自分の中に少しずつ積み上げてきた習慣です。AI に処理を任せるほどに、こうした「人間側の規律」が逆に重要になってきます。Claude Code は判断を肩代わりしてくれません。判断の素材を高速に揃えてくれるだけで、最後の一手は今でも自分で打つ必要があります。本番運用の現場で AI に判断補助をさせる設計の考え方は Claude AI を本番運用のデバッグ相棒にする実践設計 でも書いていますので、設計思想の参考にどうぞ。
下ごしらえと仕上げは、分けて考えたいと思っています。AI が削りやすい形に整えるところまでは仕組みで担保し、最後に手を下すのは自分でありたい。この感覚は、リリース後の障害対応を一人で回し続けてきた時間の中で、少しずつ固まってきたものです。
次のアクション
ここまで読んでくださってありがとうございます。仕組みの全体像をいきなり実装する必要はありません。まずは、今お使いの Crashlytics か Sentry から、過去1週間で影響ユーザー数の多いクラッシュ Issue を1件だけ JSON で書き出してみてください。そのファイルを Claude Code に渡し、本記事の層1プロンプトをそのまま投げる。返ってきた仮説3つを眺めるところから始めるのが、もっとも軽くて壊れにくい第一歩です。
私自身、最初から自動化されたパイプラインを組んだわけではなく、毎朝のルーチンを少しずつ AI に置き換える過程で、3層の構成にたどり着きました。みなさまの個人開発の朝が、少しでも軽くなることを願っています。