symbolize-android-stacktrace について
Flutter の Android アプリで、Google Play Console のクラッシュや ANR の難読化されたスタックトレースを、対応する Codemagic のビルド成果物でシンボル化するスキルです。ネイティブと Flutter のシンボルをアップロードするリリース工程向けで、R8 マッピングによる Java フレームの復号にも対応します。
主な機能
- 01ネイティブと Flutter のフレームをシンボル化
- 02Codemagic からシンボル成果物を取得
- 03R8 と ProGuard のマッピングを解決
- 04未解決フレーム数を報告
活用シーン
- 01Google Play のクラッシュトレースを調査
- 02Flutter の ANR フレームをソース行に特定
- 03mapping.txt で Java フレームを復号
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ chunkytofustudios/ flutter-skills --skill symbolize-android-stacktraceインストール後、Claude Code で/symbolize-android-stacktraceを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
scripts/codemagic_fetch_artifacts.pydeobfuscate_r8.pysymbolize_flutter_anr.shtests/classification.batshelp.batshelpers.bashmetachars.batssymbolize.batstest_codemagic_fetch.pytest_deobfuscate_r8.pytests/fixtures/mapping.txtnot-a-trace.txttests/fixtures/flutter/app.android-arm64.symbolstests/stubs/ndk/toolchains/llvm/prebuilt/bats-host/bin/llvm-addr2linellvm-readelf詳細
Android のスタックトレースをシンボル化(Flutter + Codemagic)
難読化された Google Play Console のクラッシュ/ANR スタックトレースを、シンボル化されたトレース(すべてのネイティブフレームと Flutter フレームのファイル名と行番号)に変換し、バグの原因を調査できるようにします。
使用する場面
次の3つすべてに当てはまる場合に使用してください。1つでも当てはまらない場合、このツールは適していません。
- アプリが Android を対象とした Flutter アプリである。
- トレースが Google Play Console(クラッシュダッシュボードまたは ANR ダッシュボード)からエクスポートされたものである。通常は
pc 0x…形式のフレームを含む.txt/.logファイルで、# Application:/# Version:ヘッダーが含まれる場合もあります。 - リリースビルドが Codemagic CI/CD で作成され、ワークフローによって
android_native_debug_symbols.zipと<AppName>_<N>_artifacts.zip(Flutter の split-debug-info.symbolsファイル)の両方がビルド成果物としてアップロードされている。これらがないと、難読化された PC に対応するものがありません。mapping.txt(R8/ProGuard)も存在する場合は取得します。これにより、シンボル化ツールはKa.n.l+8のような Java フレームをio.flutter.embedding.engine.FlutterJNI.onSurfaceDestroyedに戻せるため、Java 側が読みやすくなり、難読化された名前ばかりの状態を避けられます。
Codemagic でビルドされたか不明な場合は、リポジトリのルートにある codemagic.yaml を確認するか、ユーザーに尋ねてください。
セットアップ(呼び出し元のマシン)
| 要件 | 用意する方法 |
|---|---|
| Codemagic API キー | Codemagic → User settings → Integrations → Personal API token で生成します。次のいずれかの方法で指定してください。(a)CODEMAGIC_API_KEY 環境変数、または(b)リポジトリのルートに、キーをプレーンテキストで記載した .codemagic-api-key ファイル。スクリプトは CWD から上位ディレクトリをたどり、.git の境界で停止します。プライベートリポジトリに共有キーをコミットしたいチームには、このファイルが便利です。その場合は、リスクを理解したうえで使用してください。 |
llvm-addr2line を含む Android NDK | デフォルトの検索先は ~/Library/Android/sdk/ndk/27.3.13750724 です。ANDROID_NDK_HOME=/path/to/ndk で上書きできます。最近の NDK には llvm-addr2line と llvm-readelf が含まれています。 |
unzip、python3(3.10 以降)、bash | 標準的なものです。Python スクリプトは標準ライブラリのみを使用します。 |
環境変数もファイルもない場合は、推測せずに 停止してユーザーに尋ねてください。Codemagic の呼び出しはすべて 401 になります。
ほかにセットアップはありません。特に、gh は 不要です。手順3 を参照してください。
同梱スクリプト
scripts/codemagic_fetch_artifacts.py— 3つのモードで検出とダウンロードを行うツールです。JSON は stdout に、進行状況は stderr に出力します。ビルドで生成されている場合はmapping.txtも取得します。scripts/symbolize_flutter_anr.sh— 順序を問わず、トレースとシンボルをまとめるスクリプトです。どの入力がどれか(トレース、ネイティブ zip、Flutter zip、mapping.txt)を判別します。scripts/deobfuscate_r8.py— R8/ProGuard のマッピングを一括で解決するツールです。bash シンボライザーからトレースごとに1回呼び出されます。通常、直接呼び出すことはありません。
3つとも --help(または先頭の docstring)を備えています。フラグを独自に指定する前に、内容を確認してください。
ワークフロー
1. トレースをファイルに保存する
ユーザーがファイルを添付せず、テキストを貼り付けた場合は、/tmp/play-stacktrace.log に書き込んでください。bash スクリプトは ABI の検出とフレームの抽出のためにファイルを2回 grep するため、実際のファイルパスが必要です。
2. トレースからアプリとバージョンを特定する
トレースには通常、/data/app/.../<applicationId>-…/split_config.<abi>.apk にパッケージ名が含まれています。
/data/app/~~xxxx==/com.example.foo-yyyy==/split_config.arm64_v8a.apk
^^^^^^^^^^^^^^^
バージョンは、次の優先順位で特定してください。
- トレース内の
# Version:ヘッダー行(Crashlytics 形式のエクスポート)。 - トレースを共有したときにユーザーが伝えたバージョン。
- それでも不明な場合は、停止してユーザーに尋ねてください。 ユーザーは Google Play Console からトレースをダウンロードしているため、送信したトレースの横にあるクラッシュ詳細ページで
versionNameを直接確認できます。Codemagic の最新ビルドから 推測してはいけません。Symbol Build ID が解決できるのは、クラッシュしたビルドとバージョンが完全に一致する場合だけです。一致しないと、すべてのアプリフレームで[UNRESOLVED: no symbol file for BuildId …]が表示されます。
3. Codemagic からシンボルを取得する
3 ステップで検出します(不要なステップはスキップできます)。
# (a) List visible apps. Read the JSON to pick the right `appName`,
# then use that exact string for --app in step (b).
python3 scripts/codemagic_fetch_artifacts.py
# (b) List finished Android builds for the app, to confirm the version exists.
python3 scripts/codemagic_fetch_artifacts.py --app "Pixel Buddy"
# (c) Download both symbol zips for that version. Most-recent build wins on ties.
python3 scripts/codemagic_fetch_artifacts.py --app "Pixel Buddy" --build 1.2.3
--app には Codemagic の表示名を指定します(appName と大文字・小文字を区別しない完全一致で照合します)。これは標準のセレクターで、どのマシンでも機能します。Android の applicationId(例:com.chunkytofustudios.pixel_buddy)を渡したい場合は、gh CLI をインストールして認証してください(gh auth login)。その後、スクリプトがリポジトリの android/app/build.gradle{,.kts} から applicationId を解決します。このスキルだけのために gh をインストールしないでください。まずアプリを一覧表示し、名前で照合してください。
ダウンロードしたファイルは ~/.cache/codemagic-fetch-artifacts/codemagic/<appId>/<buildId>/ にキャッシュされるため、同じビルドで再実行すると即座に完了します。
ステップ(c)の標準出力は JSON です。.cacheDir と .files[].path を解析して、次のステップに渡してください。例:
{
"cacheDir": "/Users/you/.cache/codemagic-fetch-artifacts/codemagic/<appId>/<buildId>",
"files": [
{ "name": "android_native_debug_symbols.zip", "path": "…/android_native_debug_symbols.zip", "size": 123456789, "cached": true },
{ "name": "PixelBuddy_42_artifacts.zip", "path": "…/PixelBuddy_42_artifacts.zip", "size": 12345678, "cached": false },
{ "name": "mapping.txt", "path": "…/mapping.txt", "size": 6543210, "cached": false }
]
}
mapping.txt は任意で、ビルドで R8/ProGuard が有効になっている場合にのみ存在します。難読化を無効にしたアプリにはエントリーがありませんが、問題ありません。ステップ 4 では mapping.txt 引数を省略してください。
アプリまたはバージョンが見つからない場合、スクリプトは利用可能な一覧を表示して、ゼロ以外の終了コードで終了します。繰り返し実行するのではなく、その内容をユーザーに提示してください。
4. シンボル化する
bash scripts/symbolize_flutter_anr.sh \
/tmp/play-stacktrace.log \
~/.cache/codemagic-fetch-artifacts/codemagic/<appId>/<buildId>/android_native_debug_symbols.zip \
~/.cache/codemagic-fetch-artifacts/codemagic/<appId>/<buildId>/<AppName>_<N>_artifacts.zip \
~/.cache/codemagic-fetch-artifacts/codemagic/<appId>/<buildId>/mapping.txt # optional
引数の順序は問いません。スクリプトが各入力を判別し、トレース、ネイティブ zip、Flutter シンボル zip、または R8 の mapping.txt に分類します。出力先は、入力ファイルと同じ場所にある <trace>.symbolized.txt がデフォルトです。-o で変更できます。機械で読み取り可能な概要を標準出力に表示するには、--json を使用します。
mapping.txt を指定すると、ユーザーアプリの Java フレーム(<userPackage>/base.apk、<userPackage>/oat/.../base.odex、または /memfd:jit-cache のようなパス)に、難読化解除された <class>.<method> を含む [JAVA: …] 注釈が付きます。クラスがマッピングに含まれていないフレーム(R8 が難読化しなかった Flutter SDK のコードや、古いマッピングでよく見られます)には、[JAVA: not in mapping.txt] が付きます。mapping.txt を省略した場合、同じフレームには [JAVA: pass mapping.txt to deobfuscate] が付き、次に何をすればよいかが明確になります。
5. シンボル化された出力を読み、デバッグする
出力には、元の行と解決済みフレームが交互に表示されます。
#07 pc 0x00000000004bfb78 /data/app/.../split_config.arm64_v8a.apk (flutter::SurfaceDestroyed(_JNIEnv*, _jobject*, long)+…) (BuildId: d73e2148…)
-> /Users/.../libflutter.so
flutter::SurfaceDestroyed(_JNIEnv*, _jobject*, long)
shell/platform/android/platform_view_android_jni_impl.cc:1234
スクリプトで解決できなかったフレームには [UNRESOLVED] が付きます。末尾の概要には、Frames と Unresolved の件数が表示されます。
信頼モデル
このスキルはエージェントのプロセス外からファイルを取り込むため、「デフォルトで信頼しない」という基準で取り扱います。
- Codemagic のアーティファクトは、任意の第三者ではなく、ユーザー自身が認証した CI ビルド(個人用 API トークンで解決)から取得します。
codemagic_fetch_artifacts.pyは小規模な許可リスト(API 用のapi.codemagic.ioと、Codemagic の署名付き URL CDN 用のstorage.googleapis.com/codemagic-build-artifacts/)に TLS 経由で接続先を限定し、リダイレクトのたびに再検証します。また、ホストをまたぐリクエストではx-auth-tokenヘッダーを削除するため、Codemagic トークンが CDN に送信されることはありません。 mapping.txtはdeobfuscate_r8.pyが 1 行ずつ解析します。内容がeval/execされることも、インポートされることも、ファイルシステムのパスの生成に使われることもありません。ファイルサイズと 1 行ごとの上限により、過大なファイルでパーサーがメモリ不足になるのを防ぎます。symbolize_flutter_anr.shが処理するトレースとmapping.txtのフィールド(バイナリのパス、括弧で囲まれたメソッド名)は、パターン照合(case)に使うか、リテラルのフィールド値としてawk -vに渡すだけです。シェルコマンドやeval、ファイルシステムのパスに埋め込まれることはありません。
既知の注意点
- ビルド ID が照合の基準です。 シンボルファイルがスタックフレームと一致するのは、そのファイルの
BuildId:(llvm-readelf --notesから取得)がトレース行のBuildId:と等しい場合だけです。バージョンが違うと、一致するものがなくなり、すべてのフレームが[UNRESOLVED]になります。ほとんどのフレームが解決できない場合、スクリプトは最初に「wrong build artifacts (common)」と警告します。 - バージョン文字列は、Codemagic ビルドの
versionフィールドと完全に一致する必要があります。 Codemagic には、たとえば"2.3.1"のように保存されます。vプレフィックスは許容されますが、ビルド番号を付加した値(2.3.1+42)は使えません。不明な場合は、codemagic_fetch_artifacts.py --app …(--buildなし)を実行して、利用可能なバージョンを確認してください。 - ABI は、トレース内の
arm64、armeabi、x86_64、x86を grep して検出します。 99%のケースではarm64-v8aです。トレースに ABI のヒントが本当にない場合、スクリプトはデフォルトでarm64-v8aを使用します。 - Flutter シンボル ZIP の名前は
<AppName>_<N>_artifacts.zipです。<N>は Codemagic のビルド連番であり、バージョンではありません。取得スクリプトは正規表現.+_\d+_artifacts\.zipでこのファイルを検出します。Codemagic ワークフローでアーティファクト名を変更した場合は、scripts/codemagic_fetch_artifacts.pyのFLUTTER_ARTIFACTS_REを更新してください。 - システムライブラリのフレームは意図的に未解決のままになります。
libc.so、libart.so、com.google.android.gms、com.google.android.webview、/data/misc/apexdata/com.android.art/.../boot.oat(Android Runtime のプリコンパイル済みブートイメージ)、system_serverなどのフレームは、ビルドに由来するものではありません。これらの PC は Android のシステムバイナリに一致しますが、Codemagic のアーティファクトには含まれません。スクリプトはこれらに[SYSTEM: …]のタグを付けます。追跡せず、自分のパッケージのsplit_config.<abi>.apkを指すフレームに注目してください。 - R8 で難読化された Java フレームには
mapping.txtが必要です。<userPackage>/base.apkのフレームにあるKa.n.l+8のような名前は、自分のコードが難読化されているだけです。Codemagic はmapping.txtをシンボル ZIP とともにアップロードし、4 番目の引数として渡すとシンボライザーが自動的に適用します。これを渡さない場合、Java フレームは[SYSTEM]に黙ってまとめられるのではなく、[JAVA: pass mapping.txt to deobfuscate]というヒントが付きます。<userPackage>/oat/.../base.odexおよび/memfd:jit-cacheにあるフレーム(AOT および JIT コンパイルされたユーザー Java)も同様に処理されます。一方、boot.oatとframework.jarは正しくシステムとして分類されます。 - Flutter エンジンのフレーム(
libflutter.so)は、シンボル情報が削除された状態で提供されます。android_native_debug_symbols.zipに含まれるlibflutter.soにはビルド ID がありますが、DWARF デバッグ情報がないため、正しいビルド ID が一致してもソース位置情報は解決できません。スクリプトはこれらに[UNRESOLVED: BuildId … matched but PC has no debug info (Flutter engine binaries ship stripped)]のタグを付けます。エンジンのフレームについては、Play Console のトレースに含まれる(flutter::Foo()+offset)というテキストが、得られる情報として最善です。このスキルの付加価値は、app.android-<abi>.symbolsファイルで正しく解決できるlibapp.so(Dart AOT)のフレームにあります。 - Codemagic 以外でビルドした場合、ステップ 3 で失敗します。 クラッシュしたバージョンで Codemagic ワークフローが実行されていない場合(たとえば、ローカルで
flutter build appbundleを実行してアップロードした場合)、API から取得できるものはありません。そこで処理を中止し、ユーザーに伝えてください。 symbolize_flutter_anr.shの--jsonフラグは、すべてのステータスログを stderr に出力し、{"frames", "resolved", "unresolved", "unresolved_nomatch", "unresolved_stripped", "java_deobfuscated", "java_not_deobfuscated", "system", "abi", "output"}を stdout に出力します。シンボライザーを別のスクリプトに連結する場合に使用してください。unresolvedはunresolved_nomatch(ビルド ID がアーカイブ内にない → ビルドが違う)とunresolved_stripped(ビルド ID は一致したがバイナリにデバッグ情報がない → 通常はlibflutter.so)の合計です。java_deobfuscatedとjava_not_deobfuscatedは、ユーザーアプリの Java フレームを網羅します。後者は、ファイルが渡されたかどうかによって、「mapping.txtが指定されていない」と「クラスがmapping.txtに含まれていない」に意味上分かれます。
クイックリファレンス
# End-to-end, when you already know the display name + version:
export CODEMAGIC_API_KEY=... # or drop a `.codemagic-api-key` file at the repo root
python3 scripts/codemagic_fetch_artifacts.py --app "Pixel Buddy" --build 1.2.3 \
| tee /tmp/cm.json
CACHE=$(jq -r .cacheDir /tmp/cm.json)
bash scripts/symbolize_flutter_anr.sh \
/tmp/play-stacktrace.log \
"$CACHE/android_native_debug_symbols.zip" \
"$CACHE"/*_artifacts.zip \
"$CACHE/mapping.txt" # omit if the build didn't run R8/ProGuard
# → /tmp/play-stacktrace.symbolized.txt