flutter_mayday 0.2.0
flutter_mayday: ^0.2.0 copied to clipboard
On-device crash reporting for Flutter. Captures device, session, network, logs, errors, and performance into a single shareable .blackbox file — no server, no account, no dashboard required.
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