なぜ個人開発の可観測性は「ツール乱立」で破綻するのか
ある朝、運営しているアプリの AdMob 収益が前日比 32% 落ちていることに気づきました。原因を辿る作業は、Crashlytics、Sentry、Cloudflare Logs、Stripe ダッシュボード、AdMob レポート、GA4 を順番に開いて目視で照合する、というものでした。2 時間かけて分かった原因は、前日のリリースで広告ユニット ID を 1 文字タイプミスしていたという、本来 5 分で検出できるはずの事象でした。
個人開発でアプリを長く運営していると、可観測性の設計は一度で完成するものではなく、障害のたびに組み直すものだと痛感します。Rork Max で生成コードを使う比率が上がってからは、「速く作れる」反面、「壊れた時に追えない」リスクが確実に増えました。コードベースを完全に頭に入れている状態と、AI が補完した部分が混在している状態とでは、ログを読む難易度がまるで違うからです。
個人開発者の可観測性は、SRE 専任がいる組織のそれとは設計思想を変える必要があります。「人が常駐していない前提」で、夜中に鳴ったアラートがどのタイミングでどう自分に届くか、届いた後に何分以内にどこを開けば原因にたどり着けるか、という観測経路の設計が、ツール選定よりも先に来るべきです。
オブザーバビリティの 3 本柱を「誰が見るか」で分ける
教科書通りに言えば、オブザーバビリティは Logs / Metrics / Traces の 3 本柱で成り立ちます。私の場合は、これを「誰が・いつ・どんな粒度で見るか」という運用視点で再定義しています。
Logs: 障害発生時に原因特定のために遡って読むもの。自分が起こされた後に開く
Metrics: 売上・継続率・クラッシュフリー率など、毎朝 5 分の習慣で目視する指標
Traces: 上記 2 つで原因が絞れた後、ピンポイントで開くもの。常時は見ない
3 つに同じだけのコストと注意を払うと、個人開発者は持ちません。Metrics は AdMob・Stripe・Crashlytics のダッシュボードを 1 画面に集約し、Logs は「鳴った時だけ開く」前提で構造化し、Traces は障害再現のためだけにスポットで使う、というメリハリが必要です。
ツール選定の判断軸
私が採用している判断軸は次の 3 つです。
モバイル前面はベンダーが用意した SDK を使う : Crashlytics・Sentry のように、OS 側のシグナル(メモリ警告・ANR・watchdog kill)を捕捉できるものを優先する
バックエンドは生ログを自分で保管する : Cloudflare Workers の場合は Logpush で R2 に流し、外部 SaaS に依存しすぎない
アラート経路は 1 つに絞る : Sentry・Crashlytics・Stripe Webhook の通知を Slack の 1 チャンネルに集約し、優先度別に色分けする
「とりあえず全部入れる」は個人開発では必ず破綻します。3 年運用すると、契約しているがログインしなくなった監視サービスが必ず出てきます。
障害シナリオ別: ログを追う経路を 1 分以内に決める
可観測性スタックの設計は、ツール選定で終わるものではなく、「障害発生時の確認順序」を事前に決めることが本質です。私が運用している 3 つのシナリオを共有します。
シナリオ A: AdMob 収益が前日比 30% 以上落ちた
AdMob レポート → 単価か表示回数のどちらが落ちたかを確認(1 分以内)
表示回数が落ちている場合: Crashlytics のクラッシュフリー率を直近 24 時間で確認
単価が落ちている場合: AdMob のメディエーション結果を確認し、特定ネットワークがフィル率を下げていないか見る
どちらでもない場合: 当日 / 前日リリースの diff を git log で確認
このフローを文書化しておかないと、夜中に起こされた状態で正しい順序を思い出せません。私は Notion に「障害トリアージ手順」というページを作り、ロック画面のショートカットから 2 タップで開けるようにしています。
シナリオ B: アプリ起動直後にクラッシュ多発
Crashlytics のリアルタイム表示で対象 OS バージョンを特定(30 秒)
Sentry でアプリケーション例外のスタックトレースを照合
App Store Connect で当該バージョンの全段階リリースを停止
Cloudflare Workers Logs で API 5xx が増えていないか確認
クラッシュの 80% は OS バージョンか機種に集中するので、まず Crashlytics でフィルタリングするのが最短経路です。Sentry を先に見るのは、ネイティブクラッシュではない例外(JS の Hermes 例外など)の場合だけです。
シナリオ C: サブスク更新失敗の問い合わせが急増
RevenueCat ダッシュボードで Renewal Failure Rate を確認
Stripe ログで対象期間の invoice.payment_failed Webhook を一覧
Cloudflare Workers の Stripe Webhook ハンドラログを Logpush から検索
Apple サーバ通知 v2 のシグネチャ検証エラーがないか確認
サブスク系は「個人開発で最も売上に直結し、最もデバッグが面倒な領域」です。RevenueCat を間に挟むことで Apple・Stripe・Google の 3 系統の通知を 1 つの API に正規化できるため、私は必ずこれを採用しています。
Sentry と Crashlytics を二重化させない境界線
「両方入れているが境界が曖昧」というのが個人開発で最も多いアンチパターンです。私は次のルールで完全に分けています。
Crashlytics に送るもの: ネイティブクラッシュ、ANR、watchdog kill、メモリ警告由来の強制終了
Sentry に送るもの: アプリケーションコードで try/catch した例外、Workers / Cloudflare Pages の例外、フロントエンドの未捕捉エラー
両者の境界をコードで強制するため、Rork Max 生成コードのエラーハンドラに次のフラグを挿入しています。
// errorReporter.ts
type ErrorTier = "native_crash" | "app_exception" | "warning" ;
interface ReportOptions {
tier : ErrorTier ;
context ?: Record < string , unknown >;
user ?: { id : string ; tier : "free" | "premium" };
}
export function reportError ( error : Error , options : ReportOptions ) {
// tier ごとに送信先を強制的に分ける。両方に投げない。
switch (options.tier) {
case "native_crash" :
// ネイティブブリッジ経由で Crashlytics のみ
NativeModules.CrashReporter. recordError (error, options.context);
break ;
case "app_exception" :
// Sentry のみ。Crashlytics には流さない
Sentry. captureException (error, {
contexts: { app: options.context },
user: options.user,
tags: { tier: options.user?.tier ?? "anonymous" },
});
break ;
case "warning" :
// 重複アラート防止のため Sentry に Breadcrumb として残すだけ
Sentry. addBreadcrumb ({
category: "warning" ,
message: error.message,
level: "warning" ,
data: options.context,
});
break ;
}
}
この設計の本番運用で得られた効果は、月間アラート数が約 4 倍から 1.2 倍に減ったことです。重複が消えると「鳴ったアラートに全部反応する」感覚が戻ってきます。これは数値以上に運用負荷の改善が大きい変化でした。
Cloudflare Workers Logs と Logpush で「自分で保管する」設計
Cloudflare Workers の console.log は、無料プランでは直近のリアルタイムログしか見られません。3 年運用していると「2 ヶ月前のあの障害ログを見たい」という瞬間が必ず来るため、私は Logpush + R2 で全ログを保管しています。
# wrangler.toml
[[ logpush ]]
destination = "r2://logs-prod/workers/{DATE}.ndjson.gz"
dataset = "workers_trace_events"
filter = '{"where":{"and":[{"key":"Outcome","operator":"eq","value":"ok"}]}}'
[[ logpush ]]
destination = "r2://logs-prod/workers-errors/{DATE}.ndjson.gz"
dataset = "workers_trace_events"
filter = '{"where":{"and":[{"key":"Outcome","operator":"ne","value":"ok"}]}}'
エラーを別ファイルに分けておくと、障害時に grep する対象が 1/100 以下になります。本番運用の現場で「ログを開いた瞬間に欲しい行が見つかる」状態を作るのが、可観測性設計の到達点です。
推奨保管期間と費用感
私の場合は、エラーログ 90 日 / 通常ログ 30 日で保管しています。R2 の保管料は 1 GB あたり月 $0.015 で、月間ログサイズが圧縮後 5 GB の規模だと月 $0.075、年間で 1 ドル未満です。Sentry 単体で同等のことをやろうとすると最低でも月 $26 のプランが必要で、ログ量制限にも引っかかります。「自分で保管する」選択肢が常に最安というわけではありませんが、Workers の場合は R2 との親和性が高いため明らかに優位です。
Claude in Chrome × Crashlytics: 障害トリアージの自動化
ここからは現在進行形の実体験を共有します。私は最近、Claude in Chrome に Crashlytics ダッシュボードを開かせ、上位 5 件のスタックトレースを要約させて Slack に流す自動化を試しています。
毎朝のクラッシュ確認は、開発者でも 5〜10 分かかります。これを 30 秒に短縮できれば、年間で 30 時間以上を別の作業に回せる計算になります。実装は単純で、Chrome 拡張側で次のような指示プロンプトを保存しています。
Crashlytics の Issues タブを開き、過去 24 時間で発生回数の多い上位 5 件について、
スタックトレースの最上位フレームと推定原因を 1 行ずつ要約してください。
出力形式は「[発生回数] スレッド名: 原因の推定」とし、5 件を改行区切りで返してください。
重複している場合は、同じ原因の Issue として 1 行にまとめてください。
ここで重要なのは「読み上げではなく要約をさせる」ことです。Crashlytics の生のスタックトレースをそのまま Slack に流すと、ノイズが多すぎて見落とします。Claude に「最上位フレームと推定原因」という抽象度で要約させることで、人間の判断負荷を下げています。実装上のハマりどころとして、DOM 構造が Crashlytics のアップデートで変わると一気に動かなくなるため、要約の信頼度を「過去 7 日間のデータと比較した時の整合性」で自動検証する仕組みも併設しています。
コストではなく「収益保護センター」として監視を捉える
個人開発者の観測スタックは「コストセンター」と見られがちですが、私はこれを「収益保護センター」として位置付けています。具体的な数字で考えると分かりやすいです。
AdMob eCPM が 5% 落ちて 1 日気づかなかった場合、月商 150 万円規模のアプリでは 1 日あたり約 2,500 円、累計で月 7.5 万円の損失
1 日早く検知できれば、年間で 90 万円以上の収益保護効果
監視コスト合計: Sentry Team プラン月 $26、Crashlytics 無料、Cloudflare Logpush 約月 $5、合計年 $400 程度
つまり「年間 6 万円程度の投資で 90 万円の収益を守る」という ROI で考えるべきものです。eCPM や ARPU を毎日見る習慣がない人は、まず Metrics の 5 分朝確認から始めることを推奨します。これだけで月 1 回くらいは異常を早期発見できるはずです。
3 年運用して学んだアラート疲労との戦い方
最後に、長期運用で最も重要な「アラート疲労との戦い方」について書きます。個人開発を続ける中で、私は通知の量より精度を優先する設計を一貫して意識してきました。アラートが多すぎると、人間は必ず慣れて無視するようになります。これは個人開発でも組織でも同じです。
私の運用する 3 段階通知は次の通りです。
P0 (即起こす) : アプリ起動率が直近 1 時間で 50% 以下、または Crashlytics クラッシュフリー率が 95% 以下。Slack + iPhone の Critical Alert で深夜でも鳴らす
P1 (翌朝確認) : 売上前日比 -20% 以上、Sentry の新規 Issue が 24 時間で 5 件以上、Webhook 失敗率 1% 以上。Slack 通常チャンネル
P2 (週次レビュー) : 単一エンドポイントのレイテンシ p95 増加、リテンション Day 7 低下傾向、特定機種のクラッシュ率上昇。週次のメール要約
「全部 P0 にする」のは個人開発の典型的な失敗です。3 ヶ月で必ず慢性疲労になり、本当に重要なアラートを見落とします。私の場合、P0 が鳴るのは月 1 回以下、P1 が週 2〜3 回、P2 はメールで毎週金曜に届く、というバランスに落ち着きました。
ここで一つ、実装上の落とし穴を共有します。P0 で使う iPhone の Critical Alert(サイレントスイッチや集中モードを貫通して鳴る通知)は、Apple から専用の entitlement 承認を得ないと本番では使えません。申請時には「なぜ通常のプッシュ通知では不十分なのか」を運用文脈で説明する必要があり、私は「無人で運用しているため、収益停止級の障害だけは物理的に自分を起こす必要がある」という理由で承認をいただきました。ここを知らずに Critical Alert 前提で通知経路を設計してしまうと、いざという時に鳴らずに気づけない、という最悪の抜けが生まれます。深夜のアラートは「鳴るはず」ではなく「鳴ることを一度は実機で確認した」状態にしておくことを強くおすすめします。
次にやること
今週中にやれる 1 ステップは、Sentry と Crashlytics の両方に同じ例外が記録されていないかを確認することです。私の経験では、ここに重複が 1 件以上見つからない個人開発者のスタックは、ほぼ存在しません。重複を解消するだけで、アラート数は確実に減ります。
お読みいただきありがとうございました。3 年運用の中で最も「やっておいてよかった」と感じる設計を共有しましたので、同じく個人で複数アプリを運営される方の参考になれば嬉しいです。