0Pricing
Flutter Mobile Development · レッスン

クラッシュレポートとシンボル化されたスタックトレース

難読化解除マッピングとともにCrashlyticsとSentryを統合し、実用的なクラッシュレポートを作成します。

「クラッシュレポートとシンボル化されたスタックトレース」はCoddyKit上の無料Flutter Mobile Developmentレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはFlutter Mobile Development学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Flutter Mobile Developmentコースには全4レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

Why Raw Crash Traces Are Useless

When you ship a Flutter app in release mode, the Dart compiler produces optimized AOT machine code and strips human-readable symbols. A crash that happens in production no longer reports UserRepository.fetchProfile — it reports a hex offset like #00 abs 0000007f9c2a1b40.

  • Debug builds keep symbols, so traces are readable locally.
  • Release builds obfuscate and strip, so the on-device trace is just addresses.
  • To turn those addresses back into method names you need symbolication (native iOS/Android symbols) and deobfuscation (Dart symbol mapping).

This lesson wires up Crashlytics and Sentry so every production crash arrives fully symbolicated and actionable.

Obfuscation and the Symbol Map File

Flutter obfuscation is opt-in. You enable it at build time and Flutter writes a Dart symbol map per target architecture into a directory you choose.

  • --obfuscate renames Dart identifiers to short tokens.
  • --split-debug-info writes the mapping files (e.g. app.android-arm64.symbols) so you can reverse the obfuscation later.

You must archive these symbol files per release. Without the exact file that matches the shipped binary, the trace can never be deobfuscated.

# Build an obfuscated Android release and keep the Dart symbols
flutter build appbundle --release \
  --obfuscate \
  --split-debug-info=build/symbols/v1.4.0

# iOS equivalent
flutter build ipa --release \
  --obfuscate \
  --split-debug-info=build/symbols/v1.4.0

Two Layers: Native vs Dart Symbols

A Flutter crash report has two distinct symbolication problems, and confusing them is the #1 reason traces stay unreadable.

  • Native layer (iOS dSYM, Android NDK .so symbols): resolves crashes inside the engine, plugins, and platform code. Handled by Crashlytics' native symbol upload.
  • Dart layer (the --split-debug-info files): resolves crashes inside your Dart business logic after obfuscation.

Crashlytics symbolicates the native frames automatically once you upload dSYMs/NDK symbols, but the Dart frames still need the Flutter symbol file run through flutter symbolize.

Initializing Firebase Crashlytics

Crashlytics must be initialized before runApp and wired into Flutter's error hooks so nothing slips through.

  • FlutterError.onError captures synchronous framework errors.
  • PlatformDispatcher.instance.onError captures async errors that escape the zone.

Routing both into recordFlutterFatalError / recordError guarantees every uncaught Dart error reaches Crashlytics.

import 'dart:ui';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  final crashlytics = FirebaseCrashlytics.instance;
  FlutterError.onError = crashlytics.recordFlutterFatalError;
  PlatformDispatcher.instance.onError = (error, stack) {
    crashlytics.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(const MyApp());
}

Uploading Native Symbols to Crashlytics

For native frames, Crashlytics needs the build's symbol files uploaded to Firebase.

  • iOS: the FirebaseCrashlytics run script uploads dSYMs; for bitcode or missing dSYMs use the upload-symbols tool.
  • Android: the firebase-crashlytics Gradle plugin uploads NDK symbols when nativeSymbolUploadEnabled is true.

These uploads happen at build/release time, not at runtime, so they belong in your CI pipeline.

// android/app/build.gradle
plugins {
    id 'com.google.firebase.crashlytics'
}

android {
    buildTypes {
        release {
            firebaseCrashlytics {
                // Upload Android NDK native symbols
                nativeSymbolUploadEnabled true
                unstrippedNativeLibsDir 'build/app/intermediates/merged_native_libs'
            }
        }
    }
}

Deobfuscating Dart Frames with flutter symbolize

Crashlytics cannot read your Dart --split-debug-info map. When a crash's Dart frames show obfuscated tokens, you export the raw trace and run it through the Flutter SDK tool.

  • flutter symbolize takes the obfuscated stack trace plus the exact symbol file for that ABI and version.
  • The output replaces hex offsets and short tokens with real Dart method names and line numbers.

This is why archiving the per-release symbol directory is non-negotiable — you need the file matching the crashing build.

# stack.txt holds the obfuscated trace copied from Crashlytics
flutter symbolize \
  -i stack.txt \
  -d build/symbols/v1.4.0/app.android-arm64.symbols

# Output now shows real frames, e.g.:
#   #00  UserRepository.fetchProfile (package:app/data/user_repository.dart:42)

Adding Sentry Alongside Crashlytics

Sentry is a popular alternative or companion to Crashlytics. The Flutter SDK wraps your app and auto-captures unhandled errors, but the real value is its debug symbol pipeline.

  • SentryFlutter.init installs the integrations and lets you set sampling, release, and environment.
  • Wrapping runApp in appRunner ensures the zone-level error handler is active.

Set the release string to name+buildNumber so Sentry can match the crash to the correct uploaded symbols.

import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

Future<void> main() async {
  await SentryFlutter.init(
    (options) {
      options.dsn = 'https://examplePublicKey@o0.ingest.sentry.io/0';
      options.tracesSampleRate = 0.2;
      options.environment = 'production';
      options.release = 'com.coddykit.app@1.4.0+140';
    },
    appRunner: () => runApp(const MyApp()),
  );
}

Uploading Debug Symbols to Sentry

Sentry deobfuscates automatically server-side once you upload the right artifacts with sentry-cli (or the Sentry Dart plugin during the build).

  • Upload the native dSYM / ELF debug files via debug-files upload.
  • Upload the Flutter Dart symbols (the --split-debug-info output) so obfuscated Dart frames resolve.

Because Sentry matches by debug ID and release, uploads from CI must use the same --split-debug-info directory that produced the shipped binary.

# Upload native + Dart debug information files for this release
sentry-cli debug-files upload \
  --org coddykit --project flutter-app \
  build/symbols/v1.4.0

# Associate the release so Sentry can match incoming events
sentry-cli releases new com.coddykit.app@1.4.0+140
sentry-cli releases finalize com.coddykit.app@1.4.0+140

Enriching Reports with Context and Breadcrumbs

A symbolicated trace tells you where; context tells you why. Both SDKs let you attach metadata so a crash is reproducible.

  • Custom keys: feature flags, user tier, current route.
  • Breadcrumbs / logs: a trail of recent actions leading to the crash.
  • User identifier: to gauge how many users a crash affects.

Never log PII. Use opaque IDs and feature-relevant flags only.

Future<void> tagCrashContext(FirebaseCrashlytics c) async {
  await c.setUserIdentifier('user_8f3a');
  await c.setCustomKey('subscription', 'pro');
  await c.setCustomKey('active_route', '/checkout');
  await c.log('Tapped Pay button with cart total 49.99');
}

Recording Handled (Non-Fatal) Errors

Not every error should crash the app. A failed network call or a caught parsing error is a non-fatal event you still want visibility into.

  • Crashlytics: recordError(e, st, fatal: false).
  • Sentry: Sentry.captureException(e, stackTrace: st).

This standalone Dart example shows the catch-and-report pattern you would feed into either SDK from a service layer.

void reportHandled(Object error, StackTrace stack) {
  // In production this would call recordError / captureException.
  print('NON-FATAL: $error');
  print('Top frame: ${stack.toString().split('\n').first}');
}

Map<String, dynamic> parseProfile(String raw) {
  if (!raw.startsWith('{')) {
    throw const FormatException('Profile payload is not JSON');
  }
  return {'ok': true};
}

void main() {
  try {
    parseProfile('not-json');
  } catch (e, st) {
    reportHandled(e, st);
  }
  print('App keeps running after handled error');
}

Making Symbol Upload Part of CI

The single biggest cause of unreadable production crashes is a broken release process: someone ships a build but forgets to upload or archive its symbols. Automate it.

  • Build with --obfuscate --split-debug-info=build/symbols/$VERSION.
  • Upload native symbols (Crashlytics Gradle plugin / iOS run script) and Dart symbols (sentry-cli) in the same CI job.
  • Persist build/symbols/$VERSION as a CI artifact retained for as long as the version can crash in the wild.

Tie the version string used everywhere — Sentry release, Crashlytics setCustomKey, and the symbol directory name — to one source of truth so matching never drifts.

# CI snippet (bash) — one job that builds and ships symbols
VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}')

flutter build appbundle --release \
  --obfuscate --split-debug-info=build/symbols/$VERSION

# Dart + native symbols to Sentry
sentry-cli debug-files upload --org coddykit --project flutter-app \
  build/symbols/$VERSION

# Keep the map for later flutter symbolize runs
tar -czf symbols-$VERSION.tgz build/symbols/$VERSION

Quick Check: Why Dart Frames Stay Obfuscated

You shipped a Flutter release built with --obfuscate --split-debug-info. Native frames in Crashlytics are readable, but the Dart frames in your business logic still show short tokens and hex offsets. What is the correct fix?

Recap: An Actionable Crash Pipeline

You now have an end-to-end pipeline that turns cryptic production crashes into fixable bug reports.

  • Build release with --obfuscate --split-debug-info=build/symbols/$VERSION and archive that directory per release.
  • Capture via FlutterError.onError + PlatformDispatcher.onError (Crashlytics) and/or SentryFlutter.init with appRunner.
  • Symbolicate two layers: native (dSYM/NDK uploads) and Dart (flutter symbolize for Crashlytics, automatic server-side for Sentry after symbol upload).
  • Enrich with user IDs, custom keys, and breadcrumbs — no PII.
  • Automate symbol upload in CI and keep one version string as the single source of truth.

The golden rule: a crash report is only as good as the symbol file you kept for that exact build.

よくある質問

「クラッシュレポートとシンボル化されたスタックトレース」レッスンは無料ですか?

はい。「クラッシュレポートとシンボル化されたスタックトレース」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Flutter Mobile Developmentコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Flutter Mobile Developmentコースには全4レッスンが含まれています。

「クラッシュレポートとシンボル化されたスタックトレース」で何を学びますか?

難読化解除マッピングとともにCrashlyticsとSentryを統合し、実用的なクラッシュレポートを作成します。 ブラウザで直接実行するハンズオンコードでFlutter Mobile Developmentを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Flutter Mobile Developmentを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのFlutter Mobile Developmentは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「クラッシュレポートとシンボル化されたスタックトレース」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このFlutter Mobile Developmentレッスンでコードを書いて実行できますか?

はい。すべてのFlutter Mobile Developmentレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. ビルドフレーバーと環境設定
  2. FastlaneとGitHub Actionsによる自動化パイプライン
  3. クラッシュレポートとシンボル化されたスタックトレース
  4. リモート設定、フィーチャーフラグ、段階的ロールアウト
← Flutter Mobile Developmentに戻る