From da7252dc93b7e8c6a9173149ad5d1d39205cccdd Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Sun, 23 Jun 2024 13:27:24 +0530 Subject: [PATCH 01/12] implemented har feature --- .../screens/network/network_controller.dart | 183 ++++++++++++++++-- .../src/screens/network/network_screen.dart | 8 + .../lib/src/shared/analytics/constants.dart | 1 + .../constants/_network_constants.dart | 5 + .../import_export/import_export.dart | 3 +- 5 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.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 c623f74cd19..364419d0028 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -3,23 +3,21 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:vm_service/vm_service.dart'; +import '../../../devtools_app.dart'; +import '../../shared/config_specific/import_export/import_export.dart'; 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/primitives/utils.dart'; -import '../../shared/ui/filter.dart'; -import '../../shared/ui/search.dart'; -import '../../shared/utils.dart'; -import 'network_model.dart'; -import 'network_screen.dart'; import 'network_service.dart'; +final _exportController = ExportController(); +List? httpRequests; + /// Different types of Network Response which can be used to visualise response /// on Response tab enum NetworkResponseViewType { @@ -46,6 +44,7 @@ class NetworkController extends DisposableController with SearchControllerMixin, FilterControllerMixin, + OfflineScreenControllerMixin, AutoDisposeControllerMixin { NetworkController() { _networkService = NetworkService(this); @@ -57,6 +56,154 @@ class NetworkController extends DisposableController subscribeToFilterChanges(); } + String? exportAsHarFile() { + httpRequests = + filteredData.value.whereType().toList(); + + if (httpRequests!.isEmpty) { + debugPrint('No valid request data to export'); + return ''; + } + + try { + if (httpRequests!.isNotEmpty) { + final har = { + 'log': { + 'version': '1.2', + 'creator': { + 'name': 'flutter_tool', + 'version': '0.0.2', + }, + 'pages': [ + { + 'startedDateTime': httpRequests?.first.startTimestamp + .toUtc() + .toIso8601String(), + 'id': 'page_0', + 'title': 'FlutterCapture', + 'pageTimings': { + 'onContentLoad': -1, + 'onLoad': -1, + }, + }, + ], + 'entries': httpRequests + ?.map( + (e) => { + 'pageref': 'page_0', + 'startedDateTime': + e.startTimestamp.toUtc().toIso8601String(), + 'time': e.duration?.inMilliseconds, + 'request': { + 'method': e.method.toUpperCase(), + 'url': e.uri.toString(), + 'httpVersion': 'HTTP/1.1', + 'cookies': e.requestCookies + .map( + (e) => { + 'name': e.name, + 'value': e.value, + 'path': e.path, + 'domain': e.domain, + 'expires': e.expires?.toUtc().toIso8601String(), + 'httpOnly': e.httpOnly, + 'secure': e.secure, + }, + ) + .toList(), + 'headers': e.requestHeaders?.entries.map((h) { + var value = h.value; + if (value is List) { + value = value.first; + } + return { + 'name': h.key, + 'value': value, + }; + }).toList(), + 'queryString': Uri.parse(e.uri) + .queryParameters + .entries + .map( + (q) => { + 'name': q.key, + 'value': q.value, + }, + ) + .toList(), + 'postData': { + 'mimeType': e.contentType, + 'text': e.requestBody, + }, + 'headersSize': -1, + 'bodySize': -1, + }, + 'response': { + 'status': e.status, + 'statusText': '', + 'httpVersion': 'http/2.0', + 'cookies': e.responseCookies + .map( + (e) => { + 'name': e.name, + 'value': e.value, + 'path': e.path, + 'domain': e.domain, + 'expires': e.expires?.toUtc().toIso8601String(), + 'httpOnly': e.httpOnly, + 'secure': e.secure, + }, + ) + .toList(), + 'headers': e.responseHeaders?.entries.map((h) { + var value = h.value; + if (value is List) { + value = value.first; + } + return { + 'name': h.key, + 'value': value, + }; + }).toList(), + 'content': { + 'size': e.responseBody?.length, + 'mimeType': e.type, + 'text': e.responseBody, + }, + 'redirectURL': '', + 'headersSize': -1, + 'bodySize': -1, + }, + 'cache': {}, + 'timings': { + 'blocked': -1, + 'dns': -1, + 'connect': -1, + 'send': 1, + 'wait': e.duration!.inMilliseconds - 2, + 'receive': 1, + 'ssl': -1, + }, + 'serverIPAddress': '10.0.0.1', + 'connection': e.hashCode.toString(), + 'comment': '', + }, + ) + .toList(), + }, + }; + debugPrint('data is ${json.encode(har)}'); + return _exportController.downloadFile( + json.encode(har), + type: ExportFileType.har, + ); + } + } catch (ex) { + debugPrint('Exception in export $ex'); + } + return null; + } + static const methodFilterId = 'network-method-filter'; static const statusFilterId = 'network-status-filter'; @@ -266,15 +413,11 @@ class NetworkController extends DisposableController final service = serviceConnection.serviceManager.service!; await service.forEachIsolate( (isolate) async { - final future = switch (type) { - _NetworkTrafficType.http => - service.httpEnableTimelineLoggingWrapper(isolate.id!), - _NetworkTrafficType.socket => - service.socketProfilingEnabledWrapper(isolate.id!), - }; + final httpFuture = + service.httpEnableTimelineLoggingWrapper(isolate.id!); // The above call won't complete immediately if the isolate is paused, // so give up waiting after 500ms. - final state = await timeout(future, 500); + final state = await timeout(httpFuture, 500); if (state?.enabled != true) { enabled = false; } @@ -360,6 +503,16 @@ class NetworkController extends DisposableController serviceConnection.errorBadgeManager.incrementBadgeCount(NetworkScreen.id); } } + + @override + OfflineScreenData prepareOfflineScreenData() { + debugPrint('offline data - httpRequests are $httpRequests'); + return OfflineScreenData( + screenId: NetworkScreen.id, + //TODO deserialize har data and pass here + data: {}, + ); + } } /// Class for managing the set of all current websocket requests, 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 92bab290b03..edb0bea51a2 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -234,6 +234,14 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> onPressed: widget.controller.clear, ), const SizedBox(width: defaultSpacing), + DownloadButton( + minScreenWidthForTextBeforeScaling: + _NetworkProfilerControls._includeTextWidth, + onPressed: widget.controller.exportAsHarFile, + gaScreen: gac.network, + gaSelection: gac.NetworkEvent.networkDownloadHar, + ), + const SizedBox(width: defaultSpacing), const Expanded(child: SizedBox()), // TODO(kenz): fix focus issue when state is refreshed SearchField( diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart index 784b6a5c26c..b1c1ddd9547 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart @@ -12,6 +12,7 @@ part 'constants/_debugger_constants.dart'; part 'constants/_deep_links_constants.dart'; part 'constants/_extension_constants.dart'; part 'constants/_memory_constants.dart'; +part 'constants/_network_constants.dart'; part 'constants/_performance_constants.dart'; part 'constants/_vs_code_sidebar_constants.dart'; part 'constants/_inspector_constants.dart'; diff --git a/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart new file mode 100644 index 00000000000..f99248415d8 --- /dev/null +++ b/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart @@ -0,0 +1,5 @@ +part of '../constants.dart'; + +class NetworkEvent { + static const networkDownloadHar = 'networkDownloadHar'; +} diff --git a/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart b/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart index 903c12d03cb..356db3f61da 100644 --- a/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart +++ b/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart @@ -113,7 +113,8 @@ enum ExportFileType { json, csv, yaml, - data; + data, + har; @override String toString() => name; From f6f9491e0816f698a285b45b5d9a4fd8ba034447 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Sun, 23 Jun 2024 13:30:45 +0530 Subject: [PATCH 02/12] reverting some code --- .../lib/src/screens/network/network_controller.dart | 10 +++++++--- 1 file changed, 7 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 364419d0028..a38be27302e 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -413,11 +413,15 @@ class NetworkController extends DisposableController final service = serviceConnection.serviceManager.service!; await service.forEachIsolate( (isolate) async { - final httpFuture = - service.httpEnableTimelineLoggingWrapper(isolate.id!); + final future = switch (type) { + _NetworkTrafficType.http => + service.httpEnableTimelineLoggingWrapper(isolate.id!), + _NetworkTrafficType.socket => + service.socketProfilingEnabledWrapper(isolate.id!), + }; // The above call won't complete immediately if the isolate is paused, // so give up waiting after 500ms. - final state = await timeout(httpFuture, 500); + final state = await timeout(future, 500); if (state?.enabled != true) { enabled = false; } From a1ba9bdbf607ebce46c63b8cf1d4a390511cf476 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 24 Jun 2024 22:17:39 +0530 Subject: [PATCH 03/12] comments resolved, code refactoring --- .../lib/src/screens/network/har_builder.dart | 139 +++++++++++++++++ .../screens/network/network_controller.dart | 140 ++---------------- .../constants/_network_constants.dart | 74 +++++++++ 3 files changed, 226 insertions(+), 127 deletions(-) create mode 100644 packages/devtools_app/lib/src/screens/network/har_builder.dart diff --git a/packages/devtools_app/lib/src/screens/network/har_builder.dart b/packages/devtools_app/lib/src/screens/network/har_builder.dart new file mode 100644 index 00000000000..6c21bd731dd --- /dev/null +++ b/packages/devtools_app/lib/src/screens/network/har_builder.dart @@ -0,0 +1,139 @@ +import '../../../devtools_app.dart'; +import '../../shared/analytics/constants.dart'; + +Map buildHar(List httpRequests) { +// Build the creator + final creator = { + NetworkEventKeys.name: NetworkEventDefaults.creatorName, + NetworkEventKeys.creatorVersion: NetworkEventDefaults.creatorVersion, + }; + +// Build the pages + final pages = [ + { + NetworkEventKeys.startedDateTime: + httpRequests.first.startTimestamp.toUtc().toIso8601String(), + NetworkEventKeys.id: NetworkEventDefaults.id, + NetworkEventKeys.title: NetworkEventDefaults.title, + NetworkEventKeys.pageTimings: { + NetworkEventKeys.onContentLoad: NetworkEventDefaults.onContentLoad, + NetworkEventKeys.onLoad: NetworkEventDefaults.onLoad, + }, + }, + ]; + +// Build the entries + final entries = httpRequests.map((e) { + final requestCookies = e.requestCookies.map((cookie) { + return { + NetworkEventKeys.name: cookie.name, + NetworkEventKeys.value: cookie.value, + 'path': cookie.path, + 'domain': cookie.domain, + 'expires': cookie.expires?.toUtc().toIso8601String(), + 'httpOnly': cookie.httpOnly, + 'secure': cookie.secure, + }; + }).toList(); + + final requestHeaders = e.requestHeaders?.entries.map((header) { + var value = header.value; + if (value is List) { + value = value.first; + } + return { + NetworkEventKeys.name: header.key, + NetworkEventKeys.value: value, + }; + }).toList(); + + final queryString = Uri.parse(e.uri).queryParameters.entries.map((param) { + return { + NetworkEventKeys.name: param.key, + NetworkEventKeys.value: param.value, + }; + }).toList(); + + final responseCookies = e.responseCookies.map((cookie) { + return { + NetworkEventKeys.name: cookie.name, + NetworkEventKeys.value: cookie.value, + 'path': cookie.path, + 'domain': cookie.domain, + 'expires': cookie.expires?.toUtc().toIso8601String(), + 'httpOnly': cookie.httpOnly, + 'secure': cookie.secure, + }; + }).toList(); + + final responseHeaders = e.responseHeaders?.entries.map((header) { + var value = header.value; + if (value is List) { + value = value.first; + } + return { + NetworkEventKeys.name: header.key, + NetworkEventKeys.value: value, + }; + }).toList(); + + return { + NetworkEventKeys.pageref: NetworkEventDefaults.id, + NetworkEventKeys.startedDateTime: + e.startTimestamp.toUtc().toIso8601String(), + NetworkEventKeys.time: e.duration?.inMilliseconds, + NetworkEventKeys.request: { + NetworkEventKeys.method: e.method.toUpperCase(), + NetworkEventKeys.url: e.uri.toString(), + NetworkEventKeys.httpVersion: NetworkEventDefaults.httpVersion, + NetworkEventKeys.cookies: requestCookies, + NetworkEventKeys.headers: requestHeaders, + NetworkEventKeys.queryString: queryString, + NetworkEventKeys.postData: { + NetworkEventKeys.mimeType: e.contentType, + NetworkEventKeys.text: e.requestBody, + }, + NetworkEventKeys.headersSize: NetworkEventDefaults.headersSize, + NetworkEventKeys.bodySize: NetworkEventDefaults.bodySize, + }, + NetworkEventKeys.response: { + NetworkEventKeys.status: e.status, + NetworkEventKeys.statusText: '', + NetworkEventKeys.httpVersion: NetworkEventDefaults.responseHttpVersion, + NetworkEventKeys.cookies: responseCookies, + NetworkEventKeys.headers: responseHeaders, + NetworkEventKeys.content: { + NetworkEventKeys.size: e.responseBody?.length, + NetworkEventKeys.mimeType: e.type, + NetworkEventKeys.text: e.responseBody, + }, + NetworkEventKeys.redirectURL: '', + NetworkEventKeys.headersSize: NetworkEventDefaults.headersSize, + NetworkEventKeys.bodySize: NetworkEventDefaults.bodySize, + }, + NetworkEventKeys.cache: {}, + NetworkEventKeys.timings: { + NetworkEventKeys.blocked: NetworkEventDefaults.blocked, + NetworkEventKeys.dns: NetworkEventDefaults.dns, + NetworkEventKeys.connect: NetworkEventDefaults.connect, + NetworkEventKeys.send: NetworkEventDefaults.send, + NetworkEventKeys.wait: e.duration!.inMilliseconds - 2, + NetworkEventKeys.receive: NetworkEventDefaults.receive, + NetworkEventKeys.ssl: NetworkEventDefaults.ssl, + }, + NetworkEventKeys.serverIPAddress: NetworkEventDefaults.serverIPAddress, + NetworkEventKeys.connection: e.hashCode.toString(), + NetworkEventKeys.comment: '', + }; + }).toList(); + +// Assemble the final HAR object + return { + NetworkEventKeys.log: { + NetworkEventKeys.version: NetworkEventDefaults.logVersion, + NetworkEventKeys.creator: creator, + NetworkEventKeys.pages: pages, + NetworkEventKeys.entries: entries, + }, + }; +} 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 a38be27302e..e6637a0e765 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -9,10 +9,19 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:vm_service/vm_service.dart'; -import '../../../devtools_app.dart'; import '../../shared/config_specific/import_export/import_export.dart'; 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'; +import '../../shared/utils.dart'; +import 'har_builder.dart'; +import 'network_model.dart'; +import 'network_screen.dart'; import 'network_service.dart'; final _exportController = ExportController(); @@ -66,132 +75,9 @@ class NetworkController extends DisposableController } try { - if (httpRequests!.isNotEmpty) { - final har = { - 'log': { - 'version': '1.2', - 'creator': { - 'name': 'flutter_tool', - 'version': '0.0.2', - }, - 'pages': [ - { - 'startedDateTime': httpRequests?.first.startTimestamp - .toUtc() - .toIso8601String(), - 'id': 'page_0', - 'title': 'FlutterCapture', - 'pageTimings': { - 'onContentLoad': -1, - 'onLoad': -1, - }, - }, - ], - 'entries': httpRequests - ?.map( - (e) => { - 'pageref': 'page_0', - 'startedDateTime': - e.startTimestamp.toUtc().toIso8601String(), - 'time': e.duration?.inMilliseconds, - 'request': { - 'method': e.method.toUpperCase(), - 'url': e.uri.toString(), - 'httpVersion': 'HTTP/1.1', - 'cookies': e.requestCookies - .map( - (e) => { - 'name': e.name, - 'value': e.value, - 'path': e.path, - 'domain': e.domain, - 'expires': e.expires?.toUtc().toIso8601String(), - 'httpOnly': e.httpOnly, - 'secure': e.secure, - }, - ) - .toList(), - 'headers': e.requestHeaders?.entries.map((h) { - var value = h.value; - if (value is List) { - value = value.first; - } - return { - 'name': h.key, - 'value': value, - }; - }).toList(), - 'queryString': Uri.parse(e.uri) - .queryParameters - .entries - .map( - (q) => { - 'name': q.key, - 'value': q.value, - }, - ) - .toList(), - 'postData': { - 'mimeType': e.contentType, - 'text': e.requestBody, - }, - 'headersSize': -1, - 'bodySize': -1, - }, - 'response': { - 'status': e.status, - 'statusText': '', - 'httpVersion': 'http/2.0', - 'cookies': e.responseCookies - .map( - (e) => { - 'name': e.name, - 'value': e.value, - 'path': e.path, - 'domain': e.domain, - 'expires': e.expires?.toUtc().toIso8601String(), - 'httpOnly': e.httpOnly, - 'secure': e.secure, - }, - ) - .toList(), - 'headers': e.responseHeaders?.entries.map((h) { - var value = h.value; - if (value is List) { - value = value.first; - } - return { - 'name': h.key, - 'value': value, - }; - }).toList(), - 'content': { - 'size': e.responseBody?.length, - 'mimeType': e.type, - 'text': e.responseBody, - }, - 'redirectURL': '', - 'headersSize': -1, - 'bodySize': -1, - }, - 'cache': {}, - 'timings': { - 'blocked': -1, - 'dns': -1, - 'connect': -1, - 'send': 1, - 'wait': e.duration!.inMilliseconds - 2, - 'receive': 1, - 'ssl': -1, - }, - 'serverIPAddress': '10.0.0.1', - 'connection': e.hashCode.toString(), - 'comment': '', - }, - ) - .toList(), - }, - }; + if (httpRequests != null && httpRequests!.isNotEmpty) { + // Build the HAR object + final har = buildHar(httpRequests!); debugPrint('data is ${json.encode(har)}'); return _exportController.downloadFile( json.encode(har), diff --git a/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart index f99248415d8..c193b372528 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart @@ -1,5 +1,79 @@ +// 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. + part of '../constants.dart'; class NetworkEvent { static const networkDownloadHar = 'networkDownloadHar'; } + +class NetworkEventKeys { + static const log = 'log'; + static const version = 'version'; + static const creator = 'creator'; + static const name = 'name'; + static const creatorVersion = 'version'; + static const pages = 'pages'; + static const startedDateTime = 'startedDateTime'; + static const id = 'id'; + static const title = 'title'; + static const pageTimings = 'pageTimings'; + static const onContentLoad = 'onContentLoad'; + static const onLoad = 'onLoad'; + static const entries = 'entries'; + static const pageref = 'pageref'; + static const time = 'time'; + static const request = 'request'; + static const method = 'method'; + static const url = 'url'; + static const httpVersion = 'httpVersion'; + static const cookies = 'cookies'; + static const headers = 'headers'; + static const queryString = 'queryString'; + static const postData = 'postData'; + static const mimeType = 'mimeType'; + static const text = 'text'; + static const headersSize = 'headersSize'; + static const bodySize = 'bodySize'; + static const response = 'response'; + static const status = 'status'; + static const statusText = 'statusText'; + static const content = 'content'; + static const size = 'size'; + static const redirectURL = 'redirectURL'; + static const cache = 'cache'; + static const timings = 'timings'; + static const blocked = 'blocked'; + static const dns = 'dns'; + static const connect = 'connect'; + static const send = 'send'; + static const wait = 'wait'; + static const receive = 'receive'; + static const ssl = 'ssl'; + static const serverIPAddress = 'serverIPAddress'; + static const connection = 'connection'; + static const comment = 'comment'; + static const value = 'value'; +} + +class NetworkEventDefaults { + static const logVersion = '1.2'; + static const creatorName = 'devtools'; + static const creatorVersion = '0.0.2'; + static const id = 'page_0'; + static const title = 'FlutterCapture'; + static const onContentLoad = -1; + static const onLoad = -1; + static const httpVersion = 'HTTP/1.1'; + static const responseHttpVersion = 'http/2.0'; + static const headersSize = -1; + static const bodySize = -1; + static const blocked = -1; + static const dns = -1; + static const connect = -1; + static const send = 1; + static const receive = 1; + static const ssl = -1; + static const serverIPAddress = '10.0.0.1'; +} From ef3a637e0aefb5ed3ec7d3ed8c848814838a3a6e Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 24 Jun 2024 22:40:15 +0530 Subject: [PATCH 04/12] added doc comments for buildHar --- .../lib/src/screens/network/har_builder.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/devtools_app/lib/src/screens/network/har_builder.dart b/packages/devtools_app/lib/src/screens/network/har_builder.dart index 6c21bd731dd..012ced74d11 100644 --- a/packages/devtools_app/lib/src/screens/network/har_builder.dart +++ b/packages/devtools_app/lib/src/screens/network/har_builder.dart @@ -1,6 +1,20 @@ import '../../../devtools_app.dart'; import '../../shared/analytics/constants.dart'; +/// Builds a HAR (HTTP Archive) object from a list of HTTP requests. +/// +/// The HAR format is a JSON-based format used for logging a web browser's +/// interaction with a site. It is useful for web performance analysis and +/// debugging. This function constructs the HAR object based on the 1.2 +/// specification. +/// +/// For more details on the HAR format, see the [HAR 1.2 Specification](https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md). +/// +/// Parameters: +/// - [httpRequests]: A list of DartIOHttpRequestData data. +/// +/// Returns: +/// - A Map representing the HAR object. Map buildHar(List httpRequests) { // Build the creator final creator = { From 058e0535485aa144c333aa4300215df2101621cd Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 24 Jun 2024 22:48:12 +0530 Subject: [PATCH 05/12] exportController moved to method scope --- .../lib/src/screens/network/network_controller.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 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 e6637a0e765..89ebe25d2ba 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -24,7 +24,6 @@ import 'network_model.dart'; import 'network_screen.dart'; import 'network_service.dart'; -final _exportController = ExportController(); List? httpRequests; /// Different types of Network Response which can be used to visualise response @@ -66,6 +65,8 @@ class NetworkController extends DisposableController } String? exportAsHarFile() { + final exportController = ExportController(); + httpRequests = filteredData.value.whereType().toList(); @@ -79,7 +80,7 @@ class NetworkController extends DisposableController // Build the HAR object final har = buildHar(httpRequests!); debugPrint('data is ${json.encode(har)}'); - return _exportController.downloadFile( + return exportController.downloadFile( json.encode(har), type: ExportFileType.har, ); From ae292ad1bdd4bec9cb2a6ada3d15b1439bbbb8b5 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 24 Jun 2024 23:01:53 +0530 Subject: [PATCH 06/12] httpRequests moved to NetworkController class --- .../lib/src/screens/network/network_controller.dart | 13 ++++++------- 1 file changed, 6 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 89ebe25d2ba..99f3f1b8bb3 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -24,8 +24,6 @@ import 'network_model.dart'; import 'network_screen.dart'; import 'network_service.dart'; -List? httpRequests; - /// Different types of Network Response which can be used to visualise response /// on Response tab enum NetworkResponseViewType { @@ -63,22 +61,23 @@ class NetworkController extends DisposableController ); subscribeToFilterChanges(); } + List? _httpRequests; String? exportAsHarFile() { final exportController = ExportController(); - httpRequests = + _httpRequests = filteredData.value.whereType().toList(); - if (httpRequests!.isEmpty) { + if (_httpRequests!.isEmpty) { debugPrint('No valid request data to export'); return ''; } try { - if (httpRequests != null && httpRequests!.isNotEmpty) { + if (_httpRequests != null && _httpRequests!.isNotEmpty) { // Build the HAR object - final har = buildHar(httpRequests!); + final har = buildHar(_httpRequests!); debugPrint('data is ${json.encode(har)}'); return exportController.downloadFile( json.encode(har), @@ -397,7 +396,7 @@ class NetworkController extends DisposableController @override OfflineScreenData prepareOfflineScreenData() { - debugPrint('offline data - httpRequests are $httpRequests'); + debugPrint('offline data - httpRequests are $_httpRequests'); return OfflineScreenData( screenId: NetworkScreen.id, //TODO deserialize har data and pass here From 1b9e483b40b7e8c5e6a751a2ff062cd8c627ca77 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Wed, 26 Jun 2024 21:49:27 +0530 Subject: [PATCH 07/12] code comments resolved --- .../lib/src/screens/network/har_builder.dart | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/har_builder.dart b/packages/devtools_app/lib/src/screens/network/har_builder.dart index 012ced74d11..39d8c2d07c2 100644 --- a/packages/devtools_app/lib/src/screens/network/har_builder.dart +++ b/packages/devtools_app/lib/src/screens/network/har_builder.dart @@ -1,10 +1,15 @@ -import '../../../devtools_app.dart'; +// Copyright 2019 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 '../../shared/analytics/constants.dart'; +import '../../shared/http/http_request_data.dart'; + /// Builds a HAR (HTTP Archive) object from a list of HTTP requests. /// /// The HAR format is a JSON-based format used for logging a web browser's -/// interaction with a site. It is useful for web performance analysis and +/// interaction with a site. It is useful for performance analysis and /// debugging. This function constructs the HAR object based on the 1.2 /// specification. /// @@ -15,31 +20,31 @@ import '../../shared/analytics/constants.dart'; /// /// Returns: /// - A Map representing the HAR object. -Map buildHar(List httpRequests) { -// Build the creator - final creator = { +Map buildHar(List httpRequests) { + // Build the creator + final creator = { NetworkEventKeys.name: NetworkEventDefaults.creatorName, NetworkEventKeys.creatorVersion: NetworkEventDefaults.creatorVersion, }; -// Build the pages - final pages = [ + // Build the pages + final pages = >[ { NetworkEventKeys.startedDateTime: httpRequests.first.startTimestamp.toUtc().toIso8601String(), NetworkEventKeys.id: NetworkEventDefaults.id, NetworkEventKeys.title: NetworkEventDefaults.title, - NetworkEventKeys.pageTimings: { + NetworkEventKeys.pageTimings: { NetworkEventKeys.onContentLoad: NetworkEventDefaults.onContentLoad, NetworkEventKeys.onLoad: NetworkEventDefaults.onLoad, }, }, ]; -// Build the entries + // Build the entries final entries = httpRequests.map((e) { final requestCookies = e.requestCookies.map((cookie) { - return { + return { NetworkEventKeys.name: cookie.name, NetworkEventKeys.value: cookie.value, 'path': cookie.path, @@ -55,21 +60,21 @@ Map buildHar(List httpRequests) { if (value is List) { value = value.first; } - return { + return { NetworkEventKeys.name: header.key, NetworkEventKeys.value: value, }; }).toList(); final queryString = Uri.parse(e.uri).queryParameters.entries.map((param) { - return { + return { NetworkEventKeys.name: param.key, NetworkEventKeys.value: param.value, }; }).toList(); final responseCookies = e.responseCookies.map((cookie) { - return { + return { NetworkEventKeys.name: cookie.name, NetworkEventKeys.value: cookie.value, 'path': cookie.path, @@ -85,38 +90,38 @@ Map buildHar(List httpRequests) { if (value is List) { value = value.first; } - return { + return { NetworkEventKeys.name: header.key, NetworkEventKeys.value: value, }; }).toList(); - return { + return { NetworkEventKeys.pageref: NetworkEventDefaults.id, NetworkEventKeys.startedDateTime: e.startTimestamp.toUtc().toIso8601String(), NetworkEventKeys.time: e.duration?.inMilliseconds, - NetworkEventKeys.request: { + NetworkEventKeys.request: { NetworkEventKeys.method: e.method.toUpperCase(), NetworkEventKeys.url: e.uri.toString(), NetworkEventKeys.httpVersion: NetworkEventDefaults.httpVersion, NetworkEventKeys.cookies: requestCookies, NetworkEventKeys.headers: requestHeaders, NetworkEventKeys.queryString: queryString, - NetworkEventKeys.postData: { + NetworkEventKeys.postData: { NetworkEventKeys.mimeType: e.contentType, NetworkEventKeys.text: e.requestBody, }, NetworkEventKeys.headersSize: NetworkEventDefaults.headersSize, NetworkEventKeys.bodySize: NetworkEventDefaults.bodySize, }, - NetworkEventKeys.response: { + NetworkEventKeys.response: { NetworkEventKeys.status: e.status, NetworkEventKeys.statusText: '', NetworkEventKeys.httpVersion: NetworkEventDefaults.responseHttpVersion, NetworkEventKeys.cookies: responseCookies, NetworkEventKeys.headers: responseHeaders, - NetworkEventKeys.content: { + NetworkEventKeys.content: { NetworkEventKeys.size: e.responseBody?.length, NetworkEventKeys.mimeType: e.type, NetworkEventKeys.text: e.responseBody, @@ -125,8 +130,8 @@ Map buildHar(List httpRequests) { NetworkEventKeys.headersSize: NetworkEventDefaults.headersSize, NetworkEventKeys.bodySize: NetworkEventDefaults.bodySize, }, - NetworkEventKeys.cache: {}, - NetworkEventKeys.timings: { + NetworkEventKeys.cache: {}, + NetworkEventKeys.timings: { NetworkEventKeys.blocked: NetworkEventDefaults.blocked, NetworkEventKeys.dns: NetworkEventDefaults.dns, NetworkEventKeys.connect: NetworkEventDefaults.connect, @@ -141,9 +146,9 @@ Map buildHar(List httpRequests) { }; }).toList(); -// Assemble the final HAR object - return { - NetworkEventKeys.log: { + // Assemble the final HAR object + return { + NetworkEventKeys.log: { NetworkEventKeys.version: NetworkEventDefaults.logVersion, NetworkEventKeys.creator: creator, NetworkEventKeys.pages: pages, From 5c306c1c8f6b599a1931dcbed08d8febd86fb2cb Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Thu, 27 Jun 2024 12:04:56 +0530 Subject: [PATCH 08/12] removed pages, pageref and serverIPAddress --- .../lib/src/screens/network/har_builder.dart | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/har_builder.dart b/packages/devtools_app/lib/src/screens/network/har_builder.dart index 39d8c2d07c2..bd7886f4119 100644 --- a/packages/devtools_app/lib/src/screens/network/har_builder.dart +++ b/packages/devtools_app/lib/src/screens/network/har_builder.dart @@ -27,20 +27,6 @@ Map buildHar(List httpRequests) { NetworkEventKeys.creatorVersion: NetworkEventDefaults.creatorVersion, }; - // Build the pages - final pages = >[ - { - NetworkEventKeys.startedDateTime: - httpRequests.first.startTimestamp.toUtc().toIso8601String(), - NetworkEventKeys.id: NetworkEventDefaults.id, - NetworkEventKeys.title: NetworkEventDefaults.title, - NetworkEventKeys.pageTimings: { - NetworkEventKeys.onContentLoad: NetworkEventDefaults.onContentLoad, - NetworkEventKeys.onLoad: NetworkEventDefaults.onLoad, - }, - }, - ]; - // Build the entries final entries = httpRequests.map((e) { final requestCookies = e.requestCookies.map((cookie) { @@ -97,7 +83,6 @@ Map buildHar(List httpRequests) { }).toList(); return { - NetworkEventKeys.pageref: NetworkEventDefaults.id, NetworkEventKeys.startedDateTime: e.startTimestamp.toUtc().toIso8601String(), NetworkEventKeys.time: e.duration?.inMilliseconds, @@ -140,7 +125,6 @@ Map buildHar(List httpRequests) { NetworkEventKeys.receive: NetworkEventDefaults.receive, NetworkEventKeys.ssl: NetworkEventDefaults.ssl, }, - NetworkEventKeys.serverIPAddress: NetworkEventDefaults.serverIPAddress, NetworkEventKeys.connection: e.hashCode.toString(), NetworkEventKeys.comment: '', }; @@ -151,7 +135,6 @@ Map buildHar(List httpRequests) { NetworkEventKeys.log: { NetworkEventKeys.version: NetworkEventDefaults.logVersion, NetworkEventKeys.creator: creator, - NetworkEventKeys.pages: pages, NetworkEventKeys.entries: entries, }, }; From 5d8fc7552206753399f04a0ec3a7326cdcf07993 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Thu, 27 Jun 2024 13:34:13 +0530 Subject: [PATCH 09/12] added header size and body size, creatorVersion set --- .../lib/src/screens/network/har_builder.dart | 43 ++++++++++++++++--- .../constants/_network_constants.dart | 6 --- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/har_builder.dart b/packages/devtools_app/lib/src/screens/network/har_builder.dart index bd7886f4119..d6ac638b846 100644 --- a/packages/devtools_app/lib/src/screens/network/har_builder.dart +++ b/packages/devtools_app/lib/src/screens/network/har_builder.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; +import '../../shared/utils.dart'; import '../../shared/analytics/constants.dart'; import '../../shared/http/http_request_data.dart'; @@ -24,7 +26,7 @@ Map buildHar(List httpRequests) { // Build the creator final creator = { NetworkEventKeys.name: NetworkEventDefaults.creatorName, - NetworkEventKeys.creatorVersion: NetworkEventDefaults.creatorVersion, + NetworkEventKeys.creatorVersion: devToolsVersion, }; // Build the entries @@ -86,6 +88,7 @@ Map buildHar(List httpRequests) { NetworkEventKeys.startedDateTime: e.startTimestamp.toUtc().toIso8601String(), NetworkEventKeys.time: e.duration?.inMilliseconds, + // Request NetworkEventKeys.request: { NetworkEventKeys.method: e.method.toUpperCase(), NetworkEventKeys.url: e.uri.toString(), @@ -97,9 +100,10 @@ Map buildHar(List httpRequests) { NetworkEventKeys.mimeType: e.contentType, NetworkEventKeys.text: e.requestBody, }, - NetworkEventKeys.headersSize: NetworkEventDefaults.headersSize, - NetworkEventKeys.bodySize: NetworkEventDefaults.bodySize, + NetworkEventKeys.headersSize: _calculateHeadersSize(e.requestHeaders), + NetworkEventKeys.bodySize: _calculateBodySize(e.requestBody), }, + // Response NetworkEventKeys.response: { NetworkEventKeys.status: e.status, NetworkEventKeys.statusText: '', @@ -112,9 +116,10 @@ Map buildHar(List httpRequests) { NetworkEventKeys.text: e.responseBody, }, NetworkEventKeys.redirectURL: '', - NetworkEventKeys.headersSize: NetworkEventDefaults.headersSize, - NetworkEventKeys.bodySize: NetworkEventDefaults.bodySize, + NetworkEventKeys.headersSize: _calculateHeadersSize(e.responseHeaders), + NetworkEventKeys.bodySize: _calculateBodySize(e.responseBody), }, + // Cache NetworkEventKeys.cache: {}, NetworkEventKeys.timings: { NetworkEventKeys.blocked: NetworkEventDefaults.blocked, @@ -139,3 +144,31 @@ Map buildHar(List httpRequests) { }, }; } + +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 == null || requestBody.isEmpty) { + return 0; + } + return utf8.encode(requestBody).length; +} diff --git a/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart index c193b372528..5c66632d6f2 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants/_network_constants.dart @@ -60,20 +60,14 @@ class NetworkEventKeys { class NetworkEventDefaults { static const logVersion = '1.2'; static const creatorName = 'devtools'; - static const creatorVersion = '0.0.2'; - static const id = 'page_0'; - static const title = 'FlutterCapture'; static const onContentLoad = -1; static const onLoad = -1; static const httpVersion = 'HTTP/1.1'; static const responseHttpVersion = 'http/2.0'; - static const headersSize = -1; - static const bodySize = -1; static const blocked = -1; static const dns = -1; static const connect = -1; static const send = 1; static const receive = 1; static const ssl = -1; - static const serverIPAddress = '10.0.0.1'; } From 16faad7ee139b4f7eca7f076f9d16ed595cdf1da Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Thu, 27 Jun 2024 13:42:02 +0530 Subject: [PATCH 10/12] organized imports --- .../devtools_app/lib/src/screens/network/har_builder.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/network/har_builder.dart b/packages/devtools_app/lib/src/screens/network/har_builder.dart index d6ac638b846..b728b590b6b 100644 --- a/packages/devtools_app/lib/src/screens/network/har_builder.dart +++ b/packages/devtools_app/lib/src/screens/network/har_builder.dart @@ -3,10 +3,10 @@ // found in the LICENSE file. import 'dart:convert'; -import '../../shared/utils.dart'; -import '../../shared/analytics/constants.dart'; +import '../../shared/analytics/constants.dart'; import '../../shared/http/http_request_data.dart'; +import '../../shared/utils.dart'; /// Builds a HAR (HTTP Archive) object from a list of HTTP requests. /// From 2c8093ed3f93628062c9fb4f0c969f3f152e5cc6 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Sat, 6 Jul 2024 13:50:33 +0530 Subject: [PATCH 11/12] added offline support for network screen --- .../screens/network/network_controller.dart | 26 ++++-- .../src/screens/network/network_screen.dart | 92 ++++++++++++------- .../src/shared/http/http_request_data.dart | 54 +++++++++++ .../lib/src/shared/primitives/utils.dart | 6 ++ .../devtools_app/lib/src/shared/screen.dart | 1 + 5 files changed, 135 insertions(+), 44 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 99f3f1b8bb3..973537123bf 100644 --- a/packages/devtools_app/lib/src/screens/network/network_controller.dart +++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart @@ -61,23 +61,22 @@ class NetworkController extends DisposableController ); subscribeToFilterChanges(); } - List? _httpRequests; + List? httpRequests; String? exportAsHarFile() { final exportController = ExportController(); - - _httpRequests = + httpRequests = filteredData.value.whereType().toList(); - if (_httpRequests!.isEmpty) { + if (httpRequests!.isEmpty) { debugPrint('No valid request data to export'); return ''; } try { - if (_httpRequests != null && _httpRequests!.isNotEmpty) { + if (httpRequests != null && httpRequests!.isNotEmpty) { // Build the HAR object - final har = buildHar(_httpRequests!); + final har = buildHar(httpRequests!); debugPrint('data is ${json.encode(har)}'); return exportController.downloadFile( json.encode(har), @@ -396,15 +395,24 @@ class NetworkController extends DisposableController @override OfflineScreenData prepareOfflineScreenData() { - debugPrint('offline data - httpRequests are $_httpRequests'); + final requests = + filteredData.value.whereType().toList(); return OfflineScreenData( screenId: NetworkScreen.id, - //TODO deserialize har data and pass here - data: {}, + data: convertRequestsToMap(requests), ); } } +Map convertRequestsToMap( + List? requests, +) { + if (requests == null) return {}; + return { + 'requests': requests.map((request) => request.toMap()).toList(), + }; +} + /// Class for managing the set of all current websocket requests, and /// http profile requests. class CurrentNetworkRequests extends ValueNotifier> { 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 edb0bea51a2..cd4aa5e423d 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -10,24 +10,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../../devtools_app.dart'; 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'; -import '../../shared/screen.dart'; import '../../shared/table/table.dart'; import '../../shared/table/table_data.dart'; -import '../../shared/ui/filter.dart'; -import '../../shared/ui/search.dart'; import '../../shared/ui/utils.dart'; -import '../../shared/utils.dart'; -import 'network_controller.dart'; -import 'network_model.dart'; import 'network_request_inspector.dart'; +import 'network_singleton.dart'; class NetworkScreen extends Screen { NetworkScreen() : super.fromMetaData(ScreenMetaData.network); @@ -116,6 +108,7 @@ class _NetworkScreenBodyState extends State @override void initState() { super.initState(); + addAutoDisposeListener(offlineDataController.showingOfflineData); ga.screen(NetworkScreen.id); } @@ -123,19 +116,41 @@ class _NetworkScreenBodyState extends State void didChangeDependencies() { super.didChangeDependencies(); if (!initController()) return; - unawaited(controller.startRecording()); - - cancelListeners(); + try { + if (offlineDataController.showingOfflineData.value == true) { + loadOfflineData(offlineDataController.offlineDataJson); + } + if (!offlineDataController.showingOfflineData.value) { + debugPrint('started recording'); + unawaited(controller.startRecording()); + } + cancelListeners(); + if (!offlineDataController.showingOfflineData.value) { + 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']['requests'] as List; + final requests = requestsMap + .map((e) => DartIOHttpRequestData.fromJson(e as Map)) + .toList(); + controller.filteredData + ..clear() + ..addAll(requests); } @override @@ -143,7 +158,10 @@ class _NetworkScreenBodyState extends State // TODO(kenz): this won't work well if we eventually have multiple clients // that want to listen to network data. super.dispose(); - unawaited(controller.stopRecording()); + if (!(NetworkSingleton.instance.offlineMode ?? false)) { + debugPrint('stopped recording'); + unawaited(controller.stopRecording()); + } } @override @@ -200,10 +218,12 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> _requests = widget.controller.requests.value; }); }); - _filteredRequests = widget.controller.filteredData.value; + NetworkSingleton.instance.filteredData = + widget.controller.filteredData.value; addAutoDisposeListener(widget.controller.filteredData, () { setState(() { - _filteredRequests = widget.controller.filteredData.value; + NetworkSingleton.instance.filteredData = + widget.controller.filteredData.value; }); }); } @@ -211,7 +231,7 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> @override Widget build(BuildContext context) { final screenWidth = ScreenSize(context).width; - final hasRequests = _filteredRequests.isNotEmpty; + final hasRequests = NetworkSingleton.instance.filteredData?.isNotEmpty; return Row( children: [ StartStopRecordingButton( @@ -234,19 +254,20 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> onPressed: widget.controller.clear, ), const SizedBox(width: defaultSpacing), - DownloadButton( - minScreenWidthForTextBeforeScaling: - _NetworkProfilerControls._includeTextWidth, - onPressed: widget.controller.exportAsHarFile, - gaScreen: gac.network, - gaSelection: gac.NetworkEvent.networkDownloadHar, - ), + if (!offlineDataController.showingOfflineData.value) + DownloadButton( + minScreenWidthForTextBeforeScaling: + _NetworkProfilerControls._includeTextWidth, + onPressed: widget.controller.exportAsHarFile, + gaScreen: gac.network, + gaSelection: gac.NetworkEvent.networkDownloadHar, + ), const SizedBox(width: defaultSpacing), const Expanded(child: SizedBox()), // TODO(kenz): fix focus issue when state is refreshed SearchField( searchController: widget.controller, - searchFieldEnabled: hasRequests, + searchFieldEnabled: hasRequests ?? false, searchFieldWidth: screenWidth <= MediaSize.xs ? defaultSearchFieldWidth : wideSearchFieldWidth, @@ -254,7 +275,8 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls> const SizedBox(width: denseSpacing), DevToolsFilterButton( onPressed: _showFilterDialog, - isFilterActive: _filteredRequests.length != _requests.length, + isFilterActive: NetworkSingleton.instance.filteredData?.length != + _requests.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 1196793b4d7..b13e412f5cf 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 @@ -19,6 +19,17 @@ final _log = Logger('http_request_data'); /// Used to represent an instant event emitted during an HTTP request. class DartIOHttpInstantEvent { + factory DartIOHttpInstantEvent.fromJson(Map json) { + final event = HttpProfileRequestEvent( + event: json['event'] ?? '', + timestamp: DateTime.parse(json['timestamp']), + // Populate other necessary fields + ); + + final instantEvent = DartIOHttpInstantEvent._(event); + instantEvent._timeRange = TimeRange.fromJson(json['timeRange']); + return instantEvent; + } DartIOHttpInstantEvent._(this._event); final HttpProfileRequestEvent _event; @@ -37,6 +48,26 @@ class DartIOHttpInstantEvent { /// An abstraction of an HTTP request made through dart:io. class DartIOHttpRequestData extends NetworkRequest { + factory DartIOHttpRequestData.fromJson(Map json) { + final request = HttpProfileRequestRef( + id: json['id'], + method: json['method'], + uri: Uri.parse(json['uri']), + isolateId: '123', + events: [], + startTime: DateTime.now(), + ); + + final data = DartIOHttpRequestData(request); + data._responseBody = json['responseBody'] ?? ''; + data._requestBody = json['requestBody'] ?? ''; + // data._instantEvents = (json['instantEvents'] as List) + // .map((e) => DartIOHttpInstantEvent.fromJson(e as Map)) + // .toList(); + + // Populate other fields as needed + return data; + } DartIOHttpRequestData( this._request, { bool requestFullDataFromVmService = true, @@ -319,4 +350,27 @@ class DartIOHttpRequestData extends NetworkRequest { port, startTimestamp, ); + + Map toMap() { + return { + 'id': id, + 'method': method, + 'uri': uri, + 'status': 'status', + 'type': type, + 'duration': 'duration?.inMilliseconds', + 'startTimestamp': startTimestamp.toIso8601String(), + 'endTimestamp': endTimestamp!, + 'requestHeaders': requestHeaders ?? {}, + 'responseHeaders': responseHeaders ?? {}, + 'requestBody': requestBody ?? '', + 'responseBody': responseBody ?? '', + 'inProgress': inProgress, + 'didFail': didFail, + 'general': general, + 'requestCookies': requestCookies.map((c) => c.toString()).toList(), + 'responseCookies': responseCookies.map((c) => c.toString()).toList(), + 'instantEvents': 'instantEvents.map((e) => ()).toList()', + }; + } } diff --git a/packages/devtools_app/lib/src/shared/primitives/utils.dart b/packages/devtools_app/lib/src/shared/primitives/utils.dart index b5e453c47f7..488b355a790 100644 --- a/packages/devtools_app/lib/src/shared/primitives/utils.dart +++ b/packages/devtools_app/lib/src/shared/primitives/utils.dart @@ -454,6 +454,12 @@ enum TimeUnit { class TimeRange { TimeRange({this.singleAssignment = true}); + factory TimeRange.fromJson(Map json) { + return TimeRange( + singleAssignment: true, + ); + } + factory TimeRange.offset({ required TimeRange original, required Duration offset, diff --git a/packages/devtools_app/lib/src/shared/screen.dart b/packages/devtools_app/lib/src/shared/screen.dart index 3e17e38ded5..716d225bb30 100644 --- a/packages/devtools_app/lib/src/shared/screen.dart +++ b/packages/devtools_app/lib/src/shared/screen.dart @@ -75,6 +75,7 @@ enum ScreenMetaData { icon: Icons.network_check, requiresDartVm: true, tutorialVideoTimestamp: '?t=547', + worksWithOfflineData: true, ), logging( 'logging', From 7c1415b7f5186fcd411be82633af7e2d26468eb2 Mon Sep 17 00:00:00 2001 From: hrajwade96 Date: Mon, 8 Jul 2024 22:42:01 +0530 Subject: [PATCH 12/12] added feature flag network_offline_experiment --- packages/devtools_app/lib/src/shared/feature_flags.dart | 9 +++++++++ packages/devtools_app/lib/src/shared/screen.dart | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart index 41ca89d482c..6976578862f 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 _kMemoryOfflineExperiment = bool.fromEnvironment('memory_offline_experiment'); +const _kNetworkOfflineExperiment = + bool.fromEnvironment('network_offline_experiment'); + // 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,12 @@ abstract class FeatureFlags { static const memoryOffline = _kMemoryOfflineExperiment; // requires special handling because it needs to be const + /// Flag to enable offline data on network screen. + /// + /// https://github.com/flutter/devtools/issues/3806 + static const networkOffline = + _kNetworkOfflineExperiment; // requires special handling because it needs to be const + /// Flag to enable the deep link validation tooling in DevTools, both for the /// DevTools screen and the standalone tool for IDE embedding. /// diff --git a/packages/devtools_app/lib/src/shared/screen.dart b/packages/devtools_app/lib/src/shared/screen.dart index 716d225bb30..f63427a87b1 100644 --- a/packages/devtools_app/lib/src/shared/screen.dart +++ b/packages/devtools_app/lib/src/shared/screen.dart @@ -75,7 +75,8 @@ enum ScreenMetaData { icon: Icons.network_check, requiresDartVm: true, tutorialVideoTimestamp: '?t=547', - worksWithOfflineData: true, + // ignore: avoid_redundant_argument_values, false positive + worksWithOfflineData: FeatureFlags.networkOffline, ), logging( 'logging',