crash_reporting (1.2.0)
Installation
dart pub add crash_reporting:1.2.0 --hosted-url=About this package
Dart.Package.CrashReporting
Crash reporting for Flutter apps, published as crash_reporting on the Platform pub feed. Reports go to the app's own backend as JSON; there is no third-party service.
What it catches:
- Dart errors: framework errors (
FlutterError.onError), unhandled async errors (PlatformDispatcher.onError) and whatever the app'srunZonedGuardedsees. An image that fails to load for a network reason is a log warning, not a report. - Errors that escape Dart on the web: a WebAssembly trap or a stack overflow unwinds past every Dart
catch, so neitherFlutterError.onErrornor the zone sees it. The browser'serrorevent does, and it is reported with kindbrowser. When it happens during paint, the frame's half-recordedPictureLayerstays in the tree and every later frame fails withNull check operator used on a null valueinPictureLayer.addToScene: that report is the symptom, thebrowserreport before it is the cause. On the webError.stackTraceLimitis raised to 64, so traces keep more than the browser's default 10 frames. - Android process deaths Dart never sees (native crash, GPU driver, WebView, ANR, out of memory, Java crash), reported on the next launch from
ActivityManager.getHistoricalProcessExitReasons(API 30+). The first check looks back a week; later checks start where the last one stopped.- A native tombstone is decoded from protobuf into debuggerd's text form: the crashing thread's backtrace first (rel pc, library, BuildId), then the logcat lines Android kept, then six frames of every other thread, so the 60,000-character cap cuts the least useful part.
- Android keeps no trace for a Java crash, so the plugin writes the uncaught exception to
files/last_java_crash.txtand attaches it to an exit within a minute of it. - A crash on the WebView's in-process GPU thread (
Chrome_InProcGpin a GPU driver library) is flaggedwebViewGpuCrash: truein the exit, so an app can turn its WebView feature off on the next launch.
Each report carries the device, OS, screen, route, lifecycle, memory, build mode, on web the browser's userAgent, the seconds since CrashContext.facts was called (uptimeSeconds), a settings snapshot without anything named like a token or secret, and the last 400 log lines. Emails and token-like strings are scrubbed; BuildId: and build_id: hex is kept because symbolizing needs it. Reports are on by default; the user's choice and a random install id (a dashed Guid) are kept in a store the app provides.
Wiring it into an app
Future<void> main() async {
late final CrashReporter crashReporter;
runZonedGuarded(
() async {
WidgetsFlutterBinding.ensureInitialized();
final store = await openMyKeyValueStore('crash_reporting.json');
crashReporter = CrashReporter(
api: HttpCrashReportApi(client: http.Client(), baseUrl: Uri.parse(apiBaseUrl)),
store: (read: store.read, write: store.write),
appVersion: appVersion,
platform: platformName,
facts: CrashContext.facts(appVersion: appVersion, earlierLog: readLogFile),
);
await crashReporter.initialize();
unawaited(CrashContext.loadDevice().then((_) => crashReporter.reportNativeExits()));
reportUncaughtErrors(crashReporter, logError: log.error, logWarning: log.warning);
runApp(const MyApp());
},
(error, stack) => crashReporter.reportError(error, stack, kind: 'zone'),
);
}
Feed the context as the app runs:
CrashContext.addLine(line)from the app's logger, already scrubbed of anything app-specific.CrashContext.routefrom aNavigatorObserver,CrashContext.lifecyclefrom aWidgetsBindingObserver.CrashContext.useSettings(settings.toJson())whenever settings change.
crashReporter.setEnabled(bool) backs the user's switch in settings.
The backend contract
POST {baseUrl}/errors (HttpCrashReportApi.path to change it), anonymous, application/json:
| Field | |
|---|---|
message, stackTrace |
Scrubbed. A native exit's stackTrace is the decoded tombstone or Java trace |
appVersion, platform, installId |
installId is a dashed Guid |
occurredAt |
UTC ISO 8601; for a native exit, when the process died |
kind |
flutter, unhandled, zone, browser (web, an error that escaped Dart), or an Android exit reason: crash-native, crash-java, anr, init-failure, excessive-resources, low-memory, signaled |
context |
Optional JSON string: the device facts above, plus exit and reportedLater for a native exit |
log |
Optional: the last 60,000 characters of the log, cut from the front |
Any status of 400 or above counts as a failure; failures are swallowed so reporting never throws into the app. Service.Pedalsim.Backend's ErrorsController is a working receiver.
Testing
flutter test runs the Dart tests. The Kotlin tests (android/src/test) need an Android SDK, which CI lacks; run them from any app that depends on the package:
android/gradlew -p android :crash_reporting:testDebugUnitTest
Publishing
Bump Version.txt and push to main; flutter-publish publishes it when that version is not on the feed yet.