flutter_mayday
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
.blackboxenvelope - ๐ In-app viewer โ
BlackBoxViewerScreenrenders 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.offmakes every call a no-op for enterprise opt-out - ๐ฃ
onBeforeExporthook โ 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?
BlackBoxScopesits aboveMaterialAppto catch errors before the widget tree tears down. This means the overlay's context has noNavigatorancestor. Passing the sameGlobalKey<NavigatorState>to bothBlackBox.setNavigatorKey()andMaterialApp(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.