0Pricing
Flutter Mobile Development · Lekcja

Raportowanie awarii i stack trace'y z symbolami

Integruj Crashlytics i Sentry z mapami desymbolizacji, aby uzyskiwać użyteczne raporty awarii

Raportowanie awarii i stack trace'y z symbolami to bezpłatna lekcja Flutter Mobile Development na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Flutter Mobile Development, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Flutter Mobile Development zawiera 4 lekcji w sumie.

Części tej lekcji nie zostały jeszcze przetłumaczone i są wyświetlane po angielsku.

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.

Często zadawane pytania

Czy lekcja „Raportowanie awarii i stack trace'y z symbolami” jest bezpłatna?

Tak — pełny tekst „Raportowanie awarii i stack trace'y z symbolami” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Flutter Mobile Development, przejdź na CoddyKit PRO. Kurs Flutter Mobile Development zawiera 4 lekcji w sumie.

Co nauczysz się w „Raportowanie awarii i stack trace'y z symbolami”?

Integruj Crashlytics i Sentry z mapami desymbolizacji, aby uzyskiwać użyteczne raporty awarii Ćwiczysz Flutter Mobile Development z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Flutter Mobile Development?

Nie wymagamy żadnego doświadczenia. Flutter Mobile Development w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Raportowanie awarii i stack trace'y z symbolami”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Flutter Mobile Development?

Tak. Każda lekcja Flutter Mobile Development zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Warianty kompilacji i konfiguracja środowiska
  2. Automatyczne potoki z Fastlane i GitHub Actions
  3. Raportowanie awarii i stack trace'y z symbolami
  4. Zdalna konfiguracja, flagi funkcji i etapowe wdrażanie
← Powrót do Flutter Mobile Development