From fe11d5becf5e4657c936984156c1febb8dfb7d6c Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Sat, 21 Sep 2024 14:46:21 +0530 Subject: [PATCH 01/36] added initial implementation --- .../screens/network/network_controller.dart | 21 ++++ .../src/screens/network/network_screen.dart | 52 +++++++--- .../lib/src/shared/feature_flags.dart | 8 ++ .../src/shared/http/http_request_data.dart | 99 +++++++++++++++++++ .../devtools_app/lib/src/shared/screen.dart | 2 + 5 files changed, 169 insertions(+), 13 deletions(-) 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', From 6c140b4920ebc1138b4a3405b5b3785ac7c7f521 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Sun, 29 Sep 2024 18:20:30 +0530 Subject: [PATCH 02/36] adding constants file --- .../lib/src/shared/http/constants.dart | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/devtools_app/lib/src/shared/http/constants.dart diff --git a/packages/devtools_app/lib/src/shared/http/constants.dart b/packages/devtools_app/lib/src/shared/http/constants.dart new file mode 100644 index 00000000000..c3af56e515d --- /dev/null +++ b/packages/devtools_app/lib/src/shared/http/constants.dart @@ -0,0 +1,74 @@ +// Copyright 2024 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +enum HttpRequestDataKeys { + connectionInfo, + remoteAddress, + localPort, + contentLength, + startedDateTime, + time, + request, + method, + url, + httpVersion, + cookies, + headers, + queryString, + postData, + mimeType, + text, + headersSize, + bodySize, + followRedirects, + maxRedirects, + persistentConnection, + proxyDetails, + proxy, + type, + error, + response, + status, + statusCode, + statusText, + redirects, + redirectURL, + cache, + timings, + blocked, + dns, + connect, + send, + wait, + receive, + ssl, + connection, + comment, + isolateId, + uri, + id, + startTime, + events, + timestamp, + event, + compressionState, + isRedirect, + reasonPhrase, + queryParameters, + content, + size, + connectionId +} + +enum HttpRequestDataValues { + json, +} + +class HttpRequestDataDefaults { + static const none = 'None'; + static const error = 'Error'; + static const httpVersion = 'HTTP/2.0'; + static const json = 'json'; + static const httpProfileRequest = '@HttpProfileRequest'; +} From 0c1f414763658ec8a34876a56fb8649815ae45ae Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 30 Sep 2024 20:54:27 +0530 Subject: [PATCH 03/36] updated NEXT_RELEASE_NOTES.md --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index fa99239b317..1a8a6502f78 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -41,7 +41,7 @@ TODO: Remove this section if there are not any general updates. ## Network profiler updates -TODO: Remove this section if there are not any general updates. +* Resolved an issue in .har export where response content was sometimes missing in the data. - [#8333](https://github.com/flutter/devtools/pull/8333) ## Logging updates From e7eeecb668cf07561ceaf8b2d840ab79cf7160ce Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 30 Sep 2024 20:56:36 +0530 Subject: [PATCH 04/36] Revert "updated NEXT_RELEASE_NOTES.md" This reverts commit 0c1f414763658ec8a34876a56fb8649815ae45ae. --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 1a8a6502f78..fa99239b317 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -41,7 +41,7 @@ TODO: Remove this section if there are not any general updates. ## Network profiler updates -* Resolved an issue in .har export where response content was sometimes missing in the data. - [#8333](https://github.com/flutter/devtools/pull/8333) +TODO: Remove this section if there are not any general updates. ## Logging updates From c5ec6f6c43f1bdc5c24840ad1d9011e6cd816b28 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Wed, 2 Oct 2024 22:46:47 +0530 Subject: [PATCH 05/36] used constants, added util for header size calc, added params --- .../src/screens/network/har_data_entry.dart | 26 +-- .../screens/network/network_controller.dart | 4 +- .../src/screens/network/utils/http_utils.dart | 27 +++ .../src/shared/http/http_request_data.dart | 166 ++++++++++-------- 4 files changed, 124 insertions(+), 99 deletions(-) create mode 100644 packages/devtools_app/lib/src/screens/network/utils/http_utils.dart diff --git a/packages/devtools_app/lib/src/screens/network/har_data_entry.dart b/packages/devtools_app/lib/src/screens/network/har_data_entry.dart index be71e04e2d2..74272b80b78 100644 --- a/packages/devtools_app/lib/src/screens/network/har_data_entry.dart +++ b/packages/devtools_app/lib/src/screens/network/har_data_entry.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import '../../screens/network/utils/http_utils.dart'; import '../../shared/http/http_request_data.dart'; import '../../shared/primitives/utils.dart'; import 'constants.dart'; @@ -131,7 +132,7 @@ class HarDataEntry { NetworkEventKeys.text.name: e.requestBody, }, NetworkEventKeys.headersSize.name: - _calculateHeadersSize(e.requestHeaders), + calculateHeadersSize(e.requestHeaders), NetworkEventKeys.bodySize.name: _calculateBodySize(e.requestBody), }, // Response @@ -150,7 +151,7 @@ class HarDataEntry { }, NetworkEventKeys.redirectURL.name: '', NetworkEventKeys.headersSize.name: - _calculateHeadersSize(e.responseHeaders), + calculateHeadersSize(e.responseHeaders), NetworkEventKeys.bodySize.name: _calculateBodySize(e.responseBody), }, // Cache @@ -263,27 +264,6 @@ class HarDataEntry { } } -int _calculateHeadersSize(Map? headers) { - if (headers == null) return -1; - - // Combine headers into a single string with CRLF endings - String headersString = headers.entries.map((entry) { - final key = entry.key; - var value = entry.value; - // If the value is a List, join it with a comma - if (value is List) { - value = value.join(', '); - } - return '$key: $value\r\n'; - }).join(); - - // Add final CRLF to indicate end of headers - headersString += '\r\n'; - - // Calculate the byte length of the headers string - return utf8.encode(headersString).length; -} - int _calculateBodySize(String? requestBody) { if (requestBody.isNullOrEmpty) { return 0; 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 c3c16a81cee..c86e2ed9290 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -395,12 +395,12 @@ class NetworkController extends DisposableController filteredData.value.whereType().toList(); return OfflineScreenData( screenId: NetworkScreen.id, - data: convertRequestsToMap(requests), + data: serializeRequestDataForOfflineMode(requests), ); } } -Map convertRequestsToMap( +Map serializeRequestDataForOfflineMode( List? requests, ) { if (requests == null) return {}; diff --git a/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart b/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart new file mode 100644 index 00000000000..9d382acb957 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; + +/// Calculates the size of the headers in bytes. +/// +/// Takes a map of headers [headers], where keys are header names and values +/// can be strings or lists of strings. Returns the size of the headers +/// in bytes or -1 if [headers] is null. +int calculateHeadersSize(Map? headers) { + if (headers == null) return -1; + + // Combine headers into a single string with CRLF endings + String headersString = headers.entries.map((entry) { + final key = entry.key; + var value = entry.value; + // If the value is a List, join it with a comma + if (value is List) { + value = value.join(', '); + } + return '$key: $value\r\n'; + }).join(); + + // Add final CRLF to indicate end of headers + headersString += '\r\n'; + + // Calculate the byte length of the headers string + return utf8.encode(headersString).length; +} 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 40c2aae4ed4..b2bf8e3dcb5 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 @@ -11,8 +11,10 @@ import 'package:mime/mime.dart'; import 'package:vm_service/vm_service.dart'; import '../../screens/network/network_model.dart'; +import '../../screens/network/utils/http_utils.dart'; import '../globals.dart'; import '../primitives/utils.dart'; +import 'constants.dart'; import 'http.dart'; final _log = Logger('http_request_data'); @@ -55,102 +57,118 @@ class DartIOHttpRequestData extends NetworkRequest { HttpProfileRequestRef.parse(modifiedRequestData)!, requestFullDataFromVmService: false, ) - .._responseBody = responseContent?['text'].toString() - .._requestBody = requestPostData?['text'].toString(); + .._responseBody = + responseContent?[HttpRequestDataKeys.text.name].toString() + .._requestBody = + requestPostData?[HttpRequestDataKeys.text.name].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 + HttpRequestDataKeys.startedDateTime.name: + startTimestamp.toIso8601String(), + HttpRequestDataKeys.time.name: duration?.inMilliseconds ?? 0, + HttpRequestDataKeys.request.name: { + HttpRequestDataKeys.method.name: method, + HttpRequestDataKeys.url.name: uri, + HttpRequestDataKeys.httpVersion.name: + HttpRequestDataDefaults.httpVersion, 'cookies': requestCookies.map((cookie) => cookie.toString()).toList(), - 'headers': requestHeaders, - 'queryString': queryParameters?.entries + HttpRequestDataKeys.headers.name: requestHeaders, + HttpRequestDataKeys.queryString.name: queryParameters?.entries .map((entry) => {'name': entry.key, 'value': entry.value}) .toList(), - 'postData': { - 'mimeType': contentType, - 'text': requestBody ?? 'None', + 'queryParameters': queryParameters?.entries + .map((entry) => {'name': entry.key, 'value': entry.value}) + .toList(), + HttpRequestDataKeys.postData.name: { + HttpRequestDataKeys.mimeType.name: contentType, + HttpRequestDataKeys.text.name: + requestBody ?? HttpRequestDataDefaults.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'], + HttpRequestDataKeys.headersSize.name: + requestHeaders != null ? calculateHeadersSize(requestHeaders) : 0, + HttpRequestDataKeys.bodySize.name: requestBody?.length ?? 0, + HttpRequestDataKeys.connectionInfo.name: { + HttpRequestDataKeys.remoteAddress.name: + (general[HttpRequestDataKeys.connectionInfo.name] as Map?)?[HttpRequestDataKeys.remoteAddress.name], + HttpRequestDataKeys.localPort.name: (general[ + HttpRequestDataKeys.connectionInfo.name] + as Map?)?[HttpRequestDataKeys.localPort.name], }, - 'contentLength': general['contentLength'] ?? 0, - 'followRedirects': _request.request?.followRedirects ?? true, - 'maxRedirects': _request.request?.maxRedirects ?? 5, - 'persistentConnection': + HttpRequestDataKeys.contentLength.name: + general[HttpRequestDataKeys.contentLength.name] ?? 0, + HttpRequestDataKeys.followRedirects.name: + _request.request?.followRedirects ?? true, + HttpRequestDataKeys.maxRedirects.name: + _request.request?.maxRedirects ?? 5, + HttpRequestDataKeys.persistentConnection.name: _request.request?.persistentConnection ?? true, - 'proxyDetails': { - 'proxy': - (general['proxyDetails'] as Map?)?['proxy'], - 'type': (general['proxyDetails'] as Map?)?['type'], + HttpRequestDataKeys.proxyDetails.name: { + HttpRequestDataKeys.proxy.name: + (general[HttpRequestDataKeys.proxyDetails.name] + as Map?)?[HttpRequestDataKeys.proxy.name], + HttpRequestDataKeys.type.name: + (general[HttpRequestDataKeys.proxyDetails.name] + as Map?)?[HttpRequestDataKeys.type.name], }, - 'error': general['error'], + HttpRequestDataKeys.error.name: + general[HttpRequestDataKeys.error.name], }, - 'response': { - 'status': status ?? 'Error', - 'statusCode': _request.response!.statusCode, - 'statusText': '', - //TODO : get http version/ add in constants - 'httpVersion': 'HTTP/2.0', - 'cookies': + HttpRequestDataKeys.response.name: { + HttpRequestDataKeys.status.name: + status ?? HttpRequestDataDefaults.error, + HttpRequestDataKeys.statusCode.name: _request.response!.statusCode, + HttpRequestDataKeys.statusText.name: '', + HttpRequestDataKeys.httpVersion.name: + HttpRequestDataDefaults.httpVersion, + HttpRequestDataKeys.cookies.name: responseCookies.map((cookie) => cookie.toString()).toList(), - 'headers': responseHeaders, - 'content': { - 'size': responseBody?.length ?? 0, - 'mimeType': - contentType ?? 'json', - 'text': responseBody ?? '', + HttpRequestDataKeys.headers.name: responseHeaders, + HttpRequestDataKeys.content.name: { + HttpRequestDataKeys.size.name: responseBody?.length ?? 0, + HttpRequestDataKeys.mimeType.name: + contentType ?? HttpRequestDataValues.json.name, + HttpRequestDataKeys.text.name: responseBody ?? '', }, - 'redirects': _request.response!.redirects, + HttpRequestDataKeys.redirects.name: _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, + HttpRequestDataKeys.redirectURL.name: '', + HttpRequestDataKeys.headersSize.name: responseHeaders != null + ? calculateHeadersSize(responseHeaders) + : 0, + HttpRequestDataKeys.bodySize.name: encodedResponse?.length ?? 0, }, - '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 + HttpRequestDataKeys.cache.name: {}, + HttpRequestDataKeys.connection.name: + (general[HttpRequestDataKeys.connectionInfo.name] as Map?)?[HttpRequestDataKeys.connectionId.name] ?? + '', + //Remove if not required + HttpRequestDataKeys.comment.name: '', + HttpRequestDataKeys.isolateId.name: _request.isolateId, + HttpRequestDataKeys.type.name: + HttpRequestDataDefaults.httpProfileRequest, + HttpRequestDataKeys.method.name: method, + HttpRequestDataKeys.uri.name: uri, + HttpRequestDataKeys.id.name: id, + HttpRequestDataKeys.startTime.name: + startTimestamp.microsecondsSinceEpoch, + HttpRequestDataKeys.events.name: instantEvents .map( (event) => { - 'timestamp': event.timestamp.microsecondsSinceEpoch, - 'event': event.name, + HttpRequestDataKeys.timestamp.name: + event.timestamp.microsecondsSinceEpoch, + HttpRequestDataKeys.event.name: event.name, }, ) .toList(), + HttpRequestDataKeys.isRedirect.name: + _request.response?.isRedirect ?? false, + HttpRequestDataKeys.reasonPhrase.name: + _request.response?.reasonPhrase ?? '', }; } catch (ex) { _log.shout('Error in toJson: $ex'); From 2a9c4cbfb6da6457b228b04fe544954efa27eecf Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Wed, 2 Oct 2024 22:56:51 +0530 Subject: [PATCH 06/36] param renamed, used _log --- .../lib/src/screens/network/network_screen.dart | 7 +++++-- .../lib/src/shared/http/http_request_data.dart | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) 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 612c5b8693a..5ae79b76a7b 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -8,6 +8,7 @@ import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import '../../shared/analytics/analytics.dart' as ga; @@ -29,6 +30,8 @@ import 'network_controller.dart'; import 'network_model.dart'; import 'network_request_inspector.dart'; +final _log = Logger('http_request_data'); + class NetworkScreen extends Screen { NetworkScreen() : super.fromMetaData(ScreenMetaData.network); @@ -142,8 +145,8 @@ class _NetworkScreenBodyState extends State }, ); } - } catch (ex) { - debugPrint('caught ex $ex'); + } catch (e) { + _log.shout('Could not load offline data: $e'); } } 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 b2bf8e3dcb5..b5497d918ec 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 @@ -170,8 +170,8 @@ class DartIOHttpRequestData extends NetworkRequest { HttpRequestDataKeys.reasonPhrase.name: _request.response?.reasonPhrase ?? '', }; - } catch (ex) { - _log.shout('Error in toJson: $ex'); + } catch (e) { + _log.shout('Error in toJson: $e'); } return null; } From ae5873880657dc9bb6934d92acd62a20c9e9fb8b Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Wed, 2 Oct 2024 22:58:52 +0530 Subject: [PATCH 07/36] added copyright header --- .../lib/src/screens/network/utils/http_utils.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart b/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart index 9d382acb957..3550770df5d 100644 --- a/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart +++ b/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart @@ -1,3 +1,7 @@ +// Copyright 2024 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:convert'; /// Calculates the size of the headers in bytes. From 2bfbe1ce8e78aee4e7de9cc120951ea527820649 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Thu, 3 Oct 2024 18:35:18 +0530 Subject: [PATCH 08/36] setup changes implemented --- .../screens/network/network_controller.dart | 64 +++++++++++++---- .../src/screens/network/network_screen.dart | 42 ----------- .../screens/network/offline_network_data.dart | 69 +++++++++++++++++++ 3 files changed, 121 insertions(+), 54 deletions(-) create mode 100644 packages/devtools_app/lib/src/screens/network/offline_network_data.dart 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 2436dd08c95..548fb868f86 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; import 'package:vm_service/vm_service.dart'; import '../../shared/config_specific/import_export/import_export.dart'; @@ -23,6 +24,9 @@ import 'har_network_data.dart'; import 'network_model.dart'; import 'network_screen.dart'; import 'network_service.dart'; +import 'offline_network_data.dart'; + +final _log = Logger('http_request_data'); /// Different types of Network Response which can be used to visualise response /// on Response tab @@ -53,6 +57,7 @@ class NetworkController extends DisposableController OfflineScreenControllerMixin, AutoDisposeControllerMixin { NetworkController() { + _initHelper(); _networkService = NetworkService(this); _currentNetworkRequests = CurrentNetworkRequests(); addAutoDisposeListener( @@ -162,6 +167,47 @@ class NetworkController extends DisposableController @visibleForTesting bool get isPolling => _pollingTimer != null; + void _initHelper() async { + try { + if (offlineDataController.showingOfflineData.value == true) { + await maybeLoadOfflineData( + NetworkScreen.id, + createData: (json) => OfflineNetworkData.fromJson(json), + shouldLoad: (data) => data != null, + loadData: (data) => loadOfflineData(data), + ); + } + cancelListeners(); + if (!offlineDataController.showingOfflineData.value) { + unawaited(startRecording()); + debugPrint('started recording'); + addAutoDisposeListener( + serviceConnection.serviceManager.isolateManager.mainIsolate, + () { + if (serviceConnection + .serviceManager.isolateManager.mainIsolate.value != + null) { + unawaited(startRecording()); + } + }, + ); + } + } catch (e) { + _log.shout('Could not load offline data: $e'); + } + } + + void loadOfflineData(OfflineNetworkData offlineData) { + filteredData + ..clear() + ..addAll(offlineData.requests); + + if (offlineData.selectedRequestId != null) { + selectedRequest.value = + _currentNetworkRequests.getRequest(offlineData.selectedRequestId!); + } + } + @visibleForTesting void processNetworkTrafficHelper( List sockets, @@ -392,23 +438,17 @@ class NetworkController extends DisposableController @override OfflineScreenData prepareOfflineScreenData() { - final requests = - filteredData.value.whereType().toList(); + final offlineData = OfflineNetworkData( + requests: filteredData.value.whereType().toList(), + selectedRequestId: selectedRequest.value?.id, + ); + return OfflineScreenData( screenId: NetworkScreen.id, - data: serializeRequestDataForOfflineMode(requests), + data: offlineData.toJson(), ); } -} -Map serializeRequestDataForOfflineMode( - List? requests, -) { - if (requests == null) return {}; - return { - 'requests': requests.map((request) => request.toJson()).toList(), - }; - Future _fetchFullDataBeforeExport() => Future.wait( filteredData.value .whereType() 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 5ae79b76a7b..e69fc39fec0 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -8,7 +8,6 @@ import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import '../../shared/analytics/analytics.dart' as ga; @@ -30,8 +29,6 @@ import 'network_controller.dart'; import 'network_model.dart'; import 'network_request_inspector.dart'; -final _log = Logger('http_request_data'); - class NetworkScreen extends Screen { NetworkScreen() : super.fromMetaData(ScreenMetaData.network); @@ -126,45 +123,6 @@ class _NetworkScreenBodyState extends State void didChangeDependencies() { super.didChangeDependencies(); if (!initController()) return; - 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 (e) { - _log.shout('Could not load offline data: $e'); - } - } - - 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 diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart new file mode 100644 index 00000000000..745f931afc1 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -0,0 +1,69 @@ +// Copyright 2024 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:devtools_shared/devtools_shared.dart'; + +import '../../shared/http/http_request_data.dart'; + +/// Class to encapsulate offline data for the NetworkController. +/// It is responsible for serializing and deserializing offline network data. +class OfflineNetworkData with Serializable { + OfflineNetworkData({ + required this.requests, + this.selectedRequestId, + this.recording = false, + }); + + /// Creates an instance of [OfflineNetworkData] from a JSON map. + factory OfflineNetworkData.fromJson(Map json) { + final List requestsJson = json['requests'] ?? []; + final requests = requestsJson + .map( + (e) => DartIOHttpRequestData.fromJson( + e as Map, + null, + null, + ), + ) + .toList(); + + return OfflineNetworkData( + requests: requests, + selectedRequestId: json['selectedRequestId'] as String?, + recording: json['recording'] as bool? ?? false, + ); + } + + /// List of current [DartIOHttpRequestData] network requests. + final List requests; + + /// The ID of the currently selected request, if any. + final String? selectedRequestId; + + /// Whether the recording state is enabled. + final bool recording; + + /// Converts the current offline data to a JSON format. + @override + Map toJson() { + return { + 'requests': requests.map((request) => request.toJson()).toList(), + 'selectedRequestId': selectedRequestId, + 'recording': recording, + }; + } + + /// Serialize the instance to a JSON-encoded string. + String toJsonString() { + return jsonEncode(toJson()); + } + + /// Deserialize a JSON-encoded string to an [OfflineNetworkData] instance. + static OfflineNetworkData fromJsonString(String jsonString) { + final Map json = jsonDecode(jsonString); + return OfflineNetworkData.fromJson(json); + } +} From 857cfbe10eed208489473e5b7e596b4497520544 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Thu, 3 Oct 2024 23:28:02 +0530 Subject: [PATCH 09/36] put class name in square brackets --- .../lib/src/screens/network/offline_network_data.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index 745f931afc1..ea9803c2907 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -8,7 +8,7 @@ import 'package:devtools_shared/devtools_shared.dart'; import '../../shared/http/http_request_data.dart'; -/// Class to encapsulate offline data for the NetworkController. +/// Class to encapsulate offline data for the [NetworkController]. /// It is responsible for serializing and deserializing offline network data. class OfflineNetworkData with Serializable { OfflineNetworkData({ From 7150436a2b77f5abde8fec64b105fc5e2e362400 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Thu, 3 Oct 2024 23:36:08 +0530 Subject: [PATCH 10/36] added isNullOrEmpty check --- .../lib/src/screens/network/network_controller.dart | 2 +- .../lib/src/screens/network/offline_network_data.dart | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 548fb868f86..3137680fbdd 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -173,7 +173,7 @@ class NetworkController extends DisposableController await maybeLoadOfflineData( NetworkScreen.id, createData: (json) => OfflineNetworkData.fromJson(json), - shouldLoad: (data) => data != null, + shouldLoad: (data) => !data.isEmpty, loadData: (data) => loadOfflineData(data), ); } diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index ea9803c2907..cc2288eca1e 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:devtools_shared/devtools_shared.dart'; import '../../shared/http/http_request_data.dart'; +import '../../shared/primitives/utils.dart'; /// Class to encapsulate offline data for the [NetworkController]. /// It is responsible for serializing and deserializing offline network data. @@ -36,6 +37,7 @@ class OfflineNetworkData with Serializable { recording: json['recording'] as bool? ?? false, ); } + bool get isEmpty => requests.isNullOrEmpty; /// List of current [DartIOHttpRequestData] network requests. final List requests; From da11a679617c00e439e9dd8b7b922257a5844c57 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Fri, 4 Oct 2024 14:31:02 +0530 Subject: [PATCH 11/36] added exit offline cta, hiding other controls, removed json string --- .../src/screens/network/network_screen.dart | 99 +++++++++++-------- .../screens/network/offline_network_data.dart | 13 --- 2 files changed, 56 insertions(+), 56 deletions(-) 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 e69fc39fec0..7fbf426a9d6 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -137,7 +137,10 @@ class _NetworkScreenBodyState extends State Widget build(BuildContext context) { return Column( children: [ - _NetworkProfilerControls(controller: controller), + _NetworkProfilerControls( + controller: controller, + offline: offlineDataController.showingOfflineData.value, + ), const SizedBox(height: intermediateSpacing), Expanded( child: _NetworkProfilerBody(controller: controller), @@ -152,12 +155,15 @@ class _NetworkScreenBodyState extends State class _NetworkProfilerControls extends StatefulWidget { const _NetworkProfilerControls({ required this.controller, + required this.offline, }); static const _includeTextWidth = 810.0; final NetworkController controller; + final bool offline; + @override State<_NetworkProfilerControls> createState() => _NetworkProfilerControlsState(); @@ -201,48 +207,55 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> final hasRequests = _filteredRequests.isNotEmpty; return Row( children: [ - StartStopRecordingButton( - recording: _recording, - onPressed: () async => - await widget.controller.togglePolling(!_recording), - tooltipOverride: _recording - ? 'Stop recording network traffic' - : 'Resume recording network traffic', - minScreenWidthForTextBeforeScaling: double.infinity, - gaScreen: gac.network, - gaSelection: _recording ? gac.pause : gac.resume, - ), - const SizedBox(width: denseSpacing), - ClearButton( - minScreenWidthForTextBeforeScaling: - _NetworkProfilerControls._includeTextWidth, - gaScreen: gac.network, - gaSelection: gac.clear, - onPressed: widget.controller.clear, - ), - const SizedBox(width: defaultSpacing), - DownloadButton( - minScreenWidthForTextBeforeScaling: - _NetworkProfilerControls._includeTextWidth, - onPressed: widget.controller.exportAsHarFile, - gaScreen: gac.network, - gaSelection: gac.NetworkEvent.downloadAsHar.name, - ), - const SizedBox(width: defaultSpacing), - const Expanded(child: SizedBox()), - // TODO(kenz): fix focus issue when state is refreshed - SearchField( - searchController: widget.controller, - searchFieldEnabled: hasRequests, - searchFieldWidth: screenWidth <= MediaSize.xs - ? defaultSearchFieldWidth - : wideSearchFieldWidth, - ), - const SizedBox(width: denseSpacing), - DevToolsFilterButton( - onPressed: _showFilterDialog, - isFilterActive: _filteredRequests.length != _requests.length, - ), + if (widget.offline) + Padding( + padding: const EdgeInsets.only(right: defaultSpacing), + child: ExitOfflineButton(gaScreen: gac.cpuProfiler), + ) + else ...[ + StartStopRecordingButton( + recording: _recording, + onPressed: () async => + await widget.controller.togglePolling(!_recording), + tooltipOverride: _recording + ? 'Stop recording network traffic' + : 'Resume recording network traffic', + minScreenWidthForTextBeforeScaling: double.infinity, + gaScreen: gac.network, + gaSelection: _recording ? gac.pause : gac.resume, + ), + const SizedBox(width: denseSpacing), + ClearButton( + minScreenWidthForTextBeforeScaling: + _NetworkProfilerControls._includeTextWidth, + gaScreen: gac.network, + gaSelection: gac.clear, + onPressed: widget.controller.clear, + ), + const SizedBox(width: defaultSpacing), + DownloadButton( + minScreenWidthForTextBeforeScaling: + _NetworkProfilerControls._includeTextWidth, + onPressed: widget.controller.exportAsHarFile, + gaScreen: gac.network, + gaSelection: gac.NetworkEvent.downloadAsHar.name, + ), + const SizedBox(width: defaultSpacing), + const Expanded(child: SizedBox()), + // TODO(kenz): fix focus issue when state is refreshed + SearchField( + searchController: widget.controller, + searchFieldEnabled: hasRequests, + searchFieldWidth: screenWidth <= MediaSize.xs + ? defaultSearchFieldWidth + : wideSearchFieldWidth, + ), + const SizedBox(width: denseSpacing), + DevToolsFilterButton( + onPressed: _showFilterDialog, + isFilterActive: _filteredRequests.length != _requests.length, + ), + ], ], ); } diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index cc2288eca1e..ee030659dce 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:convert'; - import 'package:devtools_shared/devtools_shared.dart'; import '../../shared/http/http_request_data.dart'; @@ -57,15 +55,4 @@ class OfflineNetworkData with Serializable { 'recording': recording, }; } - - /// Serialize the instance to a JSON-encoded string. - String toJsonString() { - return jsonEncode(toJson()); - } - - /// Deserialize a JSON-encoded string to an [OfflineNetworkData] instance. - static OfflineNetworkData fromJsonString(String jsonString) { - final Map json = jsonDecode(jsonString); - return OfflineNetworkData.fromJson(json); - } } From 941ec886f9f15028b941c70c9e653a44bbc86294 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Fri, 4 Oct 2024 18:22:36 +0530 Subject: [PATCH 12/36] fix for request selection --- .../screens/network/network_controller.dart | 19 +++++++++++++++---- .../screens/network/offline_network_data.dart | 11 +++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) 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 3137680fbdd..7bc352d367b 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -57,9 +57,9 @@ class NetworkController extends DisposableController OfflineScreenControllerMixin, AutoDisposeControllerMixin { NetworkController() { - _initHelper(); _networkService = NetworkService(this); _currentNetworkRequests = CurrentNetworkRequests(); + _initHelper(); addAutoDisposeListener( _currentNetworkRequests, _filterAndRefreshSearchMatches, @@ -197,14 +197,25 @@ class NetworkController extends DisposableController } } - void loadOfflineData(OfflineNetworkData offlineData) { + Future loadOfflineData(OfflineNetworkData offlineData) async { + // Set filtered data to the requests available in offline data. filteredData ..clear() ..addAll(offlineData.requests); + // If a selectedRequestId is available, select it in offline mode. if (offlineData.selectedRequestId != null) { - selectedRequest.value = - _currentNetworkRequests.getRequest(offlineData.selectedRequestId!); + final selected = offlineData.getRequest(offlineData.selectedRequestId!); + if (selected != null) { + selectedRequest.value = selected; + if (selectedRequest.value is DartIOHttpRequestData) { + unawaited( + (selectedRequest.value as DartIOHttpRequestData) + .getFullRequestData(), + ); + } + resetDropDown(); + } } } diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index ee030659dce..2322b7dc691 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -6,6 +6,7 @@ import 'package:devtools_shared/devtools_shared.dart'; import '../../shared/http/http_request_data.dart'; import '../../shared/primitives/utils.dart'; +import 'network_model.dart'; /// Class to encapsulate offline data for the [NetworkController]. /// It is responsible for serializing and deserializing offline network data. @@ -40,6 +41,16 @@ class OfflineNetworkData with Serializable { /// List of current [DartIOHttpRequestData] network requests. final List requests; + /// Get a request by matching its `id` field. + // Temporarily added to check selection in the filtered requests data, + // until we have current requests data in place + NetworkRequest? getRequest(String id) { + // Search through the list of requests and return the one with the matching ID. + return requests.firstWhere( + (request) => request.id == id, + ); + } + /// The ID of the currently selected request, if any. final String? selectedRequestId; From 852b14447052204820f2be2ec1c99ef8151d9400 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Tue, 8 Oct 2024 09:09:26 +0530 Subject: [PATCH 13/36] comments resolved --- .../lib/src/screens/network/network_controller.dart | 10 ++++------ .../lib/src/screens/network/offline_network_data.dart | 7 +------ 2 files changed, 5 insertions(+), 12 deletions(-) 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 7bc352d367b..5e3d1b156f6 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -169,23 +169,21 @@ class NetworkController extends DisposableController void _initHelper() async { try { - if (offlineDataController.showingOfflineData.value == true) { + if (offlineDataController.showingOfflineData.value) { await maybeLoadOfflineData( NetworkScreen.id, createData: (json) => OfflineNetworkData.fromJson(json), shouldLoad: (data) => !data.isEmpty, loadData: (data) => loadOfflineData(data), ); - } - cancelListeners(); - if (!offlineDataController.showingOfflineData.value) { + }else{ unawaited(startRecording()); debugPrint('started recording'); addAutoDisposeListener( serviceConnection.serviceManager.isolateManager.mainIsolate, - () { + () { if (serviceConnection - .serviceManager.isolateManager.mainIsolate.value != + .serviceManager.isolateManager.mainIsolate.value != null) { unawaited(startRecording()); } diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index 2322b7dc691..3f29d64440c 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -9,12 +9,12 @@ import '../../shared/primitives/utils.dart'; import 'network_model.dart'; /// Class to encapsulate offline data for the [NetworkController]. +/// /// It is responsible for serializing and deserializing offline network data. class OfflineNetworkData with Serializable { OfflineNetworkData({ required this.requests, this.selectedRequestId, - this.recording = false, }); /// Creates an instance of [OfflineNetworkData] from a JSON map. @@ -33,7 +33,6 @@ class OfflineNetworkData with Serializable { return OfflineNetworkData( requests: requests, selectedRequestId: json['selectedRequestId'] as String?, - recording: json['recording'] as bool? ?? false, ); } bool get isEmpty => requests.isNullOrEmpty; @@ -54,16 +53,12 @@ class OfflineNetworkData with Serializable { /// The ID of the currently selected request, if any. final String? selectedRequestId; - /// Whether the recording state is enabled. - final bool recording; - /// Converts the current offline data to a JSON format. @override Map toJson() { return { 'requests': requests.map((request) => request.toJson()).toList(), 'selectedRequestId': selectedRequestId, - 'recording': recording, }; } } From 433af380e313ae495c3129ab7109adeeac2f06dd Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Tue, 8 Oct 2024 09:39:29 +0530 Subject: [PATCH 14/36] removing getFullRequestData section --- .../screens/network/network_controller.dart | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) 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 5e3d1b156f6..1ce899a305c 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -176,19 +176,8 @@ class NetworkController extends DisposableController shouldLoad: (data) => !data.isEmpty, loadData: (data) => loadOfflineData(data), ); - }else{ - unawaited(startRecording()); - debugPrint('started recording'); - addAutoDisposeListener( - serviceConnection.serviceManager.isolateManager.mainIsolate, - () { - if (serviceConnection - .serviceManager.isolateManager.mainIsolate.value != - null) { - unawaited(startRecording()); - } - }, - ); + } else { + await startRecording(); } } catch (e) { _log.shout('Could not load offline data: $e'); @@ -206,12 +195,6 @@ class NetworkController extends DisposableController final selected = offlineData.getRequest(offlineData.selectedRequestId!); if (selected != null) { selectedRequest.value = selected; - if (selectedRequest.value is DartIOHttpRequestData) { - unawaited( - (selectedRequest.value as DartIOHttpRequestData) - .getFullRequestData(), - ); - } resetDropDown(); } } From 4644b00cebfe8e214d30d55470cb1406cdc13f77 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Tue, 8 Oct 2024 15:24:13 +0530 Subject: [PATCH 15/36] using updateOrAddAll for setting data --- .../screens/network/network_controller.dart | 13 +++++++--- .../src/screens/network/network_service.dart | 26 +++++++++++++------ .../screens/network/offline_network_data.dart | 18 +++++++++++++ 3 files changed, 46 insertions(+), 11 deletions(-) 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 1ce899a305c..9f6bb67a237 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -185,10 +185,15 @@ class NetworkController extends DisposableController } Future loadOfflineData(OfflineNetworkData offlineData) async { - // Set filtered data to the requests available in offline data. - filteredData + _currentNetworkRequests ..clear() - ..addAll(offlineData.requests); + ..updateOrAddAll( + requests: offlineData.currentRequests!, + sockets: offlineData.socketStats, + timelineMicrosOffset: DateTime.now().microsecondsSinceEpoch - + (networkService.timeStamp ?? 0), + ); + _filterAndRefreshSearchMatches(); // If a selectedRequestId is available, select it in offline mode. if (offlineData.selectedRequestId != null) { @@ -433,6 +438,8 @@ class NetworkController extends DisposableController final offlineData = OfflineNetworkData( requests: filteredData.value.whereType().toList(), selectedRequestId: selectedRequest.value?.id, + currentRequests: _networkService.currentHttpRequests, + socketStats: _networkService.sockets ?? [], ); return OfflineScreenData( diff --git a/packages/devtools_app/lib/src/screens/network/network_service.dart b/packages/devtools_app/lib/src/screens/network/network_service.dart index 5e78a4d6d3e..1c70d5b48f4 100644 --- a/packages/devtools_app/lib/src/screens/network/network_service.dart +++ b/packages/devtools_app/lib/src/screens/network/network_service.dart @@ -10,8 +10,13 @@ import 'network_controller.dart'; class NetworkService { NetworkService(this.networkController); - + List? _httpRequests; + List? _sockets; final NetworkController networkController; + List? get currentHttpRequests => _httpRequests; + List? get sockets => _sockets; + int? _timeStamp; + int? get timeStamp => _timeStamp; /// Updates the last Socket data refresh time to the current time. /// @@ -56,15 +61,20 @@ class NetworkService { if (serviceConnection.serviceManager.service == null) return; final timestampObj = await serviceConnection.serviceManager.service!.getVMTimelineMicros(); - final timestamp = timestampObj.timestamp!; - final sockets = await _refreshSockets(); - networkController.lastSocketDataRefreshMicros = timestamp; - List? httpRequests; - httpRequests = await _refreshHttpProfile(); + _timeStamp = timestampObj.timestamp!; + + // Refresh socket data + _sockets ??= await _refreshSockets(); + _sockets?.addAll(await _refreshSockets()); + + // Refresh HTTP request data + _httpRequests ??= await _refreshHttpProfile(); + _httpRequests?.addAll(await _refreshHttpProfile()); + networkController.lastHttpDataRefreshTime = DateTime.now(); networkController.processNetworkTraffic( - sockets: sockets, - httpRequests: httpRequests, + sockets: _sockets ?? [], + httpRequests: _httpRequests, ); } diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index 3f29d64440c..c87af8cbd77 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -3,9 +3,11 @@ // found in the LICENSE file. import 'package:devtools_shared/devtools_shared.dart'; +import 'package:vm_service/vm_service.dart'; import '../../shared/http/http_request_data.dart'; import '../../shared/primitives/utils.dart'; +import '../network/network_controller.dart'; import 'network_model.dart'; /// Class to encapsulate offline data for the [NetworkController]. @@ -15,11 +17,16 @@ class OfflineNetworkData with Serializable { OfflineNetworkData({ required this.requests, this.selectedRequestId, + required this.currentRequests, + required this.socketStats, }); /// Creates an instance of [OfflineNetworkData] from a JSON map. factory OfflineNetworkData.fromJson(Map json) { final List requestsJson = json['requests'] ?? []; + final List? currentReqData = json['currentRequests']; + final List? socketStats = json['socketStats']; + final requests = requestsJson .map( (e) => DartIOHttpRequestData.fromJson( @@ -33,6 +40,8 @@ class OfflineNetworkData with Serializable { return OfflineNetworkData( requests: requests, selectedRequestId: json['selectedRequestId'] as String?, + currentRequests: currentReqData, + socketStats: socketStats!, ); } bool get isEmpty => requests.isNullOrEmpty; @@ -53,12 +62,21 @@ class OfflineNetworkData with Serializable { /// The ID of the currently selected request, if any. final String? selectedRequestId; + /// Current requests from network controller. + + final List? currentRequests; + + /// Socket statistics + final List socketStats; + /// Converts the current offline data to a JSON format. @override Map toJson() { return { 'requests': requests.map((request) => request.toJson()).toList(), 'selectedRequestId': selectedRequestId, + 'currentRequests': currentRequests, + 'socketStats': socketStats, }; } } From 6fd98d0d0f24f5712e021818d1bf1871afd34653 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Tue, 8 Oct 2024 15:52:19 +0530 Subject: [PATCH 16/36] Wrapped with OfflineAwareControls, removed if-else. --- .../src/screens/network/network_screen.dart | 100 +++++++++--------- 1 file changed, 48 insertions(+), 52 deletions(-) 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 7fbf426a9d6..4e56d952ebf 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -137,9 +137,12 @@ class _NetworkScreenBodyState extends State Widget build(BuildContext context) { return Column( children: [ - _NetworkProfilerControls( - controller: controller, - offline: offlineDataController.showingOfflineData.value, + OfflineAwareControls( + controlsBuilder: (_) => _NetworkProfilerControls( + controller: controller, + offline: offlineDataController.showingOfflineData.value, + ), + gaScreen: gac.network, ), const SizedBox(height: intermediateSpacing), Expanded( @@ -207,55 +210,48 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> final hasRequests = _filteredRequests.isNotEmpty; return Row( children: [ - if (widget.offline) - Padding( - padding: const EdgeInsets.only(right: defaultSpacing), - child: ExitOfflineButton(gaScreen: gac.cpuProfiler), - ) - else ...[ - StartStopRecordingButton( - recording: _recording, - onPressed: () async => - await widget.controller.togglePolling(!_recording), - tooltipOverride: _recording - ? 'Stop recording network traffic' - : 'Resume recording network traffic', - minScreenWidthForTextBeforeScaling: double.infinity, - gaScreen: gac.network, - gaSelection: _recording ? gac.pause : gac.resume, - ), - const SizedBox(width: denseSpacing), - ClearButton( - minScreenWidthForTextBeforeScaling: - _NetworkProfilerControls._includeTextWidth, - gaScreen: gac.network, - gaSelection: gac.clear, - onPressed: widget.controller.clear, - ), - const SizedBox(width: defaultSpacing), - DownloadButton( - minScreenWidthForTextBeforeScaling: - _NetworkProfilerControls._includeTextWidth, - onPressed: widget.controller.exportAsHarFile, - gaScreen: gac.network, - gaSelection: gac.NetworkEvent.downloadAsHar.name, - ), - const SizedBox(width: defaultSpacing), - const Expanded(child: SizedBox()), - // TODO(kenz): fix focus issue when state is refreshed - SearchField( - searchController: widget.controller, - searchFieldEnabled: hasRequests, - searchFieldWidth: screenWidth <= MediaSize.xs - ? defaultSearchFieldWidth - : wideSearchFieldWidth, - ), - const SizedBox(width: denseSpacing), - DevToolsFilterButton( - onPressed: _showFilterDialog, - isFilterActive: _filteredRequests.length != _requests.length, - ), - ], + StartStopRecordingButton( + recording: _recording, + onPressed: () async => + await widget.controller.togglePolling(!_recording), + tooltipOverride: _recording + ? 'Stop recording network traffic' + : 'Resume recording network traffic', + minScreenWidthForTextBeforeScaling: double.infinity, + gaScreen: gac.network, + gaSelection: _recording ? gac.pause : gac.resume, + ), + const SizedBox(width: denseSpacing), + ClearButton( + minScreenWidthForTextBeforeScaling: + _NetworkProfilerControls._includeTextWidth, + gaScreen: gac.network, + gaSelection: gac.clear, + onPressed: widget.controller.clear, + ), + const SizedBox(width: defaultSpacing), + DownloadButton( + minScreenWidthForTextBeforeScaling: + _NetworkProfilerControls._includeTextWidth, + onPressed: widget.controller.exportAsHarFile, + gaScreen: gac.network, + gaSelection: gac.NetworkEvent.downloadAsHar.name, + ), + const SizedBox(width: defaultSpacing), + const Expanded(child: SizedBox()), + // TODO(kenz): fix focus issue when state is refreshed + SearchField( + searchController: widget.controller, + searchFieldEnabled: hasRequests, + searchFieldWidth: screenWidth <= MediaSize.xs + ? defaultSearchFieldWidth + : wideSearchFieldWidth, + ), + const SizedBox(width: denseSpacing), + DevToolsFilterButton( + onPressed: _showFilterDialog, + isFilterActive: _filteredRequests.length != _requests.length, + ), ], ); } From 665ea0d360b4567510cd7ca5e8c1195a52e6e82f Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Tue, 8 Oct 2024 15:59:28 +0530 Subject: [PATCH 17/36] removed addAutoDisposeListener as we are using OfflineAwareControls --- .../devtools_app/lib/src/screens/network/network_screen.dart | 1 - 1 file changed, 1 deletion(-) 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 4e56d952ebf..6a034fe05a1 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -183,7 +183,6 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> @override void initState() { super.initState(); - addAutoDisposeListener(offlineDataController.showingOfflineData); _recording = widget.controller.recordingNotifier.value; addAutoDisposeListener(widget.controller.recordingNotifier, () { setState(() { From b6df57200bad03f88a056cee0b398e1b2e267541 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Tue, 8 Oct 2024 16:03:11 +0530 Subject: [PATCH 18/36] removed try-catch --- .../screens/network/network_controller.dart | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) 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 9f6bb67a237..b3621d16957 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -7,7 +7,6 @@ import 'dart:convert'; import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart'; import 'package:vm_service/vm_service.dart'; import '../../shared/config_specific/import_export/import_export.dart'; @@ -26,8 +25,6 @@ import 'network_screen.dart'; import 'network_service.dart'; import 'offline_network_data.dart'; -final _log = Logger('http_request_data'); - /// Different types of Network Response which can be used to visualise response /// on Response tab enum NetworkResponseViewType { @@ -168,19 +165,15 @@ class NetworkController extends DisposableController bool get isPolling => _pollingTimer != null; void _initHelper() async { - try { - if (offlineDataController.showingOfflineData.value) { - await maybeLoadOfflineData( - NetworkScreen.id, - createData: (json) => OfflineNetworkData.fromJson(json), - shouldLoad: (data) => !data.isEmpty, - loadData: (data) => loadOfflineData(data), - ); - } else { - await startRecording(); - } - } catch (e) { - _log.shout('Could not load offline data: $e'); + if (offlineDataController.showingOfflineData.value) { + await maybeLoadOfflineData( + NetworkScreen.id, + createData: (json) => OfflineNetworkData.fromJson(json), + shouldLoad: (data) => !data.isEmpty, + loadData: (data) => loadOfflineData(data), + ); + } else { + await startRecording(); } } From be63db4df0bb875d0aa81494a2701d8dcd5ca9b5 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Wed, 9 Oct 2024 18:05:49 +0530 Subject: [PATCH 19/36] code refactoring --- .../screens/network/network_controller.dart | 3 +- .../src/screens/network/network_screen.dart | 65 ++++++++++--------- 2 files changed, 34 insertions(+), 34 deletions(-) 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 b3621d16957..6141ce3881f 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -183,8 +183,7 @@ class NetworkController extends DisposableController ..updateOrAddAll( requests: offlineData.currentRequests!, sockets: offlineData.socketStats, - timelineMicrosOffset: DateTime.now().microsecondsSinceEpoch - - (networkService.timeStamp ?? 0), + timelineMicrosOffset: DateTime.now().microsecondsSinceEpoch, ); _filterAndRefreshSearchMatches(); 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 6a034fe05a1..3aea1482438 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -14,7 +14,6 @@ import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/common_widgets.dart'; import '../../shared/config_specific/copy_to_clipboard/copy_to_clipboard.dart'; -import '../../shared/globals.dart'; import '../../shared/http/curl_command.dart'; import '../../shared/http/http_request_data.dart'; import '../../shared/primitives/utils.dart'; @@ -138,9 +137,9 @@ class _NetworkScreenBodyState extends State return Column( children: [ OfflineAwareControls( - controlsBuilder: (_) => _NetworkProfilerControls( + controlsBuilder: (offline) => _NetworkProfilerControls( controller: controller, - offline: offlineDataController.showingOfflineData.value, + offline: offline, ), gaScreen: gac.network, ), @@ -209,35 +208,37 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> final hasRequests = _filteredRequests.isNotEmpty; return Row( children: [ - StartStopRecordingButton( - recording: _recording, - onPressed: () async => - await widget.controller.togglePolling(!_recording), - tooltipOverride: _recording - ? 'Stop recording network traffic' - : 'Resume recording network traffic', - minScreenWidthForTextBeforeScaling: double.infinity, - gaScreen: gac.network, - gaSelection: _recording ? gac.pause : gac.resume, - ), - const SizedBox(width: denseSpacing), - ClearButton( - minScreenWidthForTextBeforeScaling: - _NetworkProfilerControls._includeTextWidth, - gaScreen: gac.network, - gaSelection: gac.clear, - onPressed: widget.controller.clear, - ), - const SizedBox(width: defaultSpacing), - DownloadButton( - minScreenWidthForTextBeforeScaling: - _NetworkProfilerControls._includeTextWidth, - onPressed: widget.controller.exportAsHarFile, - gaScreen: gac.network, - gaSelection: gac.NetworkEvent.downloadAsHar.name, - ), - const SizedBox(width: defaultSpacing), - const Expanded(child: SizedBox()), + if (!widget.offline) ...[ + StartStopRecordingButton( + recording: _recording, + onPressed: () async => + await widget.controller.togglePolling(!_recording), + tooltipOverride: _recording + ? 'Stop recording network traffic' + : 'Resume recording network traffic', + minScreenWidthForTextBeforeScaling: double.infinity, + gaScreen: gac.network, + gaSelection: _recording ? gac.pause : gac.resume, + ), + const SizedBox(width: denseSpacing), + ClearButton( + minScreenWidthForTextBeforeScaling: + _NetworkProfilerControls._includeTextWidth, + gaScreen: gac.network, + gaSelection: gac.clear, + onPressed: widget.controller.clear, + ), + const SizedBox(width: defaultSpacing), + DownloadButton( + minScreenWidthForTextBeforeScaling: + _NetworkProfilerControls._includeTextWidth, + onPressed: widget.controller.exportAsHarFile, + gaScreen: gac.network, + gaSelection: gac.NetworkEvent.downloadAsHar.name, + ), + const SizedBox(width: defaultSpacing), + ], + const Spacer(), // TODO(kenz): fix focus issue when state is refreshed SearchField( searchController: widget.controller, From 91505b59c57380961a6178b402342f788c5a320d Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Wed, 9 Oct 2024 18:21:21 +0530 Subject: [PATCH 20/36] added enum --- .../screens/network/offline_network_data.dart | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index c87af8cbd77..7d9759d02c5 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -23,9 +23,12 @@ class OfflineNetworkData with Serializable { /// Creates an instance of [OfflineNetworkData] from a JSON map. factory OfflineNetworkData.fromJson(Map json) { - final List requestsJson = json['requests'] ?? []; - final List? currentReqData = json['currentRequests']; - final List? socketStats = json['socketStats']; + final List requestsJson = + json[OfflineDataKeys.requests.name] ?? []; + final List? currentReqData = + json[OfflineDataKeys.currentRequests.name]; + final List? socketStats = + json[OfflineDataKeys.socketStats.name]; final requests = requestsJson .map( @@ -39,7 +42,8 @@ class OfflineNetworkData with Serializable { return OfflineNetworkData( requests: requests, - selectedRequestId: json['selectedRequestId'] as String?, + selectedRequestId: + json[OfflineDataKeys.selectedRequestId.name] as String?, currentRequests: currentReqData, socketStats: socketStats!, ); @@ -73,10 +77,18 @@ class OfflineNetworkData with Serializable { @override Map toJson() { return { - 'requests': requests.map((request) => request.toJson()).toList(), - 'selectedRequestId': selectedRequestId, - 'currentRequests': currentRequests, - 'socketStats': socketStats, + OfflineDataKeys.requests.name: + requests.map((request) => request.toJson()).toList(), + OfflineDataKeys.selectedRequestId.name: selectedRequestId, + OfflineDataKeys.currentRequests.name: currentRequests, + OfflineDataKeys.socketStats.name: socketStats, }; } } + +enum OfflineDataKeys { + requests, + selectedRequestId, + currentRequests, + socketStats, +} From 071537909b3fe6a7ebb0a1c4f9e7bb36906b2201 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Thu, 10 Oct 2024 22:55:14 +0530 Subject: [PATCH 21/36] code refactoring, reverting changes on network service --- .../screens/network/network_controller.dart | 28 ++++++-- .../src/screens/network/network_model.dart | 64 ++++++++++++++++++- .../src/screens/network/network_service.dart | 26 +++----- .../screens/network/offline_network_data.dart | 62 +++++------------- .../src/shared/http/http_request_data.dart | 13 +++- 5 files changed, 119 insertions(+), 74 deletions(-) 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 6141ce3881f..4500723e7e8 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -178,18 +178,25 @@ class NetworkController extends DisposableController } Future loadOfflineData(OfflineNetworkData offlineData) async { + List httpProfileData = []; + List socketStatsData = []; + + httpProfileData = offlineData.httpRequestData.mapToHttpProfileRequests; + socketStatsData = offlineData.socketData.mapToSocketStatistics; + _currentNetworkRequests ..clear() ..updateOrAddAll( - requests: offlineData.currentRequests!, - sockets: offlineData.socketStats, + requests: httpProfileData, + sockets: socketStatsData, timelineMicrosOffset: DateTime.now().microsecondsSinceEpoch, ); _filterAndRefreshSearchMatches(); // If a selectedRequestId is available, select it in offline mode. if (offlineData.selectedRequestId != null) { - final selected = offlineData.getRequest(offlineData.selectedRequestId!); + final selected = _currentNetworkRequests + .getRequest(offlineData.selectedRequestId ?? ''); if (selected != null) { selectedRequest.value = selected; resetDropDown(); @@ -427,11 +434,20 @@ class NetworkController extends DisposableController @override OfflineScreenData prepareOfflineScreenData() { + final httpRequestData = []; + final socketData = []; + for (final request in _currentNetworkRequests.value) { + if (request is DartIOHttpRequestData) { + httpRequestData.add(request); + } else if (request is Socket) { + socketData.add(request); + } + } + final offlineData = OfflineNetworkData( - requests: filteredData.value.whereType().toList(), + httpRequestData: httpRequestData, + socketData: socketData, selectedRequestId: selectedRequest.value?.id, - currentRequests: _networkService.currentHttpRequests, - socketStats: _networkService.sockets ?? [], ); return OfflineScreenData( diff --git a/packages/devtools_app/lib/src/screens/network/network_model.dart b/packages/devtools_app/lib/src/screens/network/network_model.dart index 64e7ba1f4a2..a2eebb1c247 100644 --- a/packages/devtools_app/lib/src/screens/network/network_model.dart +++ b/packages/devtools_app/lib/src/screens/network/network_model.dart @@ -2,13 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:devtools_shared/devtools_shared.dart'; import 'package:flutter/material.dart'; import 'package:vm_service/vm_service.dart'; import '../../shared/primitives/utils.dart'; import '../../shared/ui/search.dart'; -abstract class NetworkRequest with ChangeNotifier, SearchableDataMixin { +abstract class NetworkRequest + with ChangeNotifier, SearchableDataMixin, Serializable { String get method; String get uri; @@ -180,4 +182,64 @@ class Socket extends NetworkRequest { @override int get hashCode => id.hashCode; + + SocketStatistic get socketData => _socket; + + static Socket fromJson(Map json) { + return Socket( + SocketStatistic.parse(json['socket'] as Map)!, + json['timelineMicrosBase'] as int, + ); + } + + @override + Map toJson() { + return { + 'timelineMicrosBase': _timelineMicrosBase, + 'socket': _socket.toJson(), + }; + } +} + +extension on SocketStatistic { + Map toJson() { + return { + 'id': id, + 'startTime': startTime, + 'endTime': endTime, + //TODO verify if these timings are in correct format + 'lastReadTime': lastReadTime, + 'lastWriteTime': lastWriteTime, + 'socketType': socketType, + 'address': address, + 'port': port, + 'readBytes': readBytes, + 'writeBytes': writeBytes, + }; + } +} + +enum SocketJsonKey { + id, + method, + uri, + contentType, + type, + port, + status, + duration, + startTimestamp, + endTimestamp, + lastReadTimestamp, + lastWriteTimestamp, + didFail, + readBytes, + writeBytes, + inProgress, +} + +extension SocketExtension on List { + List get mapToSocketStatistics { + return map((socket) => socket._socket).toList(); + } } diff --git a/packages/devtools_app/lib/src/screens/network/network_service.dart b/packages/devtools_app/lib/src/screens/network/network_service.dart index 1c70d5b48f4..5e78a4d6d3e 100644 --- a/packages/devtools_app/lib/src/screens/network/network_service.dart +++ b/packages/devtools_app/lib/src/screens/network/network_service.dart @@ -10,13 +10,8 @@ import 'network_controller.dart'; class NetworkService { NetworkService(this.networkController); - List? _httpRequests; - List? _sockets; + final NetworkController networkController; - List? get currentHttpRequests => _httpRequests; - List? get sockets => _sockets; - int? _timeStamp; - int? get timeStamp => _timeStamp; /// Updates the last Socket data refresh time to the current time. /// @@ -61,20 +56,15 @@ class NetworkService { if (serviceConnection.serviceManager.service == null) return; final timestampObj = await serviceConnection.serviceManager.service!.getVMTimelineMicros(); - _timeStamp = timestampObj.timestamp!; - - // Refresh socket data - _sockets ??= await _refreshSockets(); - _sockets?.addAll(await _refreshSockets()); - - // Refresh HTTP request data - _httpRequests ??= await _refreshHttpProfile(); - _httpRequests?.addAll(await _refreshHttpProfile()); - + final timestamp = timestampObj.timestamp!; + final sockets = await _refreshSockets(); + networkController.lastSocketDataRefreshMicros = timestamp; + List? httpRequests; + httpRequests = await _refreshHttpProfile(); networkController.lastHttpDataRefreshTime = DateTime.now(); networkController.processNetworkTraffic( - sockets: _sockets ?? [], - httpRequests: _httpRequests, + sockets: sockets, + httpRequests: httpRequests, ); } diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index 7d9759d02c5..16667615ece 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:devtools_shared/devtools_shared.dart'; -import 'package:vm_service/vm_service.dart'; import '../../shared/http/http_request_data.dart'; import '../../shared/primitives/utils.dart'; @@ -15,80 +14,49 @@ import 'network_model.dart'; /// It is responsible for serializing and deserializing offline network data. class OfflineNetworkData with Serializable { OfflineNetworkData({ - required this.requests, + required this.httpRequestData, this.selectedRequestId, - required this.currentRequests, - required this.socketStats, + required this.socketData, }); /// Creates an instance of [OfflineNetworkData] from a JSON map. factory OfflineNetworkData.fromJson(Map json) { - final List requestsJson = - json[OfflineDataKeys.requests.name] ?? []; - final List? currentReqData = - json[OfflineDataKeys.currentRequests.name]; - final List? socketStats = - json[OfflineDataKeys.socketStats.name]; - - final requests = requestsJson - .map( - (e) => DartIOHttpRequestData.fromJson( - e as Map, - null, - null, - ), - ) - .toList(); + final httpRequestData = json[OfflineDataKeys.httpRequestData.name] + as List; + List? socketReqData = []; + socketReqData = json[OfflineDataKeys.socketData.name] as List; return OfflineNetworkData( - requests: requests, + httpRequestData: httpRequestData, selectedRequestId: json[OfflineDataKeys.selectedRequestId.name] as String?, - currentRequests: currentReqData, - socketStats: socketStats!, + socketData: socketReqData, ); } - bool get isEmpty => requests.isNullOrEmpty; + bool get isEmpty => httpRequestData.isNullOrEmpty; /// List of current [DartIOHttpRequestData] network requests. - final List requests; - - /// Get a request by matching its `id` field. - // Temporarily added to check selection in the filtered requests data, - // until we have current requests data in place - NetworkRequest? getRequest(String id) { - // Search through the list of requests and return the one with the matching ID. - return requests.firstWhere( - (request) => request.id == id, - ); - } + final List httpRequestData; /// The ID of the currently selected request, if any. final String? selectedRequestId; - /// Current requests from network controller. - - final List? currentRequests; - /// Socket statistics - final List socketStats; + final List socketData; /// Converts the current offline data to a JSON format. @override Map toJson() { return { - OfflineDataKeys.requests.name: - requests.map((request) => request.toJson()).toList(), + OfflineDataKeys.httpRequestData.name: httpRequestData, OfflineDataKeys.selectedRequestId.name: selectedRequestId, - OfflineDataKeys.currentRequests.name: currentRequests, - OfflineDataKeys.socketStats.name: socketStats, + OfflineDataKeys.socketData.name: socketData, }; } } enum OfflineDataKeys { - requests, + httpRequestData, selectedRequestId, - currentRequests, - socketStats, + socketData, } 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 6abdd42f0dc..a68a73aedc5 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 @@ -63,7 +63,8 @@ class DartIOHttpRequestData extends NetworkRequest { requestPostData?[HttpRequestDataKeys.text.name].toString(); } //TODO go through all parameters, to check if they are correctly added. - Map? toJson() { + @override + Map toJson() { try { return { HttpRequestDataKeys.startedDateTime.name: @@ -173,7 +174,7 @@ class DartIOHttpRequestData extends NetworkRequest { } catch (e) { _log.shout('Error in toJson: $e'); } - return null; + return {}; } static const _connectionInfoKey = 'connectionInfo'; @@ -453,3 +454,11 @@ class DartIOHttpRequestData extends NetworkRequest { startTimestamp, ); } + +extension HttpRequestExtension on List { + List get mapToHttpProfileRequests { + return map( + (httpRequestData) => httpRequestData._request as HttpProfileRequest, + ).toList(); + } +} From ade8b5fb47d15d3c5c8a2f24687324a932d2336b Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Thu, 10 Oct 2024 23:40:29 +0530 Subject: [PATCH 22/36] using values from enum --- .../src/screens/network/network_model.dart | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/network_model.dart b/packages/devtools_app/lib/src/screens/network/network_model.dart index a2eebb1c247..53fc4b6e398 100644 --- a/packages/devtools_app/lib/src/screens/network/network_model.dart +++ b/packages/devtools_app/lib/src/screens/network/network_model.dart @@ -195,8 +195,8 @@ class Socket extends NetworkRequest { @override Map toJson() { return { - 'timelineMicrosBase': _timelineMicrosBase, - 'socket': _socket.toJson(), + SocketJsonKey.timelineMicrosBase.name: _timelineMicrosBase, + SocketJsonKey.socket.name: _socket.toJson(), }; } } @@ -204,38 +204,34 @@ class Socket extends NetworkRequest { extension on SocketStatistic { Map toJson() { return { - 'id': id, - 'startTime': startTime, - 'endTime': endTime, + SocketJsonKey.id.name: id, + SocketJsonKey.startTime.name: startTime, + SocketJsonKey.endTime.name: endTime, //TODO verify if these timings are in correct format - 'lastReadTime': lastReadTime, - 'lastWriteTime': lastWriteTime, - 'socketType': socketType, - 'address': address, - 'port': port, - 'readBytes': readBytes, - 'writeBytes': writeBytes, + SocketJsonKey.lastReadTime.name: lastReadTime, + SocketJsonKey.lastWriteTime.name: lastWriteTime, + SocketJsonKey.socketType.name: socketType, + SocketJsonKey.address.name: address, + SocketJsonKey.port.name: port, + SocketJsonKey.readBytes.name: readBytes, + SocketJsonKey.writeBytes.name: writeBytes, }; } } enum SocketJsonKey { id, - method, - uri, - contentType, - type, + startTime, + endTime, + lastReadTime, + lastWriteTime, + socketType, + address, port, - status, - duration, - startTimestamp, - endTimestamp, - lastReadTimestamp, - lastWriteTimestamp, - didFail, readBytes, writeBytes, - inProgress, + timelineMicrosBase, + socket, } extension SocketExtension on List { From f21a4f638fdcdb2bf4c965d3924a1f405b458d8e Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Wed, 23 Oct 2024 20:22:02 +0530 Subject: [PATCH 23/36] delegating toJson using extension methods, code refactoring --- .../screens/network/network_controller.dart | 7 +- .../screens/network/offline_network_data.dart | 34 ++- .../lib/src/shared/http/constants.dart | 9 +- .../src/shared/http/http_request_data.dart | 214 ++++++++---------- .../devtools_app/lib/src/shared/screen.dart | 2 + 5 files changed, 133 insertions(+), 133 deletions(-) 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 4500723e7e8..af3ca51550a 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -178,11 +178,8 @@ class NetworkController extends DisposableController } Future loadOfflineData(OfflineNetworkData offlineData) async { - List httpProfileData = []; - List socketStatsData = []; - - httpProfileData = offlineData.httpRequestData.mapToHttpProfileRequests; - socketStatsData = offlineData.socketData.mapToSocketStatistics; + final httpProfileData = offlineData.httpRequestData.mapToHttpProfileRequests; + final socketStatsData = offlineData.socketData.mapToSocketStatistics; _currentNetworkRequests ..clear() diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index 16667615ece..04e49c226e3 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -20,20 +20,35 @@ class OfflineNetworkData with Serializable { }); /// Creates an instance of [OfflineNetworkData] from a JSON map. - factory OfflineNetworkData.fromJson(Map json) { - final httpRequestData = json[OfflineDataKeys.httpRequestData.name] - as List; - List? socketReqData = []; - socketReqData = json[OfflineDataKeys.socketData.name] as List; + factory OfflineNetworkData.fromJson(Map json) { + final httpRequestJsonList = + json[OfflineDataKeys.httpRequestData.name] as List?; + final httpRequestData = httpRequestJsonList + ?.map((e) { + if (e is Map) { + final requestData = e['request'] as Map?; + return requestData != null + ? DartIOHttpRequestData.fromJson(requestData, null, null) + : null; + } + return null; + }) + .whereType() + .toList() ?? + []; + + final socketRequestData = + (json[OfflineDataKeys.socketData.name] as List?)?.cast(); return OfflineNetworkData( httpRequestData: httpRequestData, selectedRequestId: json[OfflineDataKeys.selectedRequestId.name] as String?, - socketData: socketReqData, + socketData: socketRequestData ?? [], ); } - bool get isEmpty => httpRequestData.isNullOrEmpty; + + bool get isEmpty => httpRequestData.isNullOrEmpty && socketData.isNullOrEmpty; /// List of current [DartIOHttpRequestData] network requests. final List httpRequestData; @@ -46,9 +61,10 @@ class OfflineNetworkData with Serializable { /// Converts the current offline data to a JSON format. @override - Map toJson() { + Map toJson() { return { - OfflineDataKeys.httpRequestData.name: httpRequestData, + OfflineDataKeys.httpRequestData.name: + httpRequestData.map((e) => e.toJson()).toList(), OfflineDataKeys.selectedRequestId.name: selectedRequestId, OfflineDataKeys.socketData.name: socketData, }; diff --git a/packages/devtools_app/lib/src/shared/http/constants.dart b/packages/devtools_app/lib/src/shared/http/constants.dart index c3af56e515d..3ed1190b561 100644 --- a/packages/devtools_app/lib/src/shared/http/constants.dart +++ b/packages/devtools_app/lib/src/shared/http/constants.dart @@ -58,7 +58,14 @@ enum HttpRequestDataKeys { queryParameters, content, size, - connectionId + connectionId, + requestBody, + responseBody, + endTime, + arguments, + host, + username, + isDirect } enum HttpRequestDataValues { 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 a68a73aedc5..e49b33e1e53 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 @@ -11,7 +11,6 @@ import 'package:mime/mime.dart'; import 'package:vm_service/vm_service.dart'; import '../../screens/network/network_model.dart'; -import '../../screens/network/utils/http_utils.dart'; import '../globals.dart'; import '../primitives/utils.dart'; import 'constants.dart'; @@ -53,128 +52,33 @@ class DartIOHttpRequestData extends NetworkRequest { Map? requestPostData, Map? responseContent, ) { + final isFullRequest = modifiedRequestData + .containsKey(HttpRequestDataKeys.requestBody.name) && + modifiedRequestData.containsKey(HttpRequestDataKeys.responseBody.name); + + final parsedRequest = isFullRequest + ? HttpProfileRequest.parse(modifiedRequestData) + : HttpProfileRequestRef.parse(modifiedRequestData); + + final responseBody = + responseContent?[HttpRequestDataKeys.text.name]?.toString(); + final requestBody = + requestPostData?[HttpRequestDataKeys.text.name]?.toString(); + return DartIOHttpRequestData( - HttpProfileRequestRef.parse(modifiedRequestData)!, - requestFullDataFromVmService: false, + parsedRequest!, + requestFullDataFromVmService: parsedRequest is! HttpProfileRequest, ) - .._responseBody = - responseContent?[HttpRequestDataKeys.text.name].toString() - .._requestBody = - requestPostData?[HttpRequestDataKeys.text.name].toString(); + .._responseBody = responseBody + .._requestBody = requestBody; } - //TODO go through all parameters, to check if they are correctly added. + @override Map toJson() { - try { - return { - HttpRequestDataKeys.startedDateTime.name: - startTimestamp.toIso8601String(), - HttpRequestDataKeys.time.name: duration?.inMilliseconds ?? 0, - HttpRequestDataKeys.request.name: { - HttpRequestDataKeys.method.name: method, - HttpRequestDataKeys.url.name: uri, - HttpRequestDataKeys.httpVersion.name: - HttpRequestDataDefaults.httpVersion, - 'cookies': requestCookies.map((cookie) => cookie.toString()).toList(), - HttpRequestDataKeys.headers.name: requestHeaders, - HttpRequestDataKeys.queryString.name: queryParameters?.entries - .map((entry) => {'name': entry.key, 'value': entry.value}) - .toList(), - 'queryParameters': queryParameters?.entries - .map((entry) => {'name': entry.key, 'value': entry.value}) - .toList(), - HttpRequestDataKeys.postData.name: { - HttpRequestDataKeys.mimeType.name: contentType, - HttpRequestDataKeys.text.name: - requestBody ?? HttpRequestDataDefaults.none, - }, - HttpRequestDataKeys.headersSize.name: - requestHeaders != null ? calculateHeadersSize(requestHeaders) : 0, - HttpRequestDataKeys.bodySize.name: requestBody?.length ?? 0, - HttpRequestDataKeys.connectionInfo.name: { - HttpRequestDataKeys.remoteAddress.name: - (general[HttpRequestDataKeys.connectionInfo.name] as Map?)?[HttpRequestDataKeys.remoteAddress.name], - HttpRequestDataKeys.localPort.name: (general[ - HttpRequestDataKeys.connectionInfo.name] - as Map?)?[HttpRequestDataKeys.localPort.name], - }, - HttpRequestDataKeys.contentLength.name: - general[HttpRequestDataKeys.contentLength.name] ?? 0, - HttpRequestDataKeys.followRedirects.name: - _request.request?.followRedirects ?? true, - HttpRequestDataKeys.maxRedirects.name: - _request.request?.maxRedirects ?? 5, - HttpRequestDataKeys.persistentConnection.name: - _request.request?.persistentConnection ?? true, - HttpRequestDataKeys.proxyDetails.name: { - HttpRequestDataKeys.proxy.name: - (general[HttpRequestDataKeys.proxyDetails.name] - as Map?)?[HttpRequestDataKeys.proxy.name], - HttpRequestDataKeys.type.name: - (general[HttpRequestDataKeys.proxyDetails.name] - as Map?)?[HttpRequestDataKeys.type.name], - }, - HttpRequestDataKeys.error.name: - general[HttpRequestDataKeys.error.name], - }, - HttpRequestDataKeys.response.name: { - HttpRequestDataKeys.status.name: - status ?? HttpRequestDataDefaults.error, - HttpRequestDataKeys.statusCode.name: _request.response!.statusCode, - HttpRequestDataKeys.statusText.name: '', - HttpRequestDataKeys.httpVersion.name: - HttpRequestDataDefaults.httpVersion, - HttpRequestDataKeys.cookies.name: - responseCookies.map((cookie) => cookie.toString()).toList(), - HttpRequestDataKeys.headers.name: responseHeaders, - HttpRequestDataKeys.content.name: { - HttpRequestDataKeys.size.name: responseBody?.length ?? 0, - HttpRequestDataKeys.mimeType.name: - contentType ?? HttpRequestDataValues.json.name, - HttpRequestDataKeys.text.name: responseBody ?? '', - }, - HttpRequestDataKeys.redirects.name: _request.response!.redirects, - //TODO : add redirectURL - HttpRequestDataKeys.redirectURL.name: '', - HttpRequestDataKeys.headersSize.name: responseHeaders != null - ? calculateHeadersSize(responseHeaders) - : 0, - HttpRequestDataKeys.bodySize.name: encodedResponse?.length ?? 0, - }, - HttpRequestDataKeys.cache.name: {}, - HttpRequestDataKeys.connection.name: - (general[HttpRequestDataKeys.connectionInfo.name] as Map?)?[HttpRequestDataKeys.connectionId.name] ?? - '', - //Remove if not required - HttpRequestDataKeys.comment.name: '', - HttpRequestDataKeys.isolateId.name: _request.isolateId, - HttpRequestDataKeys.type.name: - HttpRequestDataDefaults.httpProfileRequest, - HttpRequestDataKeys.method.name: method, - HttpRequestDataKeys.uri.name: uri, - HttpRequestDataKeys.id.name: id, - HttpRequestDataKeys.startTime.name: - startTimestamp.microsecondsSinceEpoch, - HttpRequestDataKeys.events.name: instantEvents - .map( - (event) => { - HttpRequestDataKeys.timestamp.name: - event.timestamp.microsecondsSinceEpoch, - HttpRequestDataKeys.event.name: event.name, - }, - ) - .toList(), - HttpRequestDataKeys.isRedirect.name: - _request.response?.isRedirect ?? false, - HttpRequestDataKeys.reasonPhrase.name: - _request.response?.reasonPhrase ?? '', - }; - } catch (e) { - _log.shout('Error in toJson: $e'); - } - return {}; + return { + HttpRequestDataKeys.request.name: + (_request as HttpProfileRequest).toJson(), + }; } static const _connectionInfoKey = 'connectionInfo'; @@ -462,3 +366,77 @@ extension HttpRequestExtension on List { ).toList(); } } + +extension HttpProfileRequestExtension on HttpProfileRequest { + Map toJson() { + return { + HttpRequestDataKeys.id.name: id, + HttpRequestDataKeys.method.name: method, + HttpRequestDataKeys.uri.name: uri.toString(), + HttpRequestDataKeys.startTime.name: startTime.microsecondsSinceEpoch, + HttpRequestDataKeys.endTime.name: endTime?.microsecondsSinceEpoch, + HttpRequestDataKeys.response.name: response?.toJson(), + HttpRequestDataKeys.request.name: request?.toJson(), + HttpRequestDataKeys.isolateId.name: isolateId, + HttpRequestDataKeys.events.name: events.map((e) => e.toJson()).toList(), + HttpRequestDataKeys.requestBody.name: requestBody?.toList(), + HttpRequestDataKeys.responseBody.name: responseBody?.toList(), + }; + } +} + +extension HttpProfileRequestDataExtension on HttpProfileRequestData { + Map toJson() { + return { + HttpRequestDataKeys.headers.name: headers, + HttpRequestDataKeys.followRedirects.name: followRedirects, + HttpRequestDataKeys.maxRedirects.name: maxRedirects, + HttpRequestDataKeys.connectionInfo.name: connectionInfo, + HttpRequestDataKeys.contentLength.name: contentLength, + HttpRequestDataKeys.cookies.name: cookies, + HttpRequestDataKeys.persistentConnection.name: persistentConnection, + HttpRequestDataKeys.proxyDetails.name: proxyDetails, + }; + } +} + +extension HttpProfileResponseDataExtension on HttpProfileResponseData { + Map toJson() { + return { + HttpRequestDataKeys.startTime.name: startTime?.microsecondsSinceEpoch, + HttpRequestDataKeys.endTime.name: endTime?.microsecondsSinceEpoch, + HttpRequestDataKeys.headers.name: headers, + HttpRequestDataKeys.compressionState.name: compressionState, + HttpRequestDataKeys.connectionInfo.name: connectionInfo, + HttpRequestDataKeys.contentLength.name: contentLength, + HttpRequestDataKeys.cookies.name: cookies, + HttpRequestDataKeys.isRedirect.name: isRedirect, + HttpRequestDataKeys.persistentConnection.name: persistentConnection, + HttpRequestDataKeys.reasonPhrase.name: reasonPhrase, + HttpRequestDataKeys.redirects.name: redirects, + HttpRequestDataKeys.statusCode.name: statusCode, + HttpRequestDataKeys.error.name: error, + }; + } +} + +extension HttpProfileRequestEventExtension on HttpProfileRequestEvent { + Map toJson() { + return { + HttpRequestDataKeys.event.name: event, + HttpRequestDataKeys.timestamp.name: timestamp.microsecondsSinceEpoch, + HttpRequestDataKeys.arguments.name: arguments, + }; + } +} + +extension HttpProfileProxyDataExtension on HttpProfileProxyData { + Map toJson() { + return { + HttpRequestDataKeys.host.name: host, + HttpRequestDataKeys.username.name: username, + HttpRequestDataKeys.isDirect.name: isDirect, + HttpRequestDataKeys.host.name: port, + }; + } +} diff --git a/packages/devtools_app/lib/src/shared/screen.dart b/packages/devtools_app/lib/src/shared/screen.dart index 3dd5ca9b1fe..c0dbf4c13eb 100644 --- a/packages/devtools_app/lib/src/shared/screen.dart +++ b/packages/devtools_app/lib/src/shared/screen.dart @@ -77,6 +77,8 @@ enum ScreenMetaData { requiresDartVm: true, tutorialVideoTimestamp: '?t=547', // ignore: avoid_redundant_argument_values, false positive + requiresConnection: FeatureFlags.networkOffline, + // ignore: avoid_redundant_argument_values, false positive worksWithOfflineData: FeatureFlags.networkOffline, ), logging( From 454ca9223d5d202f7348bb3ddf03976c6f770b15 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Sun, 27 Oct 2024 19:57:47 +0530 Subject: [PATCH 24/36] socket data de-serialisation changes --- .../screens/network/offline_network_data.dart | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index 04e49c226e3..323caee0867 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -24,10 +24,12 @@ class OfflineNetworkData with Serializable { final httpRequestJsonList = json[OfflineDataKeys.httpRequestData.name] as List?; + // Deserialize httpRequestData final httpRequestData = httpRequestJsonList ?.map((e) { if (e is Map) { - final requestData = e['request'] as Map?; + final requestData = + e[OfflineDataKeys.request.name] as Map?; return requestData != null ? DartIOHttpRequestData.fromJson(requestData, null, null) : null; @@ -38,13 +40,25 @@ class OfflineNetworkData with Serializable { .toList() ?? []; - final socketRequestData = - (json[OfflineDataKeys.socketData.name] as List?)?.cast(); + // Deserialize socketData + final socketJsonList = + json[OfflineDataKeys.socketData.name] as List?; + final socketData = socketJsonList + ?.map((e) { + if (e is Map) { + return Socket.fromJson(e); + } + return null; + }) + .whereType() + .toList() ?? + []; + return OfflineNetworkData( httpRequestData: httpRequestData, selectedRequestId: json[OfflineDataKeys.selectedRequestId.name] as String?, - socketData: socketRequestData ?? [], + socketData: socketData, ); } @@ -67,6 +81,8 @@ class OfflineNetworkData with Serializable { httpRequestData.map((e) => e.toJson()).toList(), OfflineDataKeys.selectedRequestId.name: selectedRequestId, OfflineDataKeys.socketData.name: socketData, + OfflineDataKeys.socketData.name: + socketData.map((e) => e.toJson()).toList(), }; } } @@ -75,4 +91,5 @@ enum OfflineDataKeys { httpRequestData, selectedRequestId, socketData, + request, } From ac726e8d9b446f4fcc8e555ca28b000417d4fe15 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 4 Nov 2024 13:21:41 +0530 Subject: [PATCH 25/36] used factory constructor, made enum private --- .../src/screens/network/network_model.dart | 16 +++++++++------- .../screens/network/offline_network_data.dart | 19 +++++++++---------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/network_model.dart b/packages/devtools_app/lib/src/screens/network/network_model.dart index 53fc4b6e398..b2664baafe3 100644 --- a/packages/devtools_app/lib/src/screens/network/network_model.dart +++ b/packages/devtools_app/lib/src/screens/network/network_model.dart @@ -87,6 +87,15 @@ abstract class NetworkRequest class Socket extends NetworkRequest { Socket(this._socket, this._timelineMicrosBase); + factory Socket.fromJson(Map json) { + return Socket( + SocketStatistic.parse( + json[SocketJsonKey.socket.name] as Map, + )!, + json[SocketJsonKey.timelineMicrosBase.name] as int, + ); + } + int _timelineMicrosBase; SocketStatistic _socket; @@ -185,13 +194,6 @@ class Socket extends NetworkRequest { SocketStatistic get socketData => _socket; - static Socket fromJson(Map json) { - return Socket( - SocketStatistic.parse(json['socket'] as Map)!, - json['timelineMicrosBase'] as int, - ); - } - @override Map toJson() { return { diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index 323caee0867..a54a21ae6f8 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -22,14 +22,14 @@ class OfflineNetworkData with Serializable { /// Creates an instance of [OfflineNetworkData] from a JSON map. factory OfflineNetworkData.fromJson(Map json) { final httpRequestJsonList = - json[OfflineDataKeys.httpRequestData.name] as List?; + json[_OfflineDataKeys.httpRequestData.name] as List?; // Deserialize httpRequestData final httpRequestData = httpRequestJsonList ?.map((e) { if (e is Map) { final requestData = - e[OfflineDataKeys.request.name] as Map?; + e[_OfflineDataKeys.request.name] as Map?; return requestData != null ? DartIOHttpRequestData.fromJson(requestData, null, null) : null; @@ -42,7 +42,7 @@ class OfflineNetworkData with Serializable { // Deserialize socketData final socketJsonList = - json[OfflineDataKeys.socketData.name] as List?; + json[_OfflineDataKeys.socketData.name] as List?; final socketData = socketJsonList ?.map((e) { if (e is Map) { @@ -57,7 +57,7 @@ class OfflineNetworkData with Serializable { return OfflineNetworkData( httpRequestData: httpRequestData, selectedRequestId: - json[OfflineDataKeys.selectedRequestId.name] as String?, + json[_OfflineDataKeys.selectedRequestId.name] as String?, socketData: socketData, ); } @@ -70,24 +70,23 @@ class OfflineNetworkData with Serializable { /// The ID of the currently selected request, if any. final String? selectedRequestId; - /// Socket statistics + /// The list of socket statistics for the offline network data. final List socketData; /// Converts the current offline data to a JSON format. @override Map toJson() { return { - OfflineDataKeys.httpRequestData.name: + _OfflineDataKeys.httpRequestData.name: httpRequestData.map((e) => e.toJson()).toList(), - OfflineDataKeys.selectedRequestId.name: selectedRequestId, - OfflineDataKeys.socketData.name: socketData, - OfflineDataKeys.socketData.name: + _OfflineDataKeys.selectedRequestId.name: selectedRequestId, + _OfflineDataKeys.socketData.name: socketData.map((e) => e.toJson()).toList(), }; } } -enum OfflineDataKeys { +enum _OfflineDataKeys { httpRequestData, selectedRequestId, socketData, From 2ea3044e8d7be3fdd148fd96b6016703901a877f Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 4 Nov 2024 13:33:56 +0530 Subject: [PATCH 26/36] reorder params, used isEmpty --- .../lib/src/screens/network/offline_network_data.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index a54a21ae6f8..f43bee4db7f 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -5,7 +5,6 @@ import 'package:devtools_shared/devtools_shared.dart'; import '../../shared/http/http_request_data.dart'; -import '../../shared/primitives/utils.dart'; import '../network/network_controller.dart'; import 'network_model.dart'; @@ -15,8 +14,8 @@ import 'network_model.dart'; class OfflineNetworkData with Serializable { OfflineNetworkData({ required this.httpRequestData, - this.selectedRequestId, required this.socketData, + this.selectedRequestId, }); /// Creates an instance of [OfflineNetworkData] from a JSON map. @@ -62,7 +61,7 @@ class OfflineNetworkData with Serializable { ); } - bool get isEmpty => httpRequestData.isNullOrEmpty && socketData.isNullOrEmpty; + bool get isEmpty => httpRequestData.isEmpty && socketData.isEmpty; /// List of current [DartIOHttpRequestData] network requests. final List httpRequestData; From e01bc3cda127afeebc36fbb9017476e101e5550d Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Sun, 17 Nov 2024 15:58:45 +0530 Subject: [PATCH 27/36] added tests, minor fixes --- .../screens/network/offline_network_data.dart | 4 +- .../test/network/offline_data_test.dart | 181 +++++++++++ .../network/sample_network_offline_data.json | 301 ++++++++++++++++++ 3 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 packages/devtools_app/test/network/offline_data_test.dart create mode 100644 packages/devtools_app/test/network/sample_network_offline_data.json diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index f43bee4db7f..b5dda050046 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -21,7 +21,7 @@ class OfflineNetworkData with Serializable { /// Creates an instance of [OfflineNetworkData] from a JSON map. factory OfflineNetworkData.fromJson(Map json) { final httpRequestJsonList = - json[_OfflineDataKeys.httpRequestData.name] as List?; + json[_OfflineDataKeys.httpRequestData.name] as List?; // Deserialize httpRequestData final httpRequestData = httpRequestJsonList @@ -41,7 +41,7 @@ class OfflineNetworkData with Serializable { // Deserialize socketData final socketJsonList = - json[_OfflineDataKeys.socketData.name] as List?; + json[_OfflineDataKeys.socketData.name] as List?; final socketData = socketJsonList ?.map((e) { if (e is Map) { diff --git a/packages/devtools_app/test/network/offline_data_test.dart b/packages/devtools_app/test/network/offline_data_test.dart new file mode 100644 index 00000000000..3f8c8812545 --- /dev/null +++ b/packages/devtools_app/test/network/offline_data_test.dart @@ -0,0 +1,181 @@ +// Copyright 2024 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:devtools_app/src/screens/network/network_model.dart'; +import 'package:devtools_app/src/screens/network/offline_network_data.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late Map jsonData; + late OfflineNetworkData offlineData; + late Socket firstSocket; + late Socket secondSocket; + + setUpAll(() { + final file = File('test/network/sample_network_offline_data.json'); + final fileContent = file.readAsStringSync(); + jsonData = jsonDecode(fileContent) as Map; + + // Create OfflineNetworkData + offlineData = OfflineNetworkData.fromJson(jsonData); + + // Extracting sockets for reuse in tests + firstSocket = offlineData.socketData.first; + secondSocket = offlineData.socketData.last; + }); + + group('Socket Tests', () { + test('Socket should deserialize from JSON correctly', () { + // Validate first socket + expect(firstSocket.id, '105553123901536'); + expect(firstSocket.socketType, 'tcp'); + expect(firstSocket.port, 443); + expect(firstSocket.readBytes, 4367); + expect(firstSocket.writeBytes, 18237); + + // Validate timestamps + const timelineMicrosBase = 1731482170837171; + expect( + firstSocket.startTimestamp, + DateTime.fromMicrosecondsSinceEpoch(timelineMicrosBase + 171830570040), + ); + expect( + firstSocket.endTimestamp, + DateTime.fromMicrosecondsSinceEpoch(timelineMicrosBase + 171830929647), + ); + expect( + firstSocket.lastReadTimestamp, + DateTime.fromMicrosecondsSinceEpoch(timelineMicrosBase + 171830928421), + ); + expect( + firstSocket.lastWriteTimestamp, + DateTime.fromMicrosecondsSinceEpoch(timelineMicrosBase + 171830669180), + ); + }); + + test('Socket should serialize to JSON correctly', () { + final serializedJson = firstSocket.toJson(); + + // Validate serialized JSON + expect(serializedJson['timelineMicrosBase'], 1731482170837171); + expect((serializedJson['socket'] as Map)['id'], '105553123901536'); + expect((serializedJson['socket'] as Map)['startTime'], 171830570040); + expect((serializedJson['socket'] as Map)['endTime'], 171830929647); + expect((serializedJson['socket'] as Map)['readBytes'], 4367); + expect((serializedJson['socket'] as Map)['writeBytes'], 18237); + }); + + test('Socket duration should be calculated correctly', () { + final expectedDuration = Duration( + microseconds: firstSocket.endTimestamp!.microsecondsSinceEpoch - + firstSocket.startTimestamp.microsecondsSinceEpoch, + ); + + expect(firstSocket.duration, expectedDuration); + }); + + test('Socket status should indicate "Open" or "Closed" based on endTime', + () { + expect( + firstSocket.status, + 'Closed', + ); // The provided socket has an endTime + + // Modify socket to simulate "Open" status + final openSocketJson = { + ...firstSocket.toJson(), + 'socket': { + ...(firstSocket.toJson()['socket'] as Map), + 'endTime': null, + }, + }; + final openSocket = Socket.fromJson(openSocketJson); + + expect(openSocket.status, 'Open'); // No endTime indicates "Open" + }); + + test('Socket equality and hash code should work correctly', () { + expect(firstSocket == secondSocket, isFalse); + expect(firstSocket.hashCode != secondSocket.hashCode, isTrue); + + final duplicateSocket = Socket.fromJson(firstSocket.toJson()); + expect(firstSocket, equals(duplicateSocket)); + expect(firstSocket.hashCode, equals(duplicateSocket.hashCode)); + }); + }); + + group('OfflineNetworkData Tests', () { + test('OfflineNetworkData should deserialize correctly', () { + // Validate httpRequestData + expect(offlineData.httpRequestData.length, 2); + expect(offlineData.httpRequestData.first.id, '975585676925010898'); + expect(offlineData.httpRequestData.first.method, 'GET'); + expect( + offlineData.httpRequestData.first.uri, + 'https://jsonplaceholder.typicode.com/albums/1', + ); + + // Validate socketData + expect(offlineData.socketData.length, 2); + expect(offlineData.socketData.first.id, '105553123901536'); + expect(offlineData.socketData.first.socketType, 'tcp'); + expect(offlineData.socketData.first.port, 443); + + // Validate selectedRequestId + expect(offlineData.selectedRequestId, isNull); + }); + + test('OfflineNetworkData should serialize correctly', () { + final serializedJson = offlineData.toJson(); + + // Validate serialized JSON + final httpRequestData = serializedJson['httpRequestData'] as List; + final firstRequest = httpRequestData.first as Map; + final requestDetails = firstRequest['request'] as Map; + + expect(requestDetails['id'], '975585676925010898'); + + final socketData = serializedJson['socketData'] as List; + final firstSocket = socketData.first as Map; + final socketDetails = firstSocket['socket'] as Map; + + expect(socketDetails['id'], '105553123901536'); + }); + + test( + 'isEmpty should return true when both httpRequestData and socketData are empty', + () { + final emptyOfflineData = OfflineNetworkData( + httpRequestData: [], + socketData: [], + ); + + expect(emptyOfflineData.isEmpty, isTrue); + }); + + test('isEmpty should return false when httpRequestData is populated', () { + final populatedHttpData = OfflineNetworkData( + httpRequestData: offlineData.httpRequestData, + socketData: [], + ); + + expect(populatedHttpData.isEmpty, isFalse); + }); + + test('toJson and fromJson should preserve data integrity', () { + final serializedJson = offlineData.toJson(); + final restoredData = OfflineNetworkData.fromJson(serializedJson); + + expect( + restoredData.httpRequestData.length, + offlineData.httpRequestData.length, + ); + expect(restoredData.socketData.length, offlineData.socketData.length); + expect(restoredData.selectedRequestId, offlineData.selectedRequestId); + }); + }); +} diff --git a/packages/devtools_app/test/network/sample_network_offline_data.json b/packages/devtools_app/test/network/sample_network_offline_data.json new file mode 100644 index 00000000000..4345951db15 --- /dev/null +++ b/packages/devtools_app/test/network/sample_network_offline_data.json @@ -0,0 +1,301 @@ +{ + "httpRequestData": [ + { + "request": { + "id": "975585676925010898", + "method": "GET", + "uri": "https://jsonplaceholder.typicode.com/albums/1", + "startTime": 1731654001072706, + "endTime": 1731654001547377, + "response": { + "startTime": 1731654001589754, + "endTime": 1731654001591776, + "headers": { + "x-ratelimit-reset": [ + "1730729357" + ], + "x-ratelimit-limit": [ + "1000" + ], + "date": [ + "Fri, 15 Nov 2024 07:00:01 GMT" + ], + "transfer-encoding": [ + "chunked" + ], + "vary": [ + "Origin, Accept-Encoding" + ], + "content-encoding": [ + "gzip" + ], + "x-ratelimit-remaining": [ + "999" + ], + "pragma": [ + "no-cache" + ], + "server": [ + "cloudflare" + ], + "reporting-endpoints": [ + "heroku-nel=https://nel.heroku.com/reports?ts=1730729316&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&s=486jdn49YhgGKLCA8ntm7fq%2BpZFtSZ0mXlRJhpb9Drs%3D" + ], + "cf-ray": [ + "8e2d5ca65edc3b19-BOM" + ], + "etag": [ + "W/\"40-74G1+b66MteeTYAz6G+NybtDGFA\"" + ], + "connection": [ + "keep-alive" + ], + "cache-control": [ + "max-age=43200" + ], + "age": [ + "428" + ], + "server-timing": [ + "cfL4;desc=\"?proto=TCP&rtt=1937&sent=6&recv=7&lost=0&retrans=0&sent_bytes=3149&recv_bytes=601&delivery_rate=2286315&cwnd=252&unsent_bytes=0&cid=b3587511212928f2&ts=96&x=0\"" + ], + "report-to": [ + "{\"group\":\"heroku-nel\",\"max_age\":3600,\"endpoints\":[{\"url\":\"https://nel.heroku.com/reports?ts=1730729316&sid=e11707d5-02a7-43ef-b45e-2cf4d2036f7d&s=486jdn49YhgGKLCA8ntm7fq%2BpZFtSZ0mXlRJhpb9Drs%3D\"}]}" + ], + "cf-cache-status": [ + "HIT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "access-control-allow-credentials": [ + "true" + ], + "x-powered-by": [ + "Express" + ], + "alt-svc": [ + "h3=\":443\"; ma=86400" + ], + "nel": [ + "{\"report_to\":\"heroku-nel\",\"max_age\":3600,\"success_fraction\":0.005,\"failure_fraction\":0.05,\"response_headers\":[\"Via\"]}" + ], + "via": [ + "1.1 vegur" + ], + "x-content-type-options": [ + "nosniff" + ], + "expires": [ + "-1" + ] + }, + "compressionState": "HttpClientResponseCompressionState.decompressed", + "connectionInfo": { + "localPort": 62852, + "remoteAddress": "172.67.167.151", + "remotePort": 443 + }, + "contentLength": -1, + "cookies": [], + "isRedirect": false, + "persistentConnection": true, + "reasonPhrase": "OK", + "redirects": [], + "statusCode": 200, + "error": null + }, + "request": { + "headers": { + "user-agent": [ + "Dart/3.6 (dart:io)" + ], + "accept-encoding": [ + "gzip" + ], + "content-length": [ + "0" + ], + "host": [ + "jsonplaceholder.typicode.com" + ] + }, + "followRedirects": true, + "maxRedirects": 5, + "connectionInfo": { + "localPort": 62852, + "remoteAddress": "172.67.167.151", + "remotePort": 443 + }, + "contentLength": 0, + "cookies": [], + "persistentConnection": true, + "proxyDetails": null + }, + "isolateId": "isolates/6270534775640395", + "events": [ + { + "event": "Connection established", + "timestamp": 1731654001547154, + "arguments": null + }, + { + "event": "Request sent", + "timestamp": 1731654001547179, + "arguments": null + }, + { + "event": "Waiting (TTFB)", + "timestamp": 1731654001589068, + "arguments": null + }, + { + "event": "Content Download", + "timestamp": 1731654001591839, + "arguments": null + } + ], + "requestBody": [], + "responseBody": [] + } + }, + { + "request": { + "id": "975585676925010899", + "method": "PUT", + "uri": "https://fake-store-api.mock.beeceptor.com", + "startTime": 1731654001073364, + "endTime": 1731654001502615, + "response": { + "startTime": 1731654001763913, + "endTime": 1731654001764397, + "headers": { + "content-type": [ + "text/plain" + ], + "alt-svc": [ + "h3=\":443\"; ma=2592000" + ], + "date": [ + "Fri, 15 Nov 2024 07:00:01 GMT" + ], + "access-control-allow-origin": [ + "*" + ], + "vary": [ + "Accept-Encoding" + ], + "content-length": [ + "125" + ] + }, + "compressionState": "HttpClientResponseCompressionState.notCompressed", + "connectionInfo": { + "localPort": 62851, + "remoteAddress": "159.89.140.122", + "remotePort": 443 + }, + "contentLength": 125, + "cookies": [], + "isRedirect": false, + "persistentConnection": true, + "reasonPhrase": "OK", + "redirects": [], + "statusCode": 200, + "error": null + }, + "request": { + "headers": { + "user-agent": [ + "Dart/3.6 (dart:io)" + ], + "accept-encoding": [ + "gzip" + ], + "user_id": [ + "1" + ], + "content-length": [ + "0" + ], + "items": [ + "[{\"product_id\":1,\"quantity\":2},{\"product_id\":3,\"quantity\":1}]" + ], + "host": [ + "fake-store-api.mock.beeceptor.com" + ] + }, + "followRedirects": true, + "maxRedirects": 5, + "connectionInfo": { + "localPort": 62851, + "remoteAddress": "159.89.140.122", + "remotePort": 443 + }, + "contentLength": 0, + "cookies": [], + "persistentConnection": true, + "proxyDetails": null + }, + "isolateId": "isolates/6270534775640395", + "events": [ + { + "event": "Connection established", + "timestamp": 1731654001501363, + "arguments": null + }, + { + "event": "Request sent", + "timestamp": 1731654001501436, + "arguments": null + }, + { + "event": "Waiting (TTFB)", + "timestamp": 1731654001763857, + "arguments": null + }, + { + "event": "Content Download", + "timestamp": 1731654001764409, + "arguments": null + } + ], + "requestBody": [], + "responseBody": [] + } + } + ], + "selectedRequestId": null, + "socketData": [ + { + "timelineMicrosBase": 1731482170837171, + "socket": { + "id": "105553123901536", + "startTime": 171830570040, + "endTime": 171830929647, + "lastReadTime": 171830928421, + "lastWriteTime": 171830669180, + "socketType": "tcp", + "address": "159.89.140.122", + "port": 443, + "readBytes": 4367, + "writeBytes": 18237 + } + }, + { + "timelineMicrosBase": 1731482170837171, + "socket": { + "id": "105553123902256", + "startTime": 171830571806, + "endTime": 171830757188, + "lastReadTime": 171830753067, + "lastWriteTime": 171830712602, + "socketType": "tcp", + "address": "172.67.167.151", + "port": 443, + "readBytes": 5447, + "writeBytes": 18247 + } + } + ] +} \ No newline at end of file From e7b5c906921b9f87eb2962553117d3cb2a14bda6 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Thu, 21 Nov 2024 09:24:50 +0530 Subject: [PATCH 28/36] removed repetitive expect statements --- .../devtools_app/test/network/offline_data_test.dart | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/devtools_app/test/network/offline_data_test.dart b/packages/devtools_app/test/network/offline_data_test.dart index 3f8c8812545..cf7cbfe1053 100644 --- a/packages/devtools_app/test/network/offline_data_test.dart +++ b/packages/devtools_app/test/network/offline_data_test.dart @@ -121,9 +121,6 @@ void main() { // Validate socketData expect(offlineData.socketData.length, 2); - expect(offlineData.socketData.first.id, '105553123901536'); - expect(offlineData.socketData.first.socketType, 'tcp'); - expect(offlineData.socketData.first.port, 443); // Validate selectedRequestId expect(offlineData.selectedRequestId, isNull); @@ -138,12 +135,6 @@ void main() { final requestDetails = firstRequest['request'] as Map; expect(requestDetails['id'], '975585676925010898'); - - final socketData = serializedJson['socketData'] as List; - final firstSocket = socketData.first as Map; - final socketDetails = firstSocket['socket'] as Map; - - expect(socketDetails['id'], '105553123901536'); }); test( From 4f31b922ee72d315500a7874626614bee9fafdbe Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Fri, 22 Nov 2024 09:58:31 +0530 Subject: [PATCH 29/36] minor fix --- .../src/screens/network/network_screen.dart | 198 +++++++++--------- 1 file changed, 98 insertions(+), 100 deletions(-) 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 ce1db458887..2abd767b9ef 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -44,10 +44,7 @@ class NetworkScreen extends Screen { final networkController = Provider.of(context); final color = Theme.of(context).colorScheme.onPrimary; return MultiValueListenableBuilder( - listenables: [ - networkController.requests, - networkController.filteredData, - ], + listenables: [networkController.requests, networkController.filteredData], builder: (context, values, child) { final networkRequests = values.first as List; final filteredRequests = values.second as List; @@ -72,11 +69,12 @@ class NetworkScreen extends Screen { return SizedBox( width: smallProgressSize, height: smallProgressSize, - child: recording - ? SmallCircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(color), - ) - : const SizedBox(), + child: + recording + ? SmallCircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(color), + ) + : const SizedBox(), ); }, ), @@ -120,16 +118,15 @@ class _NetworkScreenBodyState extends State return Column( children: [ OfflineAwareControls( - controlsBuilder: (offline) => _NetworkProfilerControls( - controller: controller, - offline: offline, - ), + controlsBuilder: + (offline) => _NetworkProfilerControls( + controller: controller, + offline: offline, + ), gaScreen: gac.network, ), const SizedBox(height: intermediateSpacing), - Expanded( - child: _NetworkProfilerBody(controller: controller), - ), + Expanded(child: _NetworkProfilerBody(controller: controller)), ], ); } @@ -179,51 +176,54 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> children: [ if (!widget.offline) ...[ StartStopRecordingButton( - recording: _recording, - onPressed: () async => - await widget.controller.togglePolling(!_recording), - tooltipOverride: _recording - ? 'Stop recording network traffic' - : 'Resume recording network traffic', - minScreenWidthForTextBeforeScaling: double.infinity, - gaScreen: gac.network, - gaSelection: _recording ? gac.pause : gac.resume, - ), - const SizedBox(width: denseSpacing), - ClearButton( - minScreenWidthForTextBeforeScaling: - _NetworkProfilerControls._includeTextWidth, - gaScreen: gac.network, - gaSelection: gac.clear, - onPressed: widget.controller.clear, - ), - const SizedBox(width: defaultSpacing), - DownloadButton( - tooltip: 'Download as .har file', - minScreenWidthForTextBeforeScaling: - _NetworkProfilerControls._includeTextWidth, - onPressed: widget.controller.exportAsHarFile, - gaScreen: gac.network, - gaSelection: gac.NetworkEvent.downloadAsHar.name, - ), - const Spacer(), - // TODO(kenz): fix focus issue when state is refreshed - Expanded( - child: SearchField( - searchController: widget.controller, - searchFieldEnabled: hasRequests, - searchFieldWidth: screenWidth <= MediaSize.xs - ? defaultSearchFieldWidth - : wideSearchFieldWidth, + recording: _recording, + onPressed: + () async => await widget.controller.togglePolling(!_recording), + tooltipOverride: + _recording + ? 'Stop recording network traffic' + : 'Resume recording network traffic', + minScreenWidthForTextBeforeScaling: double.infinity, + gaScreen: gac.network, + gaSelection: _recording ? gac.pause : gac.resume, ), - ), - const SizedBox(width: denseSpacing), - Expanded( - child: StandaloneFilterField( - controller: widget.controller, - filteredItem: 'request', + const SizedBox(width: denseSpacing), + ClearButton( + minScreenWidthForTextBeforeScaling: + _NetworkProfilerControls._includeTextWidth, + gaScreen: gac.network, + gaSelection: gac.clear, + onPressed: widget.controller.clear, ), - ), + const SizedBox(width: defaultSpacing), + DownloadButton( + tooltip: 'Download as .har file', + minScreenWidthForTextBeforeScaling: + _NetworkProfilerControls._includeTextWidth, + onPressed: widget.controller.exportAsHarFile, + gaScreen: gac.network, + gaSelection: gac.NetworkEvent.downloadAsHar.name, + ), + const Spacer(), + // TODO(kenz): fix focus issue when state is refreshed + Expanded( + child: SearchField( + searchController: widget.controller, + searchFieldEnabled: hasRequests, + searchFieldWidth: + screenWidth <= MediaSize.xs + ? defaultSearchFieldWidth + : wideSearchFieldWidth, + ), + ), + const SizedBox(width: denseSpacing), + Expanded( + child: StandaloneFilterField( + controller: widget.controller, + filteredItem: 'request', + ), + ), + ], ], ); } @@ -320,11 +320,11 @@ class NetworkRequestsTable extends StatelessWidget { class AddressColumn extends ColumnData implements ColumnRenderer { AddressColumn() - : super.wide( - 'Address', - minWidthPx: scaleByFontFactor(isEmbedded() ? 100 : 150.0), - showTooltip: true, - ); + : super.wide( + 'Address', + minWidthPx: scaleByFontFactor(isEmbedded() ? 100 : 150.0), + showTooltip: true, + ); @override String getValue(NetworkRequest dataObject) { @@ -365,11 +365,11 @@ class MethodColumn extends ColumnData { class ActionsColumn extends ColumnData implements ColumnRenderer { ActionsColumn() - : super( - '', - fixedWidthPx: scaleByFontFactor(32), - alignment: ColumnAlignment.right, - ); + : super( + '', + fixedWidthPx: scaleByFontFactor(32), + alignment: ColumnAlignment.right, + ); static const _actionSplashRadius = 16.0; @@ -440,12 +440,12 @@ class ActionsColumn extends ColumnData class StatusColumn extends ColumnData implements ColumnRenderer { StatusColumn() - : super( - 'Status', - alignment: ColumnAlignment.right, - headerAlignment: TextAlign.right, - fixedWidthPx: scaleByFontFactor(50), - ); + : super( + 'Status', + alignment: ColumnAlignment.right, + headerAlignment: TextAlign.right, + fixedWidthPx: scaleByFontFactor(50), + ); @override String? getValue(NetworkRequest dataObject) { @@ -468,21 +468,22 @@ class StatusColumn extends ColumnData final theme = Theme.of(context); return Text( getDisplayValue(data), - style: data.didFail - ? TextStyle(color: theme.colorScheme.error) - : theme.regularTextStyle, + style: + data.didFail + ? TextStyle(color: theme.colorScheme.error) + : theme.regularTextStyle, ); } } class TypeColumn extends ColumnData { TypeColumn() - : super( - 'Type', - alignment: ColumnAlignment.right, - headerAlignment: TextAlign.right, - fixedWidthPx: scaleByFontFactor(50), - ); + : super( + 'Type', + alignment: ColumnAlignment.right, + headerAlignment: TextAlign.right, + fixedWidthPx: scaleByFontFactor(50), + ); @override String getValue(NetworkRequest dataObject) { @@ -497,12 +498,12 @@ class TypeColumn extends ColumnData { class DurationColumn extends ColumnData { DurationColumn() - : super( - 'Duration', - alignment: ColumnAlignment.right, - headerAlignment: TextAlign.right, - fixedWidthPx: scaleByFontFactor(75), - ); + : super( + 'Duration', + alignment: ColumnAlignment.right, + headerAlignment: TextAlign.right, + fixedWidthPx: scaleByFontFactor(75), + ); @override int? getValue(NetworkRequest dataObject) { @@ -514,21 +515,18 @@ class DurationColumn extends ColumnData { final ms = getValue(dataObject); return ms == null ? 'Pending' - : durationText( - Duration(milliseconds: ms), - fractionDigits: 0, - ); + : durationText(Duration(milliseconds: ms), fractionDigits: 0); } } class TimestampColumn extends ColumnData { TimestampColumn() - : super( - 'Timestamp', - alignment: ColumnAlignment.right, - headerAlignment: TextAlign.right, - fixedWidthPx: scaleByFontFactor(115), - ); + : super( + 'Timestamp', + alignment: ColumnAlignment.right, + headerAlignment: TextAlign.right, + fixedWidthPx: scaleByFontFactor(115), + ); @override DateTime? getValue(NetworkRequest dataObject) { From 04c8b4171912b692f80ff05190b273e1bc141334 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Sun, 24 Nov 2024 15:16:56 +0530 Subject: [PATCH 30/36] added timelineMicrosOffset to offline data --- .../src/screens/network/network_controller.dart | 12 ++++++++---- .../src/screens/network/offline_network_data.dart | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) 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 105fe704702..ac8e4b00f91 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -175,6 +175,7 @@ class NetworkController extends DisposableController await maybeLoadOfflineData( NetworkScreen.id, createData: (json) => OfflineNetworkData.fromJson(json), + // ignore: avoid_dynamic_calls shouldLoad: (data) => !data.isEmpty, loadData: (data) => loadOfflineData(data), ); @@ -184,7 +185,8 @@ class NetworkController extends DisposableController } Future loadOfflineData(OfflineNetworkData offlineData) async { - final httpProfileData = offlineData.httpRequestData.mapToHttpProfileRequests; + final httpProfileData = + offlineData.httpRequestData.mapToHttpProfileRequests; final socketStatsData = offlineData.socketData.mapToSocketStatistics; _currentNetworkRequests @@ -192,14 +194,15 @@ class NetworkController extends DisposableController ..updateOrAddAll( requests: httpProfileData, sockets: socketStatsData, - timelineMicrosOffset: DateTime.now().microsecondsSinceEpoch, + timelineMicrosOffset: offlineData.timelineMicrosOffset, ); _filterAndRefreshSearchMatches(); // If a selectedRequestId is available, select it in offline mode. if (offlineData.selectedRequestId != null) { - final selected = _currentNetworkRequests - .getRequest(offlineData.selectedRequestId ?? ''); + final selected = _currentNetworkRequests.getRequest( + offlineData.selectedRequestId ?? '', + ); if (selected != null) { selectedRequest.value = selected; resetDropDown(); @@ -457,6 +460,7 @@ class NetworkController extends DisposableController httpRequestData: httpRequestData, socketData: socketData, selectedRequestId: selectedRequest.value?.id, + timelineMicrosOffset: _timelineMicrosOffset, ); return OfflineScreenData( diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index b5dda050046..0d3d83babe5 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -16,15 +16,16 @@ class OfflineNetworkData with Serializable { required this.httpRequestData, required this.socketData, this.selectedRequestId, + required this.timelineMicrosOffset, }); /// Creates an instance of [OfflineNetworkData] from a JSON map. factory OfflineNetworkData.fromJson(Map json) { final httpRequestJsonList = json[_OfflineDataKeys.httpRequestData.name] as List?; - // Deserialize httpRequestData - final httpRequestData = httpRequestJsonList + final httpRequestData = + httpRequestJsonList ?.map((e) { if (e is Map) { final requestData = @@ -42,7 +43,8 @@ class OfflineNetworkData with Serializable { // Deserialize socketData final socketJsonList = json[_OfflineDataKeys.socketData.name] as List?; - final socketData = socketJsonList + final socketData = + socketJsonList ?.map((e) { if (e is Map) { return Socket.fromJson(e); @@ -52,12 +54,14 @@ class OfflineNetworkData with Serializable { .whereType() .toList() ?? []; + final timelineMicrosOffset = json['timelineMicrosOffset']; return OfflineNetworkData( httpRequestData: httpRequestData, selectedRequestId: json[_OfflineDataKeys.selectedRequestId.name] as String?, socketData: socketData, + timelineMicrosOffset: timelineMicrosOffset as int, ); } @@ -69,6 +73,9 @@ class OfflineNetworkData with Serializable { /// The ID of the currently selected request, if any. final String? selectedRequestId; + /// used to calculate the correct wall-time for timeline events. + final int timelineMicrosOffset; + /// The list of socket statistics for the offline network data. final List socketData; @@ -81,6 +88,7 @@ class OfflineNetworkData with Serializable { _OfflineDataKeys.selectedRequestId.name: selectedRequestId, _OfflineDataKeys.socketData.name: socketData.map((e) => e.toJson()).toList(), + _OfflineDataKeys.timelineMicrosOffset.name: timelineMicrosOffset, }; } } @@ -90,4 +98,5 @@ enum _OfflineDataKeys { selectedRequestId, socketData, request, + timelineMicrosOffset, } From ab8c519938c911650b43986baf44e37cb777b9f8 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Sun, 24 Nov 2024 15:42:26 +0530 Subject: [PATCH 31/36] added timelineMicrosOffset to tests --- .../screens/network/offline_network_data.dart | 4 +- .../test/network/offline_data_test.dart | 64 ++++++++++--------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart index 0d3d83babe5..bde196cb1f0 100644 --- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart +++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart @@ -61,7 +61,7 @@ class OfflineNetworkData with Serializable { selectedRequestId: json[_OfflineDataKeys.selectedRequestId.name] as String?, socketData: socketData, - timelineMicrosOffset: timelineMicrosOffset as int, + timelineMicrosOffset: timelineMicrosOffset as int? ?? 0, ); } @@ -74,7 +74,7 @@ class OfflineNetworkData with Serializable { final String? selectedRequestId; /// used to calculate the correct wall-time for timeline events. - final int timelineMicrosOffset; + final int? timelineMicrosOffset; /// The list of socket statistics for the offline network data. final List socketData; diff --git a/packages/devtools_app/test/network/offline_data_test.dart b/packages/devtools_app/test/network/offline_data_test.dart index cf7cbfe1053..33f195c7048 100644 --- a/packages/devtools_app/test/network/offline_data_test.dart +++ b/packages/devtools_app/test/network/offline_data_test.dart @@ -71,32 +71,35 @@ void main() { test('Socket duration should be calculated correctly', () { final expectedDuration = Duration( - microseconds: firstSocket.endTimestamp!.microsecondsSinceEpoch - + microseconds: + firstSocket.endTimestamp!.microsecondsSinceEpoch - firstSocket.startTimestamp.microsecondsSinceEpoch, ); expect(firstSocket.duration, expectedDuration); }); - test('Socket status should indicate "Open" or "Closed" based on endTime', - () { - expect( - firstSocket.status, - 'Closed', - ); // The provided socket has an endTime - - // Modify socket to simulate "Open" status - final openSocketJson = { - ...firstSocket.toJson(), - 'socket': { - ...(firstSocket.toJson()['socket'] as Map), - 'endTime': null, - }, - }; - final openSocket = Socket.fromJson(openSocketJson); - - expect(openSocket.status, 'Open'); // No endTime indicates "Open" - }); + test( + 'Socket status should indicate "Open" or "Closed" based on endTime', + () { + expect( + firstSocket.status, + 'Closed', + ); // The provided socket has an endTime + + // Modify socket to simulate "Open" status + final openSocketJson = { + ...firstSocket.toJson(), + 'socket': { + ...(firstSocket.toJson()['socket'] as Map), + 'endTime': null, + }, + }; + final openSocket = Socket.fromJson(openSocketJson); + + expect(openSocket.status, 'Open'); // No endTime indicates "Open" + }, + ); test('Socket equality and hash code should work correctly', () { expect(firstSocket == secondSocket, isFalse); @@ -138,20 +141,23 @@ void main() { }); test( - 'isEmpty should return true when both httpRequestData and socketData are empty', - () { - final emptyOfflineData = OfflineNetworkData( - httpRequestData: [], - socketData: [], - ); - - expect(emptyOfflineData.isEmpty, isTrue); - }); + 'isEmpty should return true when both httpRequestData and socketData are empty', + () { + final emptyOfflineData = OfflineNetworkData( + httpRequestData: [], + socketData: [], + timelineMicrosOffset: 1731482170837171, + ); + + expect(emptyOfflineData.isEmpty, isTrue); + }, + ); test('isEmpty should return false when httpRequestData is populated', () { final populatedHttpData = OfflineNetworkData( httpRequestData: offlineData.httpRequestData, socketData: [], + timelineMicrosOffset: 1731482170837171, ); expect(populatedHttpData.isEmpty, isFalse); From 2a9cacf308c901f9d5e0e167e4ac7d7ae15b0694 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Fri, 27 Dec 2024 12:23:29 +0530 Subject: [PATCH 32/36] release notes updated --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index efcb692a04b..67211acdcbe 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -36,8 +36,8 @@ TODO: Remove this section if there are not any general updates. ## Network profiler updates -TODO: Remove this section if there are not any general updates. - +* Offline support added for the network screen. - [#8332](https://github.com/flutter/devtools/pull/8332) + ## Logging updates TODO: Remove this section if there are not any general updates. From 4298078326c67b2f89f6ec0e7018d1a3e8481af3 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Fri, 27 Dec 2024 12:51:36 +0530 Subject: [PATCH 33/36] code reformatted --- .../src/screens/network/har_data_entry.dart | 125 ++++++++++-------- .../src/screens/network/utils/http_utils.dart | 19 +-- .../lib/src/shared/feature_flags.dart | 6 +- .../lib/src/shared/http/constants.dart | 6 +- .../src/shared/http/http_request_data.dart | 48 +++---- 5 files changed, 105 insertions(+), 99 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/har_data_entry.dart b/packages/devtools_app/lib/src/screens/network/har_data_entry.dart index 74272b80b78..0318c7f7783 100644 --- a/packages/devtools_app/lib/src/screens/network/har_data_entry.dart +++ b/packages/devtools_app/lib/src/screens/network/har_data_entry.dart @@ -29,16 +29,18 @@ class HarDataEntry { final modifiedRequestData = _remapCustomFieldKeys(json); // Retrieving url, method from requestData - final requestData = modifiedRequestData[NetworkEventKeys.request.name] - as Map; + final requestData = + modifiedRequestData[NetworkEventKeys.request.name] + as Map; modifiedRequestData[NetworkEventKeys.uri.name] = requestData[NetworkEventKeys.url.name]; modifiedRequestData[NetworkEventKeys.method.name] = requestData[NetworkEventKeys.method.name]; // Adding missing keys which are mandatory for parsing - final responseData = modifiedRequestData[NetworkEventKeys.response.name] - as Map; + final responseData = + modifiedRequestData[NetworkEventKeys.response.name] + as Map; responseData[NetworkEventKeys.redirects.name] = >[]; final requestPostData = responseData[NetworkEventKeys.postData.name]; final responseContent = responseData[NetworkEventKeys.content.name]; @@ -60,60 +62,65 @@ class HarDataEntry { /// serialization. static Map toJson(DartIOHttpRequestData e) { // Implement the logic to convert DartIOHttpRequestData to HAR entry format - final requestCookies = e.requestCookies.map((cookie) { - return { - NetworkEventKeys.name.name: cookie.name, - NetworkEventKeys.value.name: cookie.value, - NetworkEventKeys.path.name: cookie.path, - NetworkEventKeys.domain.name: cookie.domain, - NetworkEventKeys.expires.name: - cookie.expires?.toUtc().toIso8601String(), - NetworkEventKeys.httpOnly.name: cookie.httpOnly, - NetworkEventKeys.secure.name: cookie.secure, - }; - }).toList(); + final requestCookies = + e.requestCookies.map((cookie) { + return { + NetworkEventKeys.name.name: cookie.name, + NetworkEventKeys.value.name: cookie.value, + NetworkEventKeys.path.name: cookie.path, + NetworkEventKeys.domain.name: cookie.domain, + NetworkEventKeys.expires.name: + cookie.expires?.toUtc().toIso8601String(), + NetworkEventKeys.httpOnly.name: cookie.httpOnly, + NetworkEventKeys.secure.name: cookie.secure, + }; + }).toList(); - final requestHeaders = e.requestHeaders?.entries.map((header) { - var value = header.value; - if (value is List) { - value = value.first; - } - return { - NetworkEventKeys.name.name: header.key, - NetworkEventKeys.value.name: value, - }; - }).toList(); + final requestHeaders = + e.requestHeaders?.entries.map((header) { + var value = header.value; + if (value is List) { + value = value.first; + } + return { + NetworkEventKeys.name.name: header.key, + NetworkEventKeys.value.name: value, + }; + }).toList(); - final queryString = Uri.parse(e.uri).queryParameters.entries.map((param) { - return { - NetworkEventKeys.name.name: param.key, - NetworkEventKeys.value.name: param.value, - }; - }).toList(); + final queryString = + Uri.parse(e.uri).queryParameters.entries.map((param) { + return { + NetworkEventKeys.name.name: param.key, + NetworkEventKeys.value.name: param.value, + }; + }).toList(); - final responseCookies = e.responseCookies.map((cookie) { - return { - NetworkEventKeys.name.name: cookie.name, - NetworkEventKeys.value.name: cookie.value, - NetworkEventKeys.path.name: cookie.path, - NetworkEventKeys.domain.name: cookie.domain, - NetworkEventKeys.expires.name: - cookie.expires?.toUtc().toIso8601String(), - NetworkEventKeys.httpOnly.name: cookie.httpOnly, - NetworkEventKeys.secure.name: cookie.secure, - }; - }).toList(); + final responseCookies = + e.responseCookies.map((cookie) { + return { + NetworkEventKeys.name.name: cookie.name, + NetworkEventKeys.value.name: cookie.value, + NetworkEventKeys.path.name: cookie.path, + NetworkEventKeys.domain.name: cookie.domain, + NetworkEventKeys.expires.name: + cookie.expires?.toUtc().toIso8601String(), + NetworkEventKeys.httpOnly.name: cookie.httpOnly, + NetworkEventKeys.secure.name: cookie.secure, + }; + }).toList(); - final responseHeaders = e.responseHeaders?.entries.map((header) { - var value = header.value; - if (value is List) { - value = value.first; - } - return { - NetworkEventKeys.name.name: header.key, - NetworkEventKeys.value.name: value, - }; - }).toList(); + final responseHeaders = + e.responseHeaders?.entries.map((header) { + var value = header.value; + if (value is List) { + value = value.first; + } + return { + NetworkEventKeys.name.name: header.key, + NetworkEventKeys.value.name: value, + }; + }).toList(); return { NetworkEventKeys.startedDateTime.name: @@ -131,8 +138,9 @@ class HarDataEntry { NetworkEventKeys.mimeType.name: e.contentType, NetworkEventKeys.text.name: e.requestBody, }, - NetworkEventKeys.headersSize.name: - calculateHeadersSize(e.requestHeaders), + NetworkEventKeys.headersSize.name: calculateHeadersSize( + e.requestHeaders, + ), NetworkEventKeys.bodySize.name: _calculateBodySize(e.requestBody), }, // Response @@ -150,8 +158,9 @@ class HarDataEntry { NetworkEventKeys.text.name: e.responseBody, }, NetworkEventKeys.redirectURL.name: '', - NetworkEventKeys.headersSize.name: - calculateHeadersSize(e.responseHeaders), + NetworkEventKeys.headersSize.name: calculateHeadersSize( + e.responseHeaders, + ), NetworkEventKeys.bodySize.name: _calculateBodySize(e.responseBody), }, // Cache diff --git a/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart b/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart index 3550770df5d..e6e564941d2 100644 --- a/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart +++ b/packages/devtools_app/lib/src/screens/network/utils/http_utils.dart @@ -13,15 +13,16 @@ int calculateHeadersSize(Map? headers) { if (headers == null) return -1; // Combine headers into a single string with CRLF endings - String headersString = headers.entries.map((entry) { - final key = entry.key; - var value = entry.value; - // If the value is a List, join it with a comma - if (value is List) { - value = value.join(', '); - } - return '$key: $value\r\n'; - }).join(); + String headersString = + headers.entries.map((entry) { + final key = entry.key; + var value = entry.value; + // If the value is a List, join it with a comma + if (value is List) { + value = value.join(', '); + } + return '$key: $value\r\n'; + }).join(); // Add final CRLF to indicate end of headers headersString += '\r\n'; diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart index 66e57fbde43..83ba0929cd9 100644 --- a/packages/devtools_app/lib/src/shared/feature_flags.dart +++ b/packages/devtools_app/lib/src/shared/feature_flags.dart @@ -41,8 +41,10 @@ const _kMemoryDisconnectExperience = bool.fromEnvironment( defaultValue: true, ); -const _kNetworkOfflineExperiment = - bool.fromEnvironment('network_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 diff --git a/packages/devtools_app/lib/src/shared/http/constants.dart b/packages/devtools_app/lib/src/shared/http/constants.dart index 3ed1190b561..6f5852b46ac 100644 --- a/packages/devtools_app/lib/src/shared/http/constants.dart +++ b/packages/devtools_app/lib/src/shared/http/constants.dart @@ -65,12 +65,10 @@ enum HttpRequestDataKeys { arguments, host, username, - isDirect + isDirect, } -enum HttpRequestDataValues { - json, -} +enum HttpRequestDataValues { json } class HttpRequestDataDefaults { static const none = 'None'; 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 e49b33e1e53..dd02e3f702a 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 @@ -52,13 +52,14 @@ class DartIOHttpRequestData extends NetworkRequest { Map? requestPostData, Map? responseContent, ) { - final isFullRequest = modifiedRequestData - .containsKey(HttpRequestDataKeys.requestBody.name) && + final isFullRequest = + modifiedRequestData.containsKey(HttpRequestDataKeys.requestBody.name) && modifiedRequestData.containsKey(HttpRequestDataKeys.responseBody.name); - final parsedRequest = isFullRequest - ? HttpProfileRequest.parse(modifiedRequestData) - : HttpProfileRequestRef.parse(modifiedRequestData); + final parsedRequest = + isFullRequest + ? HttpProfileRequest.parse(modifiedRequestData) + : HttpProfileRequestRef.parse(modifiedRequestData); final responseBody = responseContent?[HttpRequestDataKeys.text.name]?.toString(); @@ -66,9 +67,9 @@ class DartIOHttpRequestData extends NetworkRequest { requestPostData?[HttpRequestDataKeys.text.name]?.toString(); return DartIOHttpRequestData( - parsedRequest!, - requestFullDataFromVmService: parsedRequest is! HttpProfileRequest, - ) + parsedRequest!, + requestFullDataFromVmService: parsedRequest is! HttpProfileRequest, + ) .._responseBody = responseBody .._requestBody = requestBody; } @@ -95,9 +96,9 @@ class DartIOHttpRequestData extends NetworkRequest { isFetchingFullData = true; final updated = await serviceConnection.serviceManager.service! .getHttpProfileRequestWrapper( - _request.isolateId, - _request.id.toString(), - ); + _request.isolateId, + _request.id.toString(), + ); _request = updated; final fullRequest = _request as HttpProfileRequest; _responseBody = utf8.decode(fullRequest.responseBody!); @@ -233,9 +234,10 @@ class DartIOHttpRequestData extends NetworkRequest { requestCookies.isNotEmpty || responseCookies.isNotEmpty; /// A list of all cookies contained within the request headers. - List get requestCookies => _hasError - ? [] - : DartIOHttpRequestData._parseCookies(_request.request?.cookies); + List get requestCookies => + _hasError + ? [] + : DartIOHttpRequestData._parseCookies(_request.request?.cookies); /// A list of all cookies contained within the response headers. List get responseCookies => @@ -335,9 +337,10 @@ class DartIOHttpRequestData extends NetworkRequest { DateTime lastTime = _request.startTime; for (final instant in instantEvents) { final instantTime = instant.timestamp; - instant._timeRange = TimeRange() - ..start = Duration(microseconds: lastTime.microsecondsSinceEpoch) - ..end = Duration(microseconds: instantTime.microsecondsSinceEpoch); + instant._timeRange = + TimeRange() + ..start = Duration(microseconds: lastTime.microsecondsSinceEpoch) + ..end = Duration(microseconds: instantTime.microsecondsSinceEpoch); lastTime = instantTime; } } @@ -348,15 +351,8 @@ class DartIOHttpRequestData extends NetworkRequest { } @override - int get hashCode => Object.hash( - id, - method, - uri, - contentType, - type, - port, - startTimestamp, - ); + int get hashCode => + Object.hash(id, method, uri, contentType, type, port, startTimestamp); } extension HttpRequestExtension on List { From a538a745403d10c16e5af0a99d2e9b7952dc788b Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 6 Jan 2025 23:20:18 +0530 Subject: [PATCH 34/36] lint fixes - fixed import, removed unused import --- .../lib/src/screens/network/network_controller.dart | 4 ++-- .../devtools_app/lib/src/screens/network/network_screen.dart | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) 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 3aae37d6288..e200c479f45 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -14,7 +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/offline/offline_data.dart'; import '../../shared/primitives/utils.dart'; import '../../shared/ui/filter.dart'; import '../../shared/ui/search.dart'; @@ -194,7 +194,7 @@ class NetworkController extends DisposableController ..updateOrAddAll( requests: httpProfileData, sockets: socketStatsData, - timelineMicrosOffset: offlineData.timelineMicrosOffset, + timelineMicrosOffset: offlineData.timelineMicrosOffset ?? 0, ); _filterAndRefreshSearchMatches(); 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 f3d6853f1d4..2387e8dd854 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -14,7 +14,6 @@ import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/config_specific/copy_to_clipboard/copy_to_clipboard.dart'; import '../../shared/framework/screen.dart'; -import '../../shared/globals.dart'; import '../../shared/http/curl_command.dart'; import '../../shared/http/http_request_data.dart'; import '../../shared/primitives/utils.dart'; From e05cd0284cce0f885aa1e24789df391c13ce2865 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 6 Jan 2025 23:30:27 +0530 Subject: [PATCH 35/36] comment added for ignore --- .../lib/src/screens/network/network_controller.dart | 2 ++ 1 file changed, 2 insertions(+) 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 e200c479f45..6280d9fe844 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -175,6 +175,8 @@ class NetworkController extends DisposableController await maybeLoadOfflineData( NetworkScreen.id, createData: (json) => OfflineNetworkData.fromJson(json), + // This ignore is used because the 'data' parameter can have a dynamic type, + // which cannot be explicitly typed here due to its dependency on JSON parsing. // ignore: avoid_dynamic_calls shouldLoad: (data) => !data.isEmpty, loadData: (data) => loadOfflineData(data), From ae52105d182f91d34a18d946900f157ded216b33 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Tue, 7 Jan 2025 00:00:55 +0530 Subject: [PATCH 36/36] lint fix: removed redundant async --- .../lib/src/screens/network/network_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6280d9fe844..ab65b87521b 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -186,7 +186,7 @@ class NetworkController extends DisposableController } } - Future loadOfflineData(OfflineNetworkData offlineData) async { + void loadOfflineData(OfflineNetworkData offlineData) { final httpProfileData = offlineData.httpRequestData.mapToHttpProfileRequests; final socketStatsData = offlineData.socketData.mapToSocketStatistics;