Skip to content

Attach Logs to a Flutter Bug Report: Console, Errors, Network

October 14, 2026 · 3 min read · by the Tracebird team

To attach logs to a Flutter bug report, keep a small in-memory buffer of recent console lines, errors and HTTP calls, then send that buffer with the tester's report. Capture it continuously, not after the bug, because by the time someone taps "report" the evidence is already gone.

A screenshot tells you what the tester saw. The last 100 log lines and the failed request tell you why. Here is how to collect both in a Flutter app, what to leave out, and when to stop maintaining it yourself.

Why a screenshot is not enough

"The Pay button spins forever" is a symptom. The cause is usually one line away: a POST /api/payments that returned 500, or an exception thrown in a build method. Without that line you ask the tester to reproduce the bug, and most never answer.

A useful report carries four things next to the capture: recent console output, uncaught errors with stack traces, the last network calls (method, URL, status, duration), and the environment (app version, OS, locale). If you are still deciding what to collect overall, our guide to Flutter bug reporting covers the full list.

Keep a ring buffer of console output

Do not write logs to a file for this. Keep the last N entries in memory and drop the oldest as new ones arrive.

class LogBuffer {
  LogBuffer(this.capacity);
  final int capacity;
  final _lines = <String>[];

  void add(String line) {
    _lines.add('${DateTime.now().toIso8601String()} $line');
    if (_lines.length > capacity) _lines.removeAt(0);
  }

  List<String> snapshot() => List.unmodifiable(_lines);
}

final logs = LogBuffer(100);

Then feed it from the three places Flutter output comes from:

void main() {
  runZonedGuarded(() {
    WidgetsFlutterBinding.ensureInitialized();

    final previous = debugPrint;
    debugPrint = (String? message, {int? wrapWidth}) {
      if (message != null) logs.add(message);
      previous(message, wrapWidth: wrapWidth);
    };

    FlutterError.onError = (details) {
      logs.add('FlutterError: ${details.exceptionAsString()}\n${details.stack}');
      FlutterError.presentError(details);
    };

    runApp(const MyApp());
  }, (error, stack) => logs.add('Uncaught: $error\n$stack'),
      zoneSpecification: ZoneSpecification(
        print: (self, parent, zone, line) {
          logs.add(line);
          parent.print(zone, line);
        },
      ));
}

debugPrint covers framework and most package output, FlutterError.onError catches errors inside the widget tree, and the zone catches async errors and plain print calls. Also set PlatformDispatcher.instance.onError if you target a recent Flutter version where that is the recommended hook for errors outside the framework.

Record network calls with an interceptor

If you use Dio, an interceptor is the cleanest place to record requests. Store the method, URL, status code and duration, nothing more.

class ReportInterceptor extends Interceptor {
  final _started = <RequestOptions, DateTime>{};

  @override
  void onRequest(RequestOptions o, RequestInterceptorHandler h) {
    _started[o] = DateTime.now();
    h.next(o);
  }

  @override
  void onResponse(Response r, ResponseInterceptorHandler h) {
    final ms = DateTime.now().difference(_started.remove(r.requestOptions)!).inMilliseconds;
    logs.add('${r.requestOptions.method} ${r.requestOptions.uri} -> ${r.statusCode} (${ms} ms)');
    h.next(r);
  }

  @override
  void onError(DioException e, ErrorInterceptorHandler h) {
    _started.remove(e.requestOptions);
    logs.add('${e.requestOptions.method} ${e.requestOptions.uri} -> ERROR ${e.type.name}');
    h.next(e);
  }
}

With the http package or raw dart:io, wrap the client instead. The idea is identical: one line per call, written to the same buffer.

What to leave out of the report

Logs and requests are where secrets leak. Before anything leaves the device:

  • Never record request or response bodies by default. They hold tokens, emails and payment data.
  • Strip Authorization and cookie headers, and mask query parameters such as token or key.
  • Cap the buffer size, and cap each line, so one huge JSON dump cannot bloat the upload.
  • Tell testers what is attached. A short line in the feedback screen is enough, and it matters under GDPR.

When the DIY version stops being worth it

This is about 100 lines of code, and it works. The cost shows up later: keeping it in sync with Dio and Flutter releases, queueing reports when the tester is offline, and grouping fifty near-identical reports into one bug. If your testers shake the phone to report, our shake to report a bug guide shows the trigger side, and our piece on a Flutter bug tracker for tester bugs covers where those reports should land.

If you would rather not maintain this, the Tracebird Flutter package does it in one call: capture: TracebirdCapture(logs: true, network: true) attaches the last 100 console lines and 50 network requests to every report, with inputs masked and network bodies off unless you turn them on.