Flutter Network Monitoring with Dio: A Complete Guide
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 valueThe 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.