πŸ”¬ crash_forensics

The forensic crash reporter for Flutter β€” freezes a complete runtime snapshot the moment your app dies.

pub version License: MIT Platform Dart SDK


🧠 The Problem

[FATAL] Null check operator used on a null value
#0  ProfileScreen._buildHeader (profile_screen.dart:87)

That's all you get. No context. No variable values. No idea what the user was doing. No clue why userModel was null. You deploy a fix, guess blindly, and hope it sticks.

πŸ’‘ The Solution

crash_forensics acts as the black box recorder of your Flutter app. The moment a crash occurs, it instantly captures and freezes:

What Details
πŸ“‹ Stack Trace Parsed line-by-line, app frames highlighted
πŸ“œ Last 100 log lines Circular buffer, no memory waste
🍞 Breadcrumbs Every navigation, tap, lifecycle event before the crash
πŸ”­ Watched Variables You mark variables with CrashForensics.watch() β€” they appear in the report
πŸ“± Device Snapshot Platform, OS, screen, locale, battery, network
βš™οΈ Runtime Snapshot Dart heap estimate, process memory, dart/app version
πŸ”’ Privacy Filter Sensitive keywords automatically redacted

Everything is saved as a clean, structured JSON file locally β€” no servers, no subscriptions, no internet required.


πŸ“¦ Installation

dependencies:
  crash_forensics: ^1.0.0
flutter pub get

πŸš€ Quick Start

Minimal setup (2 lines)

void main() {
  ForensicsInitializer.run(
    appRunner: () => runApp(const MyApp()),
  );
}
void main() {
  ForensicsInitializer.run(
    config: ForensicsConfig.production(
      appName: 'MyApp',
      appVersion: '2.4.1',
    ),
    appRunner: () => runApp(const MyApp()),
  );
}

Full custom setup

void main() {
  ForensicsInitializer.run(
    config: ForensicsConfig(
      appName: 'MyApp',
      appVersion: '2.4.1',
      environment: 'staging',
      maxLogLines: 150,
      maxBreadcrumbs: 75,
      enableLocalReporting: true,
      enableConsoleReporting: true,
      verboseConsole: false,
      maxStoredReports: 25,
      sensitiveKeywords: ['password', 'token', 'auth', 'secret', 'api_key', 'bearer'],
      enableRuntimeSnapshot: true,
      enableDeviceInfo: true,
      customReporters: [
        HttpReporter(
          endpoint: 'https://your-server.com/api/crashes',
          headers: {'Authorization': 'Bearer your-token'},
        ),
      ],
    ),
    appRunner: () => runApp(const MyApp()),
  );
}

πŸ“ Logging

Manual log messages

// Basic
CrashForensics.log('User tapped checkout button');

// With tag and level
CrashForensics.log(
  'Payment API returned 402',
  tag: 'PAYMENT',
  level: LogLevel.warning,
);

CrashForensics.log(
  'Cart is empty β€” unexpected state',
  tag: 'CART',
  level: LogLevel.error,
);

🍞 Breadcrumbs

// Manual breadcrumb
CrashForensics.addBreadcrumb(
  'User tapped "Checkout" button',
  type: BreadcrumbType.userInteraction,
  data: {'cart_items': 3, 'total': 49.99},
);

// Navigation is automatic if you add the observer:
MaterialApp(
  navigatorObservers: [ForensicNavigatorObserver()],
  ...
)

πŸ”­ Watch Variables

Mark variables you care about β€” they will be captured in every crash report:

// In your state management / service layer
CrashForensics.watch('currentUserId', userId);
CrashForensics.watch('cartItemCount', cart.items.length);
CrashForensics.watch('paymentStep', _currentStep);
CrashForensics.watch('userModel', userModel?.toString() ?? 'null');

When a crash happens, you'll see in the report:

"watched_variables": {
  "currentUserId": "443",
  "cartItemCount": 3,
  "paymentStep": "CONFIRM",
  "userModel": "null"
}

πŸ“‚ Reading Saved Reports

// Get all saved crash reports
final reports = await CrashForensics.getSavedReports();

for (final report in reports) {
  print('--- Crash Report ---');
  print('ID:        ${report.id}');
  print('Time:      ${report.timestamp}');
  print('Exception: ${report.exception.type}');
  print('Message:   ${report.exception.message}');
  print('Last log:  ${report.lastLogs.last.message}');
  print('Breadcrumbs: ${report.breadcrumbs.length} events');
}

// Clear all reports
await CrashForensics.clearReports();

🌐 HTTP Reporter (optional)

Send crash reports to your own server or webhook:

// In your ForensicsConfig:
customReporters: [
  HttpReporter(
    endpoint: 'https://your-server.com/api/crash',
    headers: {
      'Content-Type': 'application/json',
      'X-App-Key': 'your-api-key',
    },
    timeout: const Duration(seconds: 15),
    retryCount: 3,
  ),
],

πŸ“„ Sample Report JSON

{
  "id": "cf_1748734521000_a3f9b",
  "timestamp": "2026-05-31T14:35:21.000Z",
  "environment": "production",
  "app_name": "MyApp",
  "app_version": "2.4.1",
  "exception": {
    "type": "_TypeError",
    "message": "Null check operator used on a null value",
    "context": "FlutterError.onError"
  },
  "stack_trace": [
    {
      "package": "my_app",
      "file_path": "lib/screens/profile_screen.dart",
      "file_name": "profile_screen.dart",
      "line_number": 87,
      "column_number": 23,
      "function_name": "ProfileScreen._buildHeader",
      "is_app_frame": true,
      "is_flutter_frame": false,
      "is_dart_core_frame": false
    }
  ],
  "last_logs": [
    {
      "timestamp": "2026-05-31T14:35:20.112Z",
      "level": "info",
      "tag": "AUTH",
      "message": "User logged in: user_id=443"
    },
    {
      "timestamp": "2026-05-31T14:35:21.001Z",
      "level": "warning",
      "tag": "API",
      "message": "fetchUserProfile returned 404 for user_id=443"
    }
  ],
  "breadcrumbs": [
    {
      "timestamp": "2026-05-31T14:35:15.000Z",
      "event": "Tapped My Profile button",
      "type": "user_interaction",
      "data": { "widget": "BottomNavBar", "index": 3 }
    },
    {
      "timestamp": "2026-05-31T14:35:20.000Z",
      "event": "Navigated to: /profile",
      "type": "navigation",
      "data": { "from": "/home", "to": "/profile" }
    }
  ],
  "runtime_snapshot": {
    "rss_memory_mb": 187,
    "max_rss_memory_mb": 210,
    "dart_version": "3.4.0",
    "app_version": "2.4.1",
    "app_build_number": "47",
    "is_debug_mode": false,
    "is_release_mode": true,
    "watched_variables": {
      "currentUserId": "443",
      "userModel": "null",
      "isLoggedIn": true,
      "currentRoute": "/profile"
    }
  },
  "device_info": {
    "platform": "android",
    "os_version": "Android 14 (API 34)",
    "device_model": "Pixel 7 Pro",
    "device_brand": "Google",
    "is_physical_device": true,
    "locale": "ar_SA",
    "timezone": "Asia/Riyadh",
    "screen_width_px": 1440,
    "screen_height_px": 3120,
    "device_pixel_ratio": 3.5,
    "battery_level": 67,
    "is_charging": false,
    "network_type": "wifi"
  }
}

βš–οΈ Comparison

Feature crash_forensics Firebase Crashlytics Sentry
Free forever βœ… ⚠️ Limited ⚠️ Limited
Works offline βœ… ❌ ❌
Local storage βœ… ❌ ❌
Watch variables βœ… ❌ ❌
Breadcrumbs βœ… βœ… βœ…
Circular log buffer βœ… ❌ ❌
Privacy-first βœ… (local only) ❌ Google servers ❌ External
No SDK bloat βœ… ❌ ⚠️
Custom reporters βœ… ❌ ⚠️
Pure Dart/Flutter βœ… ❌ (Firebase) ⚠️

πŸ›‘οΈ Privacy & Security

  • All data is stored locally by default β€” nothing leaves the device without your explicit HttpReporter
  • Sensitive keywords are automatically redacted from all report fields: password, token, secret, api_key, bearer, etc.
  • You control the full list via ForensicsConfig.sensitiveKeywords
  • Optional AES-256 encryption for stored reports via ReportEncryptor

πŸ“± Platform Support

Platform Local Storage Device Info Battery Network
Android βœ… βœ… βœ… βœ…
iOS βœ… βœ… βœ… βœ…
macOS βœ… βœ… βœ… βœ…
Windows βœ… βœ… ⚠️ N/A βœ…
Linux βœ… βœ… ⚠️ N/A βœ…
Web ⚠️ In-memory ⚠️ Limited ❌ βœ…

πŸ“œ License

MIT β€” see LICENSE


🀝 Contributing

Issues and PRs are welcome! See CONTRIBUTING.md for guidelines.


"Don't just tell me the app died β€” tell me how it lived in its final moments."

Libraries

crash_forensics
crash_forensics β€” The forensic crash reporter for Flutter.