fl_crashpad 1.0.0 copy "fl_crashpad: ^1.0.0" to clipboard
fl_crashpad: ^1.0.0 copied to clipboard

retracted

Native crash reporting for Flutter with Google Crashpad: sanitised minidumps of crashes the Dart VM never sees, on desktop, Android and iOS, for any minidump backend.

example/lib/main.dart

// fl_crashpad's example, and the package's end-to-end check: the only place
// the handler is found where a real build puts it, inside a real bundle.
//
// Run it, press a crash button, start it again: the crash is in the list,
// and opening it shows what the report holds.
//
// Headless, for CI:
//
//   <bundle>/fl_crashpad_example --database=/tmp/db --crash=segfault
//
// starts Crashpad on that database and crashes before any window opens; the
// report is then in /tmp/db. On Android the same arguments arrive through the
// launching intent (tool/android_e2e.dart):
//
//   adb shell am start -n dev.wilielmus.fl_crashpad_example/.MainActivity \
//     --esa dart_entrypoint_args --crash=segfault
//
// Every option, each also usable alone:
//   --database=<dir>   where reports go (default: under app support)
//   --crash=<kind>     a CrashpadTestCrash name, or `dump` for a report of a
//                      process that carries on
//   --upload=<url>     send reports there, with the user's consent given
//   --report-json      print the reports, once sanitised (and on mobile sent),
//                      as one `fl_crashpad_reports: <json>` line, and exit
import 'dart:convert';
import 'dart:io';

import 'package:fl_crashpad/fl_crashpad.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';

import 'report_page.dart';

Future<void> main(List<String> args) async {
  String? option(String name) => args
      .where((a) => a.startsWith('--$name='))
      .map((a) => a.substring(name.length + 3))
      .firstOrNull;

  final crash = option('crash');
  final upload = option('upload');
  final reportJson = args.contains('--report-json');
  final Directory database;
  if (option('database') case final path?) {
    database = Directory(path);
  } else {
    WidgetsFlutterBinding.ensureInitialized();
    database = Directory(
      '${(await getApplicationSupportDirectory()).path}/crashpad',
    );
  }

  // As early as possible: nothing that crashes before this line is caught.
  Object? startError;
  try {
    Crashpad.start(
      CrashpadOptions(
        databaseDirectory: database,
        fixedAnnotations: {'example.build': 'fl_crashpad example'},
        uploadEndpoint: upload == null
            ? null
            : CrashpadUploadEndpoint(url: Uri.parse(upload), rateLimit: false),
        // `--upload` is the headless check's: whoever runs it with a URL has
        // said yes. An app passes what its user answered — never `true`
        // because it has somewhere to send.
        uploadConsent: upload == null ? null : true,
      ),
    );
    Crashpad.annotations['example.screen'] = 'home';
    if (crash != null) {
      // Something the sanitiser has to take out, for the headless check to
      // look for in the raw dump and not find in the sanitised one.
      Crashpad.annotations['example.contact'] = 'ada@example.com';
    }
  } on CrashpadException catch (e) {
    startError = e;
  }

  if ((crash != null || reportJson) && startError != null) {
    stderr.writeln(startError);
    // print, too: on Android it is the one that reaches logcat.
    // ignore: avoid_print
    print('fl_crashpad_error: $startError');
    exit(2);
  }
  if (crash == 'dump') {
    Crashpad.dumpWithoutCrash();
  } else if (crash != null) {
    Crashpad.crashForTesting(CrashpadTestCrash.values.byName(crash));
  }
  if (reportJson) {
    await Crashpad.sanitizationIdle;
    final reports = await CrashReportDatabase(database).reports();
    // ignore: avoid_print — a headless run's output, read from logcat.
    print(
      'fl_crashpad_reports: ${jsonEncode([
        for (final r in reports) {'id': r.id, 'state': r.state.name, 'sanitized': r.sanitized, 'uploaded': r.uploaded, 'remoteId': r.remoteId, 'minidump': r.minidump.path},
      ])}',
    );
    exit(0);
  }
  if (crash == 'dump') exit(0);

  runApp(ExampleApp(database: database, startError: startError));
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key, required this.database, this.startError});

  final Directory database;
  final Object? startError;

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'fl_crashpad',
    theme: ThemeData(colorSchemeSeed: Colors.deepOrange),
    darkTheme: ThemeData(
      colorSchemeSeed: Colors.deepOrange,
      brightness: Brightness.dark,
    ),
    home: HomePage(database: database, startError: startError),
  );
}

class HomePage extends StatefulWidget {
  const HomePage({super.key, required this.database, this.startError});

  final Directory database;
  final Object? startError;

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late final CrashReportDatabase _reports = CrashReportDatabase(
    widget.database,
  );
  final _annotation = TextEditingController();
  List<CrashReport> _list = const [];
  bool _uploads = false;

  @override
  void initState() {
    super.initState();
    _refresh();
  }

  @override
  void dispose() {
    _annotation.dispose();
    super.dispose();
  }

  Future<void> _refresh() async {
    final list = await _reports.reports();
    if (!mounted) return;
    setState(() {
      _list = list;
      _uploads = _reports.uploadConsent;
    });
  }

  void _say(String message) => ScaffoldMessenger.of(
    context,
  ).showSnackBar(SnackBar(content: Text(message)));

  @override
  Widget build(BuildContext context) {
    final started = Crashpad.isStarted;
    final theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(title: const Text('fl_crashpad')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          ListTile(
            leading: Icon(
              started ? Icons.shield : Icons.error_outline,
              color: started ? Colors.green : theme.colorScheme.error,
            ),
            title: Text(started ? 'Crashpad is running' : 'Not started'),
            subtitle: Text(
              widget.startError?.toString() ??
                  'Crashpad ${Crashpad.crashpadRevision.substring(0, 12)} · '
                      '${_reports.sanitizer.sanitizeText(widget.database.path)}',
            ),
          ),
          const Divider(),
          Text('Runtime annotation', style: theme.textTheme.titleMedium),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: TextField(
                  controller: _annotation,
                  decoration: const InputDecoration(
                    labelText: 'example.note',
                    border: OutlineInputBorder(),
                  ),
                ),
              ),
              const SizedBox(width: 8),
              FilledButton(
                onPressed: () {
                  try {
                    Crashpad.annotations['example.note'] = _annotation.text;
                    _say('The next report carries example.note.');
                  } on ArgumentError catch (e) {
                    _say('${e.message}');
                  }
                },
                child: const Text('Set'),
              ),
            ],
          ),
          const SizedBox(height: 24),
          Text('Crash on purpose', style: theme.textTheme.titleMedium),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              for (final kind in CrashpadTestCrash.values)
                OutlinedButton(
                  onPressed: started
                      ? () => Crashpad.crashForTesting(kind)
                      : null,
                  child: Text(kind.name),
                ),
              OutlinedButton(
                onPressed: started
                    ? () {
                        Crashpad.dumpWithoutCrash();
                        _say('Report written; the app carries on.');
                        Future<void>.delayed(
                          const Duration(seconds: 1),
                          _refresh,
                        );
                      }
                    : null,
                child: const Text('dump without crash'),
              ),
            ],
          ),
          const SizedBox(height: 24),
          SwitchListTile(
            title: const Text('Send crash reports'),
            subtitle: const Text(
              'The user\'s consent, kept in the database. Nothing is sent '
              'without an upload URL either, and this example has none.',
            ),
            value: _uploads,
            onChanged: (value) {
              _reports.uploadConsent = value;
              _refresh();
            },
          ),
          const Divider(),
          Row(
            children: [
              Text(
                'Reports (${_list.length})',
                style: theme.textTheme.titleMedium,
              ),
              const Spacer(),
              IconButton(
                tooltip: 'Refresh',
                onPressed: _refresh,
                icon: const Icon(Icons.refresh),
              ),
            ],
          ),
          for (final report in _list)
            ListTile(
              dense: true,
              leading: const Icon(Icons.description_outlined),
              onTap: () async {
                await Navigator.of(context).push(
                  MaterialPageRoute<void>(
                    builder: (_) =>
                        ReportPage(report: report, database: _reports),
                  ),
                );
                _refresh();
              },
              title: Text(report.id),
              subtitle: Text(
                '${report.sanitized ? 'sanitised' : 'raw'} · '
                '${report.state.name} · ${report.createdAt.toLocal()} · '
                '${report.minidump.existsSync() ? report.minidump.lengthSync() : 0} bytes',
              ),
              trailing: IconButton(
                tooltip: 'Delete',
                icon: const Icon(Icons.delete_outline),
                onPressed: () {
                  _reports.delete(report.id);
                  _refresh();
                },
              ),
            ),
        ],
      ),
    );
  }
}
2
likes
0
points
430
downloads

Publisher

verified publisherconnecting-the-dots.dev

Weekly Downloads

Native crash reporting for Flutter with Google Crashpad: sanitised minidumps of crashes the Dart VM never sees, on desktop, Android and iOS, for any minidump backend.

Repository (GitHub)
View/report issues

Topics

#crash-reporting #crashpad #minidump #ffi #privacy

Funding

Consider supporting this project:

buymeacoffee.com

License

unknown (license)

Dependencies

archive, code_assets, crypto, ffi, hooks

More

Packages that depend on fl_crashpad

Packages that implement fl_crashpad