AI-powered crash analysis is now available on all plans — including Free.Read the crash analysis guide

Flutter Network Monitoring with Dio: A Complete Guide

NFNourin Mahfuj Finick··7 min read

Network failures cause a large fraction of Flutter app crashes and user-visible errors. An API that returns a 500 triggers an unhandled exception in code that assumed success. A timeout causes a null reference when the response never arrives. Without network monitoring, your crash report shows the exception but not the API failure that caused it.


This guide covers how to monitor every HTTP request in a Flutter app using Dio's interceptor system.


Why Dio for Network Monitoring


Dio is Flutter's most popular HTTP client because it has a mature interceptor API that makes monitoring clean. You add one interceptor and every request made through that Dio instance is captured — including requests from other parts of your app that use the same Dio instance.


If you use the built-in http package instead, BugsPulse provides a wrapper client. Both approaches are covered below.


Dio Interceptor Setup


import 'package:dio/dio.dart';
import 'package:bugspulse/bugspulse.dart';

final dio = Dio(
  BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 30),
  ),
);

// Add BugsPulse monitoring interceptor
dio.interceptors.add(
  BugsPulseDioInterceptor(
    captureRequestBody: false,    // never — may contain PII
    captureResponseBody: false,   // never — may contain PII
    captureHeaders: false,        // never — contains auth tokens
    sanitizeUrl: (url) {
      // Remove user IDs from paths
      return url.replaceAll(RegExp(r'/users/[^/]+'), '/users/[id]');
    },
  ),
);

Every request made through this dio instance is captured: URL (sanitized), method, status code, response time, and any error message.


Custom Dio Interceptor (Manual Implementation)


If you need custom logic, implement the interceptor directly:


class NetworkMonitorInterceptor extends Interceptor {
  final _requestTimes = <String, int>{};

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // Record start time — use request key for correlation
    final key = '${options.method}:${options.path}:${DateTime.now().millisecondsSinceEpoch}';
    options.extra['_monitoring_key'] = key;
    _requestTimes[key] = DateTime.now().millisecondsSinceEpoch;
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    _recordRequest(
      options: response.requestOptions,
      statusCode: response.statusCode ?? 0,
    );
    handler.next(response);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    _recordRequest(
      options: err.requestOptions,
      statusCode: err.response?.statusCode ?? 0,
      errorMessage: err.message,
    );
    handler.next(err);
  }

  void _recordRequest({
    required RequestOptions options,
    required int statusCode,
    String? errorMessage,
  }) {
    final key = options.extra['_monitoring_key'] as String?;
    final startTime = key != null ? _requestTimes.remove(key) : null;
    final duration = startTime != null
        ? DateTime.now().millisecondsSinceEpoch - startTime
        : null;

    BugsPulse.captureNetworkRequest(
      url: _sanitizeUrl(options.uri.toString()),
      method: options.method,
      statusCode: statusCode,
      durationMs: duration,
      errorMessage: errorMessage,
    );
  }

  String _sanitizeUrl(String url) {
    return url.replaceAll(RegExp(r'/users/[^/?]+'), '/users/[id]');
  }
}

// Add to Dio
dio.interceptors.add(NetworkMonitorInterceptor());

http Package Alternative


If you use Dart's built-in http package:


import 'package:http/http.dart' as http;
import 'package:bugspulse/bugspulse.dart';

// Wrap the default client
final client = BugsPulseHttpClient(http.Client());

// Use client.get() instead of http.get()
final response = await client.get(
  Uri.parse('https://api.example.com/data'),
  headers: {'Authorization': 'Bearer $token'},
);

Monitoring GraphQL Requests


GraphQL requests are all POST to the same endpoint, so URL alone isn't useful. Capture the operation name instead:


class GraphQLMonitorInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    // Extract operation name from request body (safe — operation name is not PII)
    if (options.data is Map) {
      final operationName = (options.data as Map)['operationName'] as String?;
      if (operationName != null) {
        options.extra['_operation_name'] = operationName;
        BugsPulse.addBreadcrumb(
          message: 'GraphQL: $operationName',
          category: 'graphql',
          level: BreadcrumbLevel.info,
        );
      }
    }
    handler.next(options);
  }
}

Reading the Network Timeline


After setup, crash sessions in your BugsPulse dashboard show a network timeline. A typical Flutter crash story:


15:42:01  Navigation → ProductDetailsScreen
15:42:01  GET /api/products/detail?id=... 200 (312ms)
15:42:04  TAP → AddToCartButton
15:42:04  POST /api/cart 422 (88ms)
15:42:04  CRASH: Null check operator used on null value

The 422 response returned a body without the field your code expected after success. Root cause found in seconds.


Dio Error Types and What They Mean


try {
  final response = await dio.get('/api/data');
} on DioException catch (e) {
  switch (e.type) {
    case DioExceptionType.connectionTimeout:
      // Server didn't respond in time — network issue or server overload
    case DioExceptionType.receiveTimeout:
      // Server started responding but didn't finish — large payload or slow server
    case DioExceptionType.badResponse:
      // Got a response but status code indicated failure (4xx, 5xx)
      final statusCode = e.response?.statusCode;
    case DioExceptionType.connectionError:
      // No network connection
    case DioExceptionType.cancel:
      // Request was explicitly cancelled
    default:
      // Other Dio error
  }

  BugsPulse.captureException(e,
    context: {
      'url': e.requestOptions.path,
      'error_type': e.type.name,
      'status': e.response?.statusCode,
    }
  );
}

Summary


Flutter network monitoring is most cleanly implemented with a Dio interceptor that captures URL, method, status, and duration — never bodies or headers. Add it once to your Dio instance and every request in the app is covered. The resulting network timeline in crash reports shows the API failure that caused the exception, reducing triage time from hours to minutes.