壁紙アプリの一つを更新してストアに出した数日後、Firebase Crashlytics のダッシュボードに見覚えのない行が並びました。クラッシュは記録されているのに、スタックトレースが 0x0000000102f4c8a8 のようなアドレスのまま。どの関数で落ちたのか、何行目なのかが一切読めない状態です。上部には黄色い「Missing dSYM」の警告。心当たりはありました。その少し前に、Firebase を CocoaPods から Swift Package Manager へ移していたのです。
アドレスの羅列を前にして最初に思い出したのは、宮大工だった二人の祖父のことでした。仕上げを省いた木組みは外からは分からなくても、後で必ず歪みとして出てくる。クラッシュレポートも同じで、記号化という地味な仕上げを飛ばすと、いざユーザーが困っているときに肝心の中身が読めません。2014年から個人開発を続けてきて、この種の「動いてはいるが仕上がっていない」状態が一番こわいと感じています。今回はその記号化を、Claude Code に手伝ってもらいながら確実に通るところまで組み直しました。
そもそも dSYM が何をしているか
dSYM(debug symbols)は、リリースビルドで取り除かれた関数名・ファイル名・行番号と、実行バイナリのメモリアドレスを対応づける辞書のようなファイルです。Crashlytics はクラッシュ時のアドレスを集めて、後からこの辞書を引いて「WallpaperViewModel.swift:142 の applyFilter(_:)」のような人間が読める形に戻します。これが記号化(symbolication)です。
辞書とバイナリの対応づけは UUID で行われます。つまり、ストアに出したバイナリと完全に同じビルドで生成された dSYM が Crashlytics に届いていないと、UUID が噛み合わず「Missing dSYM」になります。逆に言えば、原因は「dSYM が作られていない」か「作られたが届いていない」のどちらかに必ず絞れます。この切り分けを最初にやることが、遠回りを避ける近道でした。
Claude Code で原因を二択に絞る
私はターミナルから Claude Code を起動して、まず警告に出ている UUID をそのまま渡しました。Crashlytics は「この UUID の dSYM が足りません」と教えてくれるので、その UUID がローカルに存在するかを調べてもらいます。macOS には UUID で dSYM を逆引きする仕組みがあります。
# Crashlytics が要求している UUID をローカルから探す
mdfind "com_apple_xcode_dsym_uuids == E1B2C3D4-5678-90AB-CDEF-1234567890AB"
# アーカイブ内の dSYM の UUID を直接確認する
dwarfdump --uuid ~/Library/Developer/Xcode/Archives/2026-06-01/MyApp.xcarchive/dSYMs/*.dSYMここで分かったのは、dSYM 自体はアーカイブの中にきちんと生成されていたということでした。つまり「作られていない」側ではなく「届いていない」側の問題です。原因は二つに一つだと分かった時点で、調査範囲が一気に狭まりました。Claude Code に「アーカイブには UUID 一致の dSYM がある。生成はされているがアップロードされていない。SPM 移行で run script のパスが変わった可能性を疑いたい」と伝えると、まさにそこが SPM 移行で壊れやすい箇所だという前提で次の確認に進めました。
SPM 移行で run script のパスが変わっていた
CocoaPods 時代の Crashlytics は、Pods/FirebaseCrashlytics/run を Build Phase のスクリプトから呼んでいました。SPM に移すと Pods ディレクトリ自体が無くなるので、この古いパスを指したままだとスクリプトが何もせず終わります。エラーにならず静かに失敗するのが厄介で、ビルドは緑のまま通ってしまいます。
正しい SPM 版のパスは、ビルドディレクトリ配下にチェックアウトされた firebase-ios-sdk を指します。Build Phases に新しい Run Script を追加し、入力ファイルも明示しました。
# Build Phases > Run Script(SPM 版 Crashlytics)
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"入力ファイル欄(Input Files)にも、dSYM と Info.plist の場所を渡します。ここを空にしておくと、新しいビルドシステムでスクリプトが走らないことがあります。
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)
あわせて、Release 構成の Build Settings で Debug Information Format が DWARF with dSYM File になっていることを確認しました。ここが DWARF だけだと dSYM がそもそも生成されません。私のプロジェクトは生成されていたので該当しませんでしたが、もう一つの壁紙アプリ側では Release だけ設定が外れていて、こちらは「作られていない」側の典型でした。同じ症状でも原因が逆だったわけです。
取りこぼした分を手動で送る
run script を直しても、すでにストアに出てしまったビルド分の dSYM は自動では送られません。これは手で送る必要があります。SPM 版の upload-symbols を直接叩きます。
# SPM のチェックアウト先にある upload-symbols を使う
UPLOAD="$(find ~/Library/Developer/Xcode/DerivedData -name upload-symbols -path '*firebase-ios-sdk*' | head -1)"
"$UPLOAD" -gsp ./MyApp/GoogleService-Info.plist -p ios \
~/Library/Developer/Xcode/Archives/2026-06-01/MyApp.xcarchive/dSYMsこのコマンドを Claude Code に組み立ててもらう際、私がよくやるのは「複数アプリ分のアーカイブをまとめて処理する小さなループにして」と頼むことです。私は壁紙・癒し系を中心に複数アプリを並行で運用しているので、一本ずつ送ると確実に取りこぼします。アーカイブ一覧を走査して、未送信のものだけ送る形にしておくと、月次の更新作業に組み込みやすくなりました。
# 直近のアーカイブを順に処理する(アプリごとに plist を変える)
for ARCHIVE in ~/Library/Developer/Xcode/Archives/2026-*/*.xcarchive; do
echo "▶ $(basename "$ARCHIVE")"
"$UPLOAD" -gsp ./MyApp/GoogleService-Info.plist -p ios "$ARCHIVE/dSYMs" || echo " skip"
done送った後、Crashlytics のダッシュボードに反映されるまでには少し時間がかかります。すぐに記号化されないからといって何度も送り直さず、数十分から数時間は待つのが正解でした。私は一度ここで焦って多重送信し、ログが二重になって余計に確認しづらくしてしまったので、これは自分への戒めとして残しておきます。
クラッシュを読み解くところまで Claude Code に任せる
記号化が通れば、スタックトレースは関数名と行番号で読めるようになります。ここから先こそ Claude Code が効く場面です。私は記号化済みのスタックトレースをそのまま貼り付けて、「このクラッシュが起きる条件と、該当しそうな箇所を挙げてほしい」と頼みます。アドレスのままでは AI に渡しても推測の域を出ませんが、関数名と行番号が乗っていれば、該当ファイルを実際に読みに行って原因の仮説を絞り込んでくれます。
今回のクラッシュは、画像フィルタを適用する処理がメインスレッド外から UI を触っていたことが原因でした。記号化されたトレースに applyFilter(_:) と出ていたおかげで、Claude Code は該当の ViewModel を開いて「ここは @MainActor の保証が切れている」と指摘してくれました。アドレスのままだったら、おそらく数時間は見当違いの場所を眺めていたと思います。
次に同じ警告を見たらやること
もし今後また「Missing dSYM」が出たら、私は次の順で動きます。まず警告の UUID を mdfind と dwarfdump でローカルに照合し、「作られていない」のか「届いていない」のかを最初に確定させます。前者なら Release の Debug Information Format を、後者なら run script のパスと Input Files を疑う。手動アップロードは一度だけ流して、あとは反映を待つ。この順番を決めておくだけで、同じ落とし穴に二度はまらずに済みます。
地味な仕上げほど、後から効いてきます。クラッシュレポートが読める状態を保っておくことは、ユーザーが黙って離れていく前に手を打てるかどうかに直結します。同じように複数アプリを抱えて運用している方の、月次更新のチェック項目に一行加わるきっかけになれば嬉しいです。