flutter_mayday

pub.dev License: MIT Flutter Platform

On-device crash reporting for Flutter โ€” no server, no account, no dashboard.

When something goes wrong in production, BlackBox gives your developer everything they need to reproduce the bug in a single file they can attach to any message.


The problem

Mobile bugs are notoriously hard to reproduce. By the time a developer receives a vague bug report, the device state, network responses, navigation history, and logs that caused the crash are gone.

The solution

BlackBox runs inside your app like an airplane's flight data recorder. It continuously writes context into fixed-size in-memory buffers โ€” zero disk I/O during normal operation. When a crash fires or a tester taps the badge, it assembles one self-contained .blackbox file containing everything needed to reproduce the bug.

One file. No upload. No account. Attach it to a GitHub issue, Slack message, or support ticket and your developer can open the in-app viewer or decode it locally in seconds.


Features

  • ๐Ÿ“ฆ One-file reports โ€” everything in a gzip-compressed, CRC32-verified .blackbox envelope
  • ๐Ÿ” In-app viewer โ€” BlackBoxViewerScreen renders the full report on-device, structured and searchable
  • ๐Ÿ›ก๏ธ Privacy-first โ€” PII is redacted before any file is written: key blocklist, JWT, credit card, and custom regex rules
  • โšก Zero runtime overhead โ€” bounded circular buffers, no disk I/O, no network calls during normal operation
  • ๐Ÿ“ณ Shake to report โ€” shake the device to open the viewer or share a file instantly
  • ๐Ÿ“ธ Screenshot on crash โ€” captures the screen automatically when a fatal error fires
  • ๐ŸŒ Dio integration โ€” one interceptor captures every request, response, and error
  • ๐Ÿ—๏ธ State adapters โ€” snapshot BLoC, GetX, or any custom state manager at report time
  • ๐Ÿ“Š Four export formats โ€” .blackbox, .json, .md, .html
  • ๐Ÿ”‡ Off mode โ€” BlackBoxMode.off makes every call a no-op for enterprise opt-out
  • ๐ŸŽฃ onBeforeExport hook โ€” transform, enrich, or strip the report before it is written

What's in every report

Section Captured data
App Name, version, build number, flavor, Flutter & Dart version, compilation mode, git commit
Session Unique session ID, start time, duration, crash-free session count before this one
Device Manufacturer, model, OS version, screen dimensions, DPR, locale, timezone, accessibility flags
Environment Battery level & charging state, network type, memory pressure, text scale, brightness
Crash Error type, message, full stack trace, source (Flutter / platform dispatcher / isolate)
Errors All fatal and non-fatal errors recorded this session
Navigation Current route, route stack, full history with push/pop/replace actions and timestamps
Network Every Dio request โ€” URL, method, status, latency, request & response headers and truncated bodies
Logs All log entries with level, tag, extras, and timestamp
Performance Total frames, janky frames, janky%, average and worst frame ms, estimated FPS
Breadcrumbs Custom event trail with labels, extras, and timestamps
State Snapshot of every registered state adapter (BLoC, GetX, custom)

Quick start

1. Add the dependency

dependencies:
  flutter_mayday: ^0.2.0

2. Initialise before runApp

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

// Share this key between BlackBox and MaterialApp so the overlay can navigate.
final _navKey = GlobalKey<NavigatorState>();

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

  await BlackBox.init(
    config: const BlackBoxConfig(
      mode: BlackBoxMode.always,         // verbose in debug, always in release
      onCrash: BlackBoxOnCrash.exportAndShare,
      shakeToReport: true,
      screenshotOnCrash: true,
    ),
  );

  BlackBox.setNavigatorKey(_navKey);     // required for shake / badge tap

  runApp(
    BlackBoxScope(                       // wraps your whole app
      child: MaterialApp(
        navigatorKey: _navKey,
        navigatorObservers: [BlackBox.navigationObserver],
        home: const MyApp(),
      ),
    ),
  );
}

Why the navigator key?
BlackBoxScope sits above MaterialApp to catch errors before the widget tree tears down. This means the overlay's context has no Navigator ancestor. Passing the same GlobalKey<NavigatorState> to both BlackBox.setNavigatorKey() and MaterialApp(navigatorKey:) gives the overlay a direct reference to the navigator state.

3. Add the Dio interceptor

final dio = Dio();
dio.interceptors.add(BlackBox.dioInterceptor);

That's it. Crashes, logs, navigation, and network traffic are now captured automatically.


In-app viewer

BlackBoxViewerScreen renders the full report snapshot inside your app โ€” no external tools needed.

Open it programmatically:

Navigator.of(context).push(
  MaterialPageRoute(builder: (_) => const BlackBoxViewerScreen()),
);

Or use the badge controls:

Gesture Action
Tap the badge Opens BlackBoxViewerScreen
Long-press the badge Shares the .blackbox file via the system sheet
Shake the device Opens the viewer (when shakeToReport: true)

The badge appears in the bottom-right corner whenever shakeToReport: true. In production you can disable it and trigger the viewer from your own support screen.


State adapters

Register adapters once โ€” state is captured lazily at export time, never on every change.

BLoC / Cubit

BlackBox.registerAdapter(
  BlocStateAdapter<CartState>(
    bloc: cartBloc,
    name: 'cart',
    serialiser: (state) => state.toJson(),
  ),
);

GetX

BlackBox.registerAdapter(
  GetXStateAdapter<ProfileController>(
    controller: profileController,
    name: 'profile',
    serialiser: (c) => c.toJson(),
  ),
);

Custom (any state manager)

class AppSettingsAdapter extends StateAdapter {
  @override
  String get name => 'settings';

  @override
  Map<String, dynamic> captureState() => {
    'theme': AppSettings.theme,
    'locale': AppSettings.locale,
    'flags': AppSettings.featureFlags,
  };
}

BlackBox.registerAdapter(AppSettingsAdapter());

Manual API

// Breadcrumbs โ€” build a user journey trail
BlackBox.addBreadcrumb('user_tapped_checkout', extras: {'cart_id': 'abc'});

// Structured logging โ€” five levels
BlackBox.log(BlackBoxLogLevel.warning, 'Retry attempt 3', tag: 'network');
BlackBox.log(BlackBoxLogLevel.info, 'Session started', extras: {'plan': 'pro'});

// User identity โ€” PII-scrubbed on export
BlackBox.identify(userId: 'u_123', traits: {'plan': 'pro', 'role': 'admin'});

// Non-fatal capture โ€” records error, adds breadcrumb, writes report
await BlackBox.capture(
  label: 'payment_failed',
  error: exception,
  stackTrace: stackTrace,
  extras: {'amount': 99.0, 'currency': 'USD'},
);

Export

// Write to a file
final file = await BlackBox.export(format: ReportFormat.blackbox);
final json  = await BlackBox.export(format: ReportFormat.json);

// Write + share via system sheet
await BlackBox.share(format: ReportFormat.blackbox);

// Build report map for in-app display (no file written)
final report = await BlackBox.buildSnapshot();

Formats

Format Extension Best for
blackbox .blackbox Sharing โ€” compact gzip binary, decoded with BlackboxDecoder
json .json Tooling โ€” pretty-printed, opens in any editor
markdown .md Documentation โ€” renders on GitHub and GitLab
html .html Quick review โ€” opens directly in any browser

Privacy & PII scrubbing

BlackBox never uploads data. Reports are generated on-device and only written when your code asks for it.

Every export is recursively scrubbed before being written:

Rule What it catches
Key blocklist password, token, authorization, secret, api_key, access_token, refresh_token, private_key, credential โ€” and any keys you add
JWT bearer tokens Any Authorization: Bearer eyJโ€ฆ value
Credit card numbers Any sequence that passes the Luhn checksum
Custom keys Via BlackBoxConfig.redactKeys
Custom patterns Via BlackBoxConfig.redactPatterns
BlackBoxConfig(
  redactKeys: const ['ssn', 'date_of_birth', 'routing_number'],
  redactPatterns: [
    RegExp(r'\b\d{3}-\d{2}-\d{4}\b'),   // SSN
    RegExp(r'\b\d{9}\b'),                // routing number
  ],
)

Configuration reference

BlackBoxConfig(
  // Collection mode
  mode: BlackBoxMode.always,              // always | verbose | off

  // Crash behaviour
  onCrash: BlackBoxOnCrash.exportAndShare, // none | exportOnly | exportAndShare
  screenshotOnCrash: true,

  // Overlay / shake
  shakeToReport: true,

  // Buffer sizes
  maxLogEntries: 500,
  maxNetworkEntries: 100,
  maxNetworkBodyBytes: 4096,             // bytes per request/response body
  maxBreadcrumbs: 100,
  frameWindowSeconds: 10,               // rolling window for frame timing

  // Privacy
  redactKeys: const ['ssn'],
  redactPatterns: const [],

  // Export
  defaultExportFormat: ReportFormat.blackbox,

  // Hook โ€” transform the report map before writing
  onBeforeExport: (report) async {
    return {...report, 'server_region': await fetchRegion()};
  },
)

Mode comparison

Mode Use case Overhead Extra collectors
always Production Minimal โ€” bounded in-memory buffers All core collectors
verbose Debug & staging Higher โ€” tracks user input + gesture capture, widget tree dump
off Enterprise opt-out Zero None โ€” every call is a silent no-op

onBeforeExport hook

Transform the full report map before it is sanitised and written. Async is supported.

onBeforeExport: (report) async {
  // Add server-side enrichment
  final flags = await FeatureFlags.fetchAll();
  return {
    ...report,
    'feature_flags': flags,
    'server_region': Platform.environment['REGION'] ?? 'unknown',
  };
},

.blackbox file format

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Magic      โ”‚  42 4C 4B 42  (BLKB)          โ”‚
โ”‚  Version    โ”‚  00 01                         โ”‚
โ”‚  Flags      โ”‚  bit 0 = gzip, bit 1 = screenshot โ”‚
โ”‚  Length     โ”‚  uint32 big-endian payload len โ”‚
โ”‚  Payload    โ”‚  gzip-compressed UTF-8 JSON    โ”‚
โ”‚  CRC32      โ”‚  uint32 checksum of JSON bytes โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Decode programmatically:

import 'package:flutter_mayday/flutter_mayday.dart';

final bytes = await file.readAsBytes();
final report = BlackboxDecoder.decode(bytes); // Map<String, dynamic>

Complete setup example

import 'package:flutter_mayday/flutter_mayday.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

final _navKey = GlobalKey<NavigatorState>();

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

  await BlackBox.init(
    config: BlackBoxConfig(
      mode: kDebugMode ? BlackBoxMode.verbose : BlackBoxMode.always,
      onCrash: BlackBoxOnCrash.exportAndShare,
      shakeToReport: true,
      screenshotOnCrash: true,
      redactKeys: const ['ssn', 'dob'],
      onBeforeExport: (r) async => {...r, 'app_env': 'production'},
    ),
  );

  BlackBox.setNavigatorKey(_navKey);
  BlackBox.identify(userId: 'u_001', traits: {'plan': 'pro'});

  // Dio
  final dio = Dio()..interceptors.add(BlackBox.dioInterceptor);

  // BLoC
  BlackBox.registerAdapter(BlocStateAdapter<AuthState>(
    bloc: authBloc,
    name: 'auth',
    serialiser: (s) => s.toJson(),
  ));

  runApp(BlackBoxScope(
    child: MaterialApp(
      navigatorKey: _navKey,
      navigatorObservers: [BlackBox.navigationObserver],
      home: const MyHomePage(),
    ),
  ));
}

Platform support

Platform Supported
Android โœ…
iOS โœ…
macOS โŒ
Web โŒ
Windows โŒ
Linux โŒ

iOS notes

iOS sandboxing limits some diagnostics. ANR detection always returns false โ€” iOS does not expose Android-style process error APIs. Memory pressure is reported via DispatchSource.makeMemoryPressureSource. Some memory fields may be null depending on OS availability.


Contributing

Run flutter analyze and flutter test before opening a pull request.

Package goals: on-device first ยท privacy first ยท bounded memory ยท clean public API ยท no generated code ยท no server dependency.


Built with โค๏ธ by Yudiz Solutions

Libraries

flutter_mayday
On-device crash reporting and environment capture for Flutter.