Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions packages/devtools_app/lib/src/screens/network/har_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// 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 'dart:convert';

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.
///
/// The HAR format is a JSON-based format used for logging a web browser's
/// interaction with a site. It is useful for 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<String, Object?> buildHar(List<DartIOHttpRequestData> httpRequests) {
// Build the creator
final creator = <String, Object?>{
NetworkEventKeys.name: NetworkEventDefaults.creatorName,
NetworkEventKeys.creatorVersion: devToolsVersion,
};

// Build the entries
final entries = httpRequests.map((e) {
final requestCookies = e.requestCookies.map((cookie) {
return <String, Object?>{
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 <String, Object?>{
NetworkEventKeys.name: header.key,
NetworkEventKeys.value: value,
};
}).toList();

final queryString = Uri.parse(e.uri).queryParameters.entries.map((param) {
return <String, Object?>{
NetworkEventKeys.name: param.key,
NetworkEventKeys.value: param.value,
};
}).toList();

final responseCookies = e.responseCookies.map((cookie) {
return <String, Object?>{
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 <String, Object?>{
NetworkEventKeys.name: header.key,
NetworkEventKeys.value: value,
};
}).toList();

return <String, Object?>{
NetworkEventKeys.startedDateTime:
e.startTimestamp.toUtc().toIso8601String(),
NetworkEventKeys.time: e.duration?.inMilliseconds,
// Request
NetworkEventKeys.request: <String, Object?>{
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: <String, Object?>{
NetworkEventKeys.mimeType: e.contentType,
NetworkEventKeys.text: e.requestBody,
},
NetworkEventKeys.headersSize: _calculateHeadersSize(e.requestHeaders),
NetworkEventKeys.bodySize: _calculateBodySize(e.requestBody),
},
// Response
NetworkEventKeys.response: <String, Object?>{
NetworkEventKeys.status: e.status,
NetworkEventKeys.statusText: '',
NetworkEventKeys.httpVersion: NetworkEventDefaults.responseHttpVersion,
NetworkEventKeys.cookies: responseCookies,
NetworkEventKeys.headers: responseHeaders,
NetworkEventKeys.content: <String, Object?>{
NetworkEventKeys.size: e.responseBody?.length,
NetworkEventKeys.mimeType: e.type,
NetworkEventKeys.text: e.responseBody,
},
NetworkEventKeys.redirectURL: '',
NetworkEventKeys.headersSize: _calculateHeadersSize(e.responseHeaders),
NetworkEventKeys.bodySize: _calculateBodySize(e.responseBody),
},
// Cache
NetworkEventKeys.cache: <String, Object?>{},
NetworkEventKeys.timings: <String, Object?>{
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.connection: e.hashCode.toString(),
NetworkEventKeys.comment: '',
};
}).toList();

// Assemble the final HAR object
return <String, Object?>{
NetworkEventKeys.log: <String, Object?>{
NetworkEventKeys.version: NetworkEventDefaults.logVersion,
NetworkEventKeys.creator: creator,
NetworkEventKeys.entries: entries,
},
};
}

int _calculateHeadersSize(Map<String, dynamic>? 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<String>) {
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@
// 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 '../../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';
Expand Down Expand Up @@ -46,6 +50,7 @@ class NetworkController extends DisposableController
with
SearchControllerMixin<NetworkRequest>,
FilterControllerMixin<NetworkRequest>,
OfflineScreenControllerMixin,
AutoDisposeControllerMixin {
NetworkController() {
_networkService = NetworkService(this);
Expand All @@ -56,6 +61,33 @@ class NetworkController extends DisposableController
);
subscribeToFilterChanges();
}
List<DartIOHttpRequestData>? httpRequests;

String? exportAsHarFile() {
final exportController = ExportController();
httpRequests =
filteredData.value.whereType<DartIOHttpRequestData>().toList();

if (httpRequests!.isEmpty) {
debugPrint('No valid request data to export');
return '';
}

try {
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),
type: ExportFileType.har,
);
}
} catch (ex) {
debugPrint('Exception in export $ex');
}
return null;
}

static const methodFilterId = 'network-method-filter';

Expand Down Expand Up @@ -360,6 +392,25 @@ class NetworkController extends DisposableController
serviceConnection.errorBadgeManager.incrementBadgeCount(NetworkScreen.id);
}
}

@override
OfflineScreenData prepareOfflineScreenData() {
final requests =
filteredData.value.whereType<DartIOHttpRequestData>().toList();
return OfflineScreenData(
screenId: NetworkScreen.id,
data: convertRequestsToMap(requests),
);
}
}

Map<String, Object> convertRequestsToMap(
List<DartIOHttpRequestData>? requests,
) {
if (requests == null) return {};
return {
'requests': requests.map((request) => request.toMap()).toList(),
};
}

/// Class for managing the set of all current websocket requests, and
Expand Down
Loading