diff --git a/packages/devtools_app/lib/src/screens/network/network_controller.dart b/packages/devtools_app/lib/src/screens/network/network_controller.dart index dc76ad14cc1..c3c16a81cee 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -14,6 +14,7 @@ import '../../shared/config_specific/logger/allowed_error.dart'; import '../../shared/globals.dart'; import '../../shared/http/http_request_data.dart'; import '../../shared/http/http_service.dart' as http_service; +import '../../shared/offline_data.dart'; import '../../shared/primitives/utils.dart'; import '../../shared/ui/filter.dart'; import '../../shared/ui/search.dart'; @@ -49,6 +50,7 @@ class NetworkController extends DisposableController with SearchControllerMixin, FilterControllerMixin, + OfflineScreenControllerMixin, AutoDisposeControllerMixin { NetworkController() { _networkService = NetworkService(this); @@ -386,6 +388,25 @@ class NetworkController extends DisposableController serviceConnection.errorBadgeManager.incrementBadgeCount(NetworkScreen.id); } } + + @override + OfflineScreenData prepareOfflineScreenData() { + final requests = + filteredData.value.whereType().toList(); + return OfflineScreenData( + screenId: NetworkScreen.id, + data: convertRequestsToMap(requests), + ); + } +} + +Map convertRequestsToMap( + List? requests, +) { + if (requests == null) return {}; + return { + 'requests': requests.map((request) => request.toJson()).toList(), + }; } /// Class for managing the set of all current sockets, and diff --git a/packages/devtools_app/lib/src/screens/network/network_screen.dart b/packages/devtools_app/lib/src/screens/network/network_screen.dart index 8b2b3785269..612c5b8693a 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -123,19 +123,45 @@ class _NetworkScreenBodyState extends State void didChangeDependencies() { super.didChangeDependencies(); if (!initController()) return; - unawaited(controller.startRecording()); - - cancelListeners(); + try { + if (offlineDataController.showingOfflineData.value == true) { + loadOfflineData(offlineDataController.offlineDataJson); + } + cancelListeners(); + if (!offlineDataController.showingOfflineData.value) { + unawaited(controller.startRecording()); + debugPrint('started recording'); + addAutoDisposeListener( + serviceConnection.serviceManager.isolateManager.mainIsolate, + () { + if (serviceConnection + .serviceManager.isolateManager.mainIsolate.value != + null) { + unawaited(controller.startRecording()); + } + }, + ); + } + } catch (ex) { + debugPrint('caught ex $ex'); + } + } - addAutoDisposeListener( - serviceConnection.serviceManager.isolateManager.mainIsolate, - () { - if (serviceConnection.serviceManager.isolateManager.mainIsolate.value != - null) { - unawaited(controller.startRecording()); - } - }, - ); + void loadOfflineData(Map offlineData) { + final requestsMap = (offlineData['network'] + as Map)['requests'] as List; + final requests = requestsMap + .map( + (e) => DartIOHttpRequestData.fromJson( + e as Map, + null, + null, + ), + ) + .toList(); + controller.filteredData + ..clear() + ..addAll(requests); } @override @@ -187,7 +213,7 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> @override void initState() { super.initState(); - + addAutoDisposeListener(offlineDataController.showingOfflineData); _recording = widget.controller.recordingNotifier.value; addAutoDisposeListener(widget.controller.recordingNotifier, () { setState(() { diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart index 319d641c01e..9dec4ba2fdc 100644 --- a/packages/devtools_app/lib/src/shared/feature_flags.dart +++ b/packages/devtools_app/lib/src/shared/feature_flags.dart @@ -38,6 +38,9 @@ bool get enableBeta => enableExperiments || !isExternalBuild; const _kMemoryDisconnectExperience = bool.fromEnvironment('memory_disconnect_experience', defaultValue: true); +const _kNetworkOfflineExperiment = + bool.fromEnvironment('network_disconnect_experience', defaultValue: true); + // It is ok to have enum-like static only classes. // ignore: avoid_classes_with_only_static_members /// Flags to hide features under construction. @@ -62,6 +65,11 @@ abstract class FeatureFlags { /// https://github.com/flutter/devtools/issues/5606 static const memoryDisconnectExperience = _kMemoryDisconnectExperience; + /// Flag to enable offline data on network screen. + /// + /// https://github.com/flutter/devtools/issues/3806 + static const networkOffline = _kNetworkOfflineExperiment; + /// Flag to enable save/load for the Memory screen. /// /// https://github.com/flutter/devtools/issues/8019 diff --git a/packages/devtools_app/lib/src/shared/http/http_request_data.dart b/packages/devtools_app/lib/src/shared/http/http_request_data.dart index bef81dc7de1..40c2aae4ed4 100644 --- a/packages/devtools_app/lib/src/shared/http/http_request_data.dart +++ b/packages/devtools_app/lib/src/shared/http/http_request_data.dart @@ -58,6 +58,105 @@ class DartIOHttpRequestData extends NetworkRequest { .._responseBody = responseContent?['text'].toString() .._requestBody = requestPostData?['text'].toString(); } + //TODO go through all parameters, to check if they are correctly added. + Map? toJson() { + try { + return { + 'startedDateTime': startTimestamp.toIso8601String(), + 'time': duration?.inMilliseconds ?? 0, + 'request': { + 'method': method, + 'url': uri, + 'httpVersion': + 'HTTP/2.0', // Assuming HTTP/1.1, can adjust dynamically if needed + 'cookies': requestCookies.map((cookie) => cookie.toString()).toList(), + 'headers': requestHeaders, + 'queryString': queryParameters?.entries + .map((entry) => {'name': entry.key, 'value': entry.value}) + .toList(), + 'postData': { + 'mimeType': contentType, + 'text': requestBody ?? 'None', + }, + //TODO check headersSize calculation + 'headersSize': requestHeaders != null + ? requestHeaders!.length * 40 + : 0, + 'bodySize': requestBody?.length ?? 0, + 'connectionInfo': { + 'remoteAddress': (general['connectionInfo'] + as Map?)?['remoteAddress'], + 'localPort': (general['connectionInfo'] + as Map?)?['localPort'], + }, + 'contentLength': general['contentLength'] ?? 0, + 'followRedirects': _request.request?.followRedirects ?? true, + 'maxRedirects': _request.request?.maxRedirects ?? 5, + 'persistentConnection': + _request.request?.persistentConnection ?? true, + 'proxyDetails': { + 'proxy': + (general['proxyDetails'] as Map?)?['proxy'], + 'type': (general['proxyDetails'] as Map?)?['type'], + }, + 'error': general['error'], + }, + 'response': { + 'status': status ?? 'Error', + 'statusCode': _request.response!.statusCode, + 'statusText': '', + //TODO : get http version/ add in constants + 'httpVersion': 'HTTP/2.0', + 'cookies': + responseCookies.map((cookie) => cookie.toString()).toList(), + 'headers': responseHeaders, + 'content': { + 'size': responseBody?.length ?? 0, + 'mimeType': + contentType ?? 'json', + 'text': responseBody ?? '', + }, + 'redirects': _request.response!.redirects, + //TODO : add redirectURL + 'redirectURL': '', + 'headersSize': + responseHeaders != null ? responseHeaders!.length * 40 : 0, + 'bodySize': encodedResponse?.length ?? 0, + }, + 'cache': {}, + 'timings': { + 'blocked': -1, + 'dns': -1, + 'connect': -1, + 'send': 1, + 'wait': duration?.inMilliseconds ?? 0, + 'receive': 1, + 'ssl': -1, + }, + 'connection': (general['connectionInfo'] + as Map?)?['connectionId'] ?? + '525659655', // sample connection ID + 'comment': '', + 'isolateId': _request.isolateId, + 'type': '@HttpProfileRequest', + 'method': method, + 'uri': uri, + 'id': id, + 'startTime': startTimestamp.microsecondsSinceEpoch, + 'events': instantEvents + .map( + (event) => { + 'timestamp': event.timestamp.microsecondsSinceEpoch, + 'event': event.name, + }, + ) + .toList(), + }; + } catch (ex) { + _log.shout('Error in toJson: $ex'); + } + return null; + } static const _connectionInfoKey = 'connectionInfo'; static const _contentTypeKey = 'content-type'; diff --git a/packages/devtools_app/lib/src/shared/screen.dart b/packages/devtools_app/lib/src/shared/screen.dart index 74c89719f98..3dd5ca9b1fe 100644 --- a/packages/devtools_app/lib/src/shared/screen.dart +++ b/packages/devtools_app/lib/src/shared/screen.dart @@ -76,6 +76,8 @@ enum ScreenMetaData { iconAsset: 'icons/app_bar/network.png', requiresDartVm: true, tutorialVideoTimestamp: '?t=547', + // ignore: avoid_redundant_argument_values, false positive + worksWithOfflineData: FeatureFlags.networkOffline, ), logging( 'logging',