Skip to content

Commit

Permalink
Populate package:http_profile (#1046)
Browse files Browse the repository at this point in the history
derekxu16 authored Feb 21, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 75e01f4 commit ce0de37
Showing 5 changed files with 837 additions and 76 deletions.
126 changes: 63 additions & 63 deletions .github/workflows/dart.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

380 changes: 370 additions & 10 deletions pkgs/http_profile/lib/http_profile.dart
Original file line number Diff line number Diff line change
@@ -2,7 +2,274 @@
// for details. 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:io';
import 'dart:async' show StreamController, StreamSink;
import 'dart:developer' show Service, addHttpClientProfilingData;
import 'dart:io' show HttpClient, HttpClientResponseCompressionState;
import 'dart:isolate' show Isolate;

/// Describes an event related to an HTTP request.
final class HttpProfileRequestEvent {
final int _timestamp;
final String _name;

HttpProfileRequestEvent({required DateTime timestamp, required String name})
: _timestamp = timestamp.microsecondsSinceEpoch,
_name = name;

Map<String, dynamic> _toJson() => <String, dynamic>{
'timestamp': _timestamp,
'event': _name,
};
}

/// Describes proxy authentication details associated with an HTTP request.
final class HttpProfileProxyData {
final String? _host;
final String? _username;
final bool? _isDirect;
final int? _port;

HttpProfileProxyData({
String? host,
String? username,
bool? isDirect,
int? port,
}) : _host = host,
_username = username,
_isDirect = isDirect,
_port = port;

Map<String, dynamic> _toJson() => <String, dynamic>{
if (_host != null) 'host': _host,
if (_username != null) 'username': _username,
if (_isDirect != null) 'isDirect': _isDirect,
if (_port != null) 'port': _port,
};
}

/// Describes a redirect that an HTTP connection went through.
class HttpProfileRedirectData {
final int _statusCode;
final String _method;
final String _location;

HttpProfileRedirectData({
required int statusCode,
required String method,
required String location,
}) : _statusCode = statusCode,
_method = method,
_location = location;

Map<String, dynamic> _toJson() => <String, dynamic>{
'statusCode': _statusCode,
'method': _method,
'location': _location,
};
}

/// Describes details about an HTTP request.
final class HttpProfileRequestData {
final Map<String, dynamic> _data;

final void Function() _updated;

/// Information about the networking connection used in the HTTP request.
///
/// This information is meant to be used for debugging.
///
/// It can contain any arbitrary data as long as the values are of type
/// [String] or [int]. For example:
/// { 'localPort': 1285, 'remotePort': 443, 'connectionPoolId': '21x23' }
set connectionInfo(Map<String, dynamic /*String|int*/ > value) {
for (final v in value.values) {
if (!(v is String || v is int)) {
throw ArgumentError(
'The values in connectionInfo must be of type String or int.',
);
}
}
_data['connectionInfo'] = {...value};
_updated();
}

/// The content length of the request, in bytes.
set contentLength(int value) {
_data['contentLength'] = value;
_updated();
}

/// The cookies presented to the server (in the 'cookie' header).
///
/// Usage example:
///
/// ```dart
/// profile.requestData.cookies = [
/// 'sessionId=abc123',
/// 'csrftoken=def456',
/// ];
/// ```
set cookies(List<String> value) {
_data['cookies'] = [...value];
_updated();
}

/// The error associated with a failed request.
set error(String value) {
_data['error'] = value;
_updated();
}

/// Whether automatic redirect following was enabled for the request.
set followRedirects(bool value) {
_data['followRedirects'] = value;
_updated();
}

set headers(Map<String, List<String>> value) {
_data['headers'] = {...value};
_updated();
}

/// The maximum number of redirects allowed during the request.
set maxRedirects(int value) {
_data['maxRedirects'] = value;
_updated();
}

/// The requested persistent connection state.
set persistentConnection(bool value) {
_data['persistentConnection'] = value;
_updated();
}

/// Proxy authentication details for the request.
set proxyDetails(HttpProfileProxyData value) {
_data['proxyDetails'] = value._toJson();
_updated();
}

const HttpProfileRequestData._(
this._data,
this._updated,
);
}

/// Describes details about a response to an HTTP request.
final class HttpProfileResponseData {
final Map<String, dynamic> _data;

final void Function() _updated;

/// Records a redirect that the connection went through.
void addRedirect(HttpProfileRedirectData redirect) {
(_data['redirects'] as List<Map<String, dynamic>>).add(redirect._toJson());
_updated();
}

/// The cookies set by the server (from the 'set-cookie' header).
///
/// Usage example:
///
/// ```dart
/// profile.responseData.cookies = [
/// 'sessionId=abc123',
/// 'id=def456; Max-Age=2592000; Domain=example.com',
/// ];
/// ```
set cookies(List<String> value) {
_data['cookies'] = [...value];
_updated();
}

/// Information about the networking connection used in the HTTP response.
///
/// This information is meant to be used for debugging.
///
/// It can contain any arbitrary data as long as the values are of type
/// [String] or [int]. For example:
/// { 'localPort': 1285, 'remotePort': 443, 'connectionPoolId': '21x23' }
set connectionInfo(Map<String, dynamic /*String|int*/ > value) {
for (final v in value.values) {
if (!(v is String || v is int)) {
throw ArgumentError(
'The values in connectionInfo must be of type String or int.',
);
}
}
_data['connectionInfo'] = {...value};
_updated();
}

set headers(Map<String, List<String>> value) {
_data['headers'] = {...value};
_updated();
}

// The compression state of the response.
//
// This specifies whether the response bytes were compressed when they were
// received across the wire and whether callers will receive compressed or
// uncompressed bytes when they listen to the response body byte stream.
set compressionState(HttpClientResponseCompressionState value) {
_data['compressionState'] = value.name;
_updated();
}

set reasonPhrase(String value) {
_data['reasonPhrase'] = value;
_updated();
}

/// Whether the status code was one of the normal redirect codes.
set isRedirect(bool value) {
_data['isRedirect'] = value;
_updated();
}

/// The persistent connection state returned by the server.
set persistentConnection(bool value) {
_data['persistentConnection'] = value;
_updated();
}

/// The content length of the response body, in bytes.
set contentLength(int value) {
_data['contentLength'] = value;
_updated();
}

set statusCode(int value) {
_data['statusCode'] = value;
_updated();
}

/// The time at which the initial response was received.
set startTime(DateTime value) {
_data['startTime'] = value.microsecondsSinceEpoch;
_updated();
}

/// The time at which the response was completed. Note that DevTools will not
/// consider the request to be complete until [endTime] is non-null.
set endTime(DateTime value) {
_data['endTime'] = value.microsecondsSinceEpoch;
_updated();
}

/// The error associated with a failed request.
set error(String value) {
_data['error'] = value;
_updated();
}

HttpProfileResponseData._(
this._data,
this._updated,
) {
_data['redirects'] = <Map<String, dynamic>>[];
}
}

/// A record of debugging information about an HTTP request.
final class HttpClientRequestProfile {
@@ -14,20 +281,113 @@ final class HttpClientRequestProfile {
static set profilingEnabled(bool enabled) =>
HttpClient.enableTimelineLogging = enabled;

String? requestMethod;
String? requestUri;
final _data = <String, dynamic>{};

/// Records an event related to the request.
///
/// Usage example:
///
/// ```dart
/// profile.addEvent(
/// HttpProfileRequestEvent(
/// timestamp: DateTime.now(),
/// name: "Connection Established",
/// ),
/// );
/// profile.addEvent(
/// HttpProfileRequestEvent(
/// timestamp: DateTime.now(),
/// name: "Remote Disconnected",
/// ),
/// );
/// ```
void addEvent(HttpProfileRequestEvent event) {
(_data['events'] as List<Map<String, dynamic>>).add(event._toJson());
_updated();
}

/// The time at which the request was completed. Note that DevTools will not
/// consider the request to be complete until [requestEndTimestamp] is
/// non-null.
set requestEndTimestamp(DateTime value) {
_data['requestEndTimestamp'] = value.microsecondsSinceEpoch;
_updated();
}

/// Details about the request.
late final HttpProfileRequestData requestData;

final StreamController<List<int>> _requestBody =
StreamController<List<int>>();

/// The body of the request.
StreamSink<List<int>> get requestBodySink {
_updated();
return _requestBody.sink;
}

/// Details about the response.
late final HttpProfileResponseData responseData;

final StreamController<List<int>> _responseBody =
StreamController<List<int>>();

/// The body of the response.
StreamSink<List<int>> get responseBodySink {
_updated();
return _responseBody.sink;
}

void _updated() =>
_data['_lastUpdateTime'] = DateTime.now().microsecondsSinceEpoch;

HttpClientRequestProfile._({
required DateTime requestStartTimestamp,
required String requestMethod,
required String requestUri,
}) {
_data['isolateId'] = Service.getIsolateId(Isolate.current)!;
_data['requestStartTimestamp'] =
requestStartTimestamp.microsecondsSinceEpoch;
_data['requestMethod'] = requestMethod;
_data['requestUri'] = requestUri;
_data['events'] = <Map<String, dynamic>>[];
_data['requestData'] = <String, dynamic>{};
requestData = HttpProfileRequestData._(
_data['requestData'] as Map<String, dynamic>, _updated);
_data['responseData'] = <String, dynamic>{};
responseData = HttpProfileResponseData._(
_data['responseData'] as Map<String, dynamic>, _updated);
_data['_requestBodyStream'] = _requestBody.stream;
_data['_responseBodyStream'] = _responseBody.stream;
// This entry is needed to support the updatedSince parameter of
// ext.dart.io.getHttpProfile.
_data['_lastUpdateTime'] = DateTime.now().microsecondsSinceEpoch;
}

/// If HTTP profiling is enabled, returns an [HttpClientRequestProfile],
/// otherwise returns `null`.
static HttpClientRequestProfile? profile({
/// The time at which the request was initiated.
required DateTime requestStartTimestamp,

HttpClientRequestProfile._();
/// The HTTP request method associated with the request.
required String requestMethod,

/// If HTTP profiling is enabled, returns
/// a [HttpClientRequestProfile] otherwise returns `null`.
static HttpClientRequestProfile? profile() {
// Always return `null` in product mode so that the
// profiling code can be tree shaken away.
/// The URI to which the request was sent.
required String requestUri,
}) {
// Always return `null` in product mode so that the profiling code can be
// tree shaken away.
if (const bool.fromEnvironment('dart.vm.product') || !profilingEnabled) {
return null;
}
final requestProfile = HttpClientRequestProfile._();
final requestProfile = HttpClientRequestProfile._(
requestStartTimestamp: requestStartTimestamp,
requestMethod: requestMethod,
requestUri: requestUri,
);
addHttpClientProfilingData(requestProfile._data);
return requestProfile;
}
}
3 changes: 2 additions & 1 deletion pkgs/http_profile/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -7,7 +7,8 @@ repository: https://github.com/dart-lang/http/tree/master/pkgs/http_profile
publish_to: none

environment:
sdk: ^3.0.0
# TODO(derekxu16): Change the following constraint to ^3.4.0 before publishing this package.
sdk: ^3.4.0-154.0.dev

dependencies:
test: ^1.24.9
386 changes: 386 additions & 0 deletions pkgs/http_profile/test/populating_profiles_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,386 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. 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:async';
import 'dart:developer' show Service, getHttpClientProfilingData;
import 'dart:io';
import 'dart:isolate' show Isolate;

import 'package:http_profile/http_profile.dart';
import 'package:test/test.dart';

void main() {
late HttpClientRequestProfile profile;
late Map<String, dynamic> backingMap;

setUp(() {
HttpClientRequestProfile.profilingEnabled = true;

profile = HttpClientRequestProfile.profile(
requestStartTimestamp: DateTime.parse('2024-03-21'),
requestMethod: 'GET',
requestUri: 'https://www.example.com',
)!;

final profileBackingMaps = getHttpClientProfilingData();
expect(profileBackingMaps.length, isPositive);
backingMap = profileBackingMaps.lastOrNull!;
});

test(
'mandatory fields are populated when an HttpClientRequestProfile is '
'constructed', () async {
expect(backingMap['id'], isNotNull);
expect(backingMap['isolateId'], Service.getIsolateId(Isolate.current)!);
expect(
backingMap['requestStartTimestamp'],
DateTime.parse('2024-03-21').microsecondsSinceEpoch,
);
expect(backingMap['requestMethod'], 'GET');
expect(backingMap['requestUri'], 'https://www.example.com');
});

test('calling HttpClientRequestProfile.addEvent', () async {
final events = backingMap['events'] as List<Map<String, dynamic>>;
expect(events, isEmpty);

profile.addEvent(HttpProfileRequestEvent(
timestamp: DateTime.parse('2024-03-22'),
name: 'an event',
));

expect(events.length, 1);
final event = events.last;
expect(
event['timestamp'],
DateTime.parse('2024-03-22').microsecondsSinceEpoch,
);
expect(event['event'], 'an event');
});

test('populating HttpClientRequestProfile.requestEndTimestamp', () async {
expect(backingMap['requestEndTimestamp'], isNull);

profile.requestEndTimestamp = DateTime.parse('2024-03-23');

expect(
backingMap['requestEndTimestamp'],
DateTime.parse('2024-03-23').microsecondsSinceEpoch,
);
});

test('populating HttpClientRequestProfile.requestData.connectionInfo',
() async {
final requestData = backingMap['requestData'] as Map<String, dynamic>;
expect(requestData['connectionInfo'], isNull);

profile.requestData.connectionInfo = {
'localPort': 1285,
'remotePort': 443,
'connectionPoolId': '21x23'
};

final connectionInfo =
requestData['connectionInfo'] as Map<String, dynamic>;
expect(connectionInfo['localPort'], 1285);
expect(connectionInfo['remotePort'], 443);
expect(connectionInfo['connectionPoolId'], '21x23');
});

test('populating HttpClientRequestProfile.requestData.contentLength',
() async {
final requestData = backingMap['requestData'] as Map<String, dynamic>;
expect(requestData['contentLength'], isNull);

profile.requestData.contentLength = 1200;

expect(requestData['contentLength'], 1200);
});

test('populating HttpClientRequestProfile.requestData.cookies', () async {
final requestData = backingMap['requestData'] as Map<String, dynamic>;
expect(requestData['cookies'], isNull);

profile.requestData.cookies = <String>[
'sessionId=abc123',
'csrftoken=def456',
];

final cookies = requestData['cookies'] as List<String>;
expect(cookies.length, 2);
expect(cookies[0], 'sessionId=abc123');
expect(cookies[1], 'csrftoken=def456');
});

test('populating HttpClientRequestProfile.requestData.error', () async {
final requestData = backingMap['requestData'] as Map<String, dynamic>;
expect(requestData['error'], isNull);

profile.requestData.error = 'failed';

expect(requestData['error'], 'failed');
});

test('populating HttpClientRequestProfile.requestData.followRedirects',
() async {
final requestData = backingMap['requestData'] as Map<String, dynamic>;
expect(requestData['followRedirects'], isNull);

profile.requestData.followRedirects = true;

expect(requestData['followRedirects'], true);
});

test('populating HttpClientRequestProfile.requestData.headers', () async {
final requestData = backingMap['requestData'] as Map<String, dynamic>;
expect(requestData['headers'], isNull);

profile.requestData.headers = {
'content-length': ['0'],
};

final headers = requestData['headers'] as Map<String, List<String>>;
expect(headers['content-length']!.length, 1);
expect(headers['content-length']![0], '0');
});

test('populating HttpClientRequestProfile.requestData.maxRedirects',
() async {
final requestData = backingMap['requestData'] as Map<String, dynamic>;
expect(requestData['maxRedirects'], isNull);

profile.requestData.maxRedirects = 5;

expect(requestData['maxRedirects'], 5);
});

test('populating HttpClientRequestProfile.requestData.persistentConnection',
() async {
final requestData = backingMap['requestData'] as Map<String, dynamic>;
expect(requestData['persistentConnection'], isNull);

profile.requestData.persistentConnection = true;

expect(requestData['persistentConnection'], true);
});

test('populating HttpClientRequestProfile.requestData.proxyDetails',
() async {
final requestData = backingMap['requestData'] as Map<String, dynamic>;
expect(requestData['proxyDetails'], isNull);

profile.requestData.proxyDetails = HttpProfileProxyData(
host: 'https://www.example.com',
username: 'abc123',
isDirect: true,
port: 4321,
);

final proxyDetails = requestData['proxyDetails'] as Map<String, dynamic>;
expect(
proxyDetails['host'],
'https://www.example.com',
);
expect(proxyDetails['username'], 'abc123');
expect(proxyDetails['isDirect'], true);
expect(proxyDetails['port'], 4321);
});

test('calling HttpClientRequestProfile.responseData.addRedirect', () async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
final redirects = responseData['redirects'] as List<Map<String, dynamic>>;
expect(redirects, isEmpty);

profile.responseData.addRedirect(HttpProfileRedirectData(
statusCode: 301,
method: 'GET',
location: 'https://images.example.com/1',
));

expect(redirects.length, 1);
final redirect = redirects.last;
expect(redirect['statusCode'], 301);
expect(redirect['method'], 'GET');
expect(redirect['location'], 'https://images.example.com/1');
});

test('populating HttpClientRequestProfile.responseData.cookies', () async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['cookies'], isNull);

profile.responseData.cookies = <String>[
'sessionId=abc123',
'id=def456; Max-Age=2592000; Domain=example.com',
];

final cookies = responseData['cookies'] as List<String>;
expect(cookies.length, 2);
expect(cookies[0], 'sessionId=abc123');
expect(cookies[1], 'id=def456; Max-Age=2592000; Domain=example.com');
});

test('populating HttpClientRequestProfile.responseData.connectionInfo',
() async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['connectionInfo'], isNull);

profile.responseData.connectionInfo = {
'localPort': 1285,
'remotePort': 443,
'connectionPoolId': '21x23'
};

final connectionInfo =
responseData['connectionInfo'] as Map<String, dynamic>;
expect(connectionInfo['localPort'], 1285);
expect(connectionInfo['remotePort'], 443);
expect(connectionInfo['connectionPoolId'], '21x23');
});

test('populating HttpClientRequestProfile.responseData.headers', () async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['headers'], isNull);

profile.responseData.headers = {
'connection': ['keep-alive'],
'cache-control': ['max-age=43200'],
'content-type': ['application/json', 'charset=utf-8'],
};

final headers = responseData['headers'] as Map<String, List<String>>;
expect(headers['connection']!.length, 1);
expect(headers['connection']![0], 'keep-alive');
expect(headers['cache-control']!.length, 1);
expect(headers['cache-control']![0], 'max-age=43200');
expect(headers['content-type']!.length, 2);
expect(headers['content-type']![0], 'application/json');
expect(headers['content-type']![1], 'charset=utf-8');
});

test('populating HttpClientRequestProfile.responseData.compressionState',
() async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['compressionState'], isNull);

profile.responseData.compressionState =
HttpClientResponseCompressionState.decompressed;

expect(responseData['compressionState'], 'decompressed');
});

test('populating HttpClientRequestProfile.responseData.reasonPhrase',
() async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['reasonPhrase'], isNull);

profile.responseData.reasonPhrase = 'OK';

expect(responseData['reasonPhrase'], 'OK');
});

test('populating HttpClientRequestProfile.responseData.isRedirect', () async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['isRedirect'], isNull);

profile.responseData.isRedirect = true;

expect(responseData['isRedirect'], true);
});

test('populating HttpClientRequestProfile.responseData.persistentConnection',
() async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['persistentConnection'], isNull);

profile.responseData.persistentConnection = true;

expect(responseData['persistentConnection'], true);
});

test('populating HttpClientRequestProfile.responseData.contentLength',
() async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['contentLength'], isNull);

profile.responseData.contentLength = 1200;

expect(responseData['contentLength'], 1200);
});

test('populating HttpClientRequestProfile.responseData.statusCode', () async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['statusCode'], isNull);

profile.responseData.statusCode = 200;

expect(responseData['statusCode'], 200);
});

test('populating HttpClientRequestProfile.responseData.startTime', () async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['startTime'], isNull);

profile.responseData.startTime = DateTime.parse('2024-03-21');

expect(
responseData['startTime'],
DateTime.parse('2024-03-21').microsecondsSinceEpoch,
);
});

test('populating HttpClientRequestProfile.responseData.endTime', () async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['endTime'], isNull);

profile.responseData.endTime = DateTime.parse('2024-03-23');

expect(
responseData['endTime'],
DateTime.parse('2024-03-23').microsecondsSinceEpoch,
);
});

test('populating HttpClientRequestProfile.responseData.error', () async {
final responseData = backingMap['responseData'] as Map<String, dynamic>;
expect(responseData['error'], isNull);

profile.responseData.error = 'failed';

expect(responseData['error'], 'failed');
});

test('using HttpClientRequestProfile.requestBodySink', () async {
final requestBodyStream =
backingMap['_requestBodyStream'] as Stream<List<int>>;

profile.requestBodySink.add([1, 2, 3]);

await Future.wait([
Future.sync(
() async => expect(
await requestBodyStream.expand((i) => i).toList(),
[1, 2, 3],
),
),
profile.requestBodySink.close(),
]);
});

test('using HttpClientRequestProfile.responseBodySink', () async {
final requestBodyStream =
backingMap['_responseBodyStream'] as Stream<List<int>>;

profile.responseBodySink.add([1, 2, 3]);

await Future.wait([
Future.sync(
() async => expect(
await requestBodyStream.expand((i) => i).toList(),
[1, 2, 3],
),
),
profile.responseBodySink.close(),
]);
});
}
18 changes: 16 additions & 2 deletions pkgs/http_profile/test/profiling_enabled_test.dart
Original file line number Diff line number Diff line change
@@ -11,12 +11,26 @@ void main() {
test('profiling enabled', () async {
HttpClientRequestProfile.profilingEnabled = true;
expect(HttpClient.enableTimelineLogging, true);
expect(HttpClientRequestProfile.profile(), isNotNull);
expect(
HttpClientRequestProfile.profile(
requestStartTimestamp: DateTime.parse('2024-03-21'),
requestMethod: 'GET',
requestUri: 'https://www.example.com',
),
isNotNull,
);
});

test('profiling disabled', () async {
HttpClientRequestProfile.profilingEnabled = false;
expect(HttpClient.enableTimelineLogging, false);
expect(HttpClientRequestProfile.profile(), isNull);
expect(
HttpClientRequestProfile.profile(
requestStartTimestamp: DateTime.parse('2024-03-21'),
requestMethod: 'GET',
requestUri: 'https://www.example.com',
),
isNull,
);
});
}

0 comments on commit ce0de37

Please sign in to comment.