From ce0de370e4f6fabf54d2c47fafe38f80cdc2501c Mon Sep 17 00:00:00 2001 From: Derek Xu Date: Wed, 21 Feb 2024 10:30:40 -0500 Subject: [PATCH] Populate package:http_profile (#1046) --- .github/workflows/dart.yml | 126 +++--- pkgs/http_profile/lib/http_profile.dart | 380 ++++++++++++++++- pkgs/http_profile/pubspec.yaml | 3 +- .../test/populating_profiles_test.dart | 386 ++++++++++++++++++ .../test/profiling_enabled_test.dart | 18 +- 5 files changed, 837 insertions(+), 76 deletions(-) create mode 100644 pkgs/http_profile/test/populating_profiles_test.dart diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 22c1e5205c..400bdd10df 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -40,36 +40,6 @@ jobs: - name: mono_repo self validate run: dart pub global run mono_repo generate --validate job_002: - name: "analyze_and_format; linux; Dart 3.0.0; PKG: pkgs/http_profile; `dart analyze --fatal-infos`" - runs-on: ubuntu-latest - steps: - - name: Cache Pub hosted dependencies - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 - with: - path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_profile;commands:analyze_1" - restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_profile - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0 - os:ubuntu-latest;pub-cache-hosted - os:ubuntu-latest - - name: Setup Dart SDK - uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 - with: - sdk: "3.0.0" - - id: checkout - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - - id: pkgs_http_profile_pub_upgrade - name: pkgs/http_profile; dart pub upgrade - run: dart pub upgrade - if: "always() && steps.checkout.conclusion == 'success'" - working-directory: pkgs/http_profile - - name: "pkgs/http_profile; dart analyze --fatal-infos" - run: dart analyze --fatal-infos - if: "always() && steps.pkgs_http_profile_pub_upgrade.conclusion == 'success'" - working-directory: pkgs/http_profile - job_003: name: "analyze_and_format; linux; Dart 3.2.0; PKG: pkgs/http_client_conformance_tests; `dart analyze --fatal-infos`" runs-on: ubuntu-latest steps: @@ -99,7 +69,7 @@ jobs: run: dart analyze --fatal-infos if: "always() && steps.pkgs_http_client_conformance_tests_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http_client_conformance_tests - job_004: + job_003: name: "analyze_and_format; linux; Dart 3.2.6; PKG: pkgs/web_socket; `dart analyze --fatal-infos`" runs-on: ubuntu-latest steps: @@ -129,7 +99,7 @@ jobs: run: dart analyze --fatal-infos if: "always() && steps.pkgs_web_socket_pub_upgrade.conclusion == 'success'" working-directory: pkgs/web_socket - job_005: + job_004: name: "analyze_and_format; linux; Dart 3.3.0; PKG: pkgs/http; `dart analyze --fatal-infos`" runs-on: ubuntu-latest steps: @@ -159,6 +129,36 @@ jobs: run: dart analyze --fatal-infos if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http + job_005: + name: "analyze_and_format; linux; Dart 3.4.0-154.0.dev; PKG: pkgs/http_profile; `dart analyze --fatal-infos`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0-154.0.dev;packages:pkgs/http_profile;commands:analyze_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0-154.0.dev;packages:pkgs/http_profile + os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0-154.0.dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 + with: + sdk: "3.4.0-154.0.dev" + - id: checkout + name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - id: pkgs_http_profile_pub_upgrade + name: pkgs/http_profile; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/http_profile + - name: "pkgs/http_profile; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_http_profile_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/http_profile job_006: name: "analyze_and_format; linux; Dart dev; PKGS: pkgs/http, pkgs/http_client_conformance_tests, pkgs/http_profile, pkgs/web_socket; `dart analyze --fatal-infos`" runs-on: ubuntu-latest @@ -334,35 +334,35 @@ jobs: if: "always() && steps.pkgs_flutter_http_example_pub_upgrade.conclusion == 'success'" working-directory: pkgs/flutter_http_example job_010: - name: "unit_test; linux; Dart 3.0.0; PKG: pkgs/http_profile; `dart test --platform vm`" + name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart run --define=no_default_http_client=true test/no_default_http_client_test.dart`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_profile;commands:test_2" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http;commands:command_1" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0;packages:pkgs/http_profile - os:ubuntu-latest;pub-cache-hosted;sdk:3.0.0 + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0 os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 with: - sdk: "3.0.0" + sdk: "3.3.0" - id: checkout name: Checkout repository uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - - id: pkgs_http_profile_pub_upgrade - name: pkgs/http_profile; dart pub upgrade + - id: pkgs_http_pub_upgrade + name: pkgs/http; dart pub upgrade run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" - working-directory: pkgs/http_profile - - name: "pkgs/http_profile; dart test --platform vm" - run: dart test --platform vm - if: "always() && steps.pkgs_http_profile_pub_upgrade.conclusion == 'success'" - working-directory: pkgs/http_profile + working-directory: pkgs/http + - name: "pkgs/http; dart run --define=no_default_http_client=true test/no_default_http_client_test.dart" + run: "dart run --define=no_default_http_client=true test/no_default_http_client_test.dart" + if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/http needs: - job_001 - job_002 @@ -374,14 +374,14 @@ jobs: - job_008 - job_009 job_011: - name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart run --define=no_default_http_client=true test/no_default_http_client_test.dart`" + name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart test --platform chrome`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http;commands:command_1" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http;commands:test_3" restore-keys: | os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0 @@ -399,8 +399,8 @@ jobs: run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: pkgs/http - - name: "pkgs/http; dart run --define=no_default_http_client=true test/no_default_http_client_test.dart" - run: "dart run --define=no_default_http_client=true test/no_default_http_client_test.dart" + - name: "pkgs/http; dart test --platform chrome" + run: dart test --platform chrome if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http needs: @@ -414,14 +414,14 @@ jobs: - job_008 - job_009 job_012: - name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart test --platform chrome`" + name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart test --platform vm`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http;commands:test_3" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http;commands:test_2" restore-keys: | os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0 @@ -439,8 +439,8 @@ jobs: run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: pkgs/http - - name: "pkgs/http; dart test --platform chrome" - run: dart test --platform chrome + - name: "pkgs/http; dart test --platform vm" + run: dart test --platform vm if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" working-directory: pkgs/http needs: @@ -454,35 +454,35 @@ jobs: - job_008 - job_009 job_013: - name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/http; `dart test --platform vm`" + name: "unit_test; linux; Dart 3.4.0-154.0.dev; PKG: pkgs/http_profile; `dart test --platform vm`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http;commands:test_2" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0-154.0.dev;packages:pkgs/http_profile;commands:test_2" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/http - os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0 + os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0-154.0.dev;packages:pkgs/http_profile + os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0-154.0.dev os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 with: - sdk: "3.3.0" + sdk: "3.4.0-154.0.dev" - id: checkout name: Checkout repository uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - - id: pkgs_http_pub_upgrade - name: pkgs/http; dart pub upgrade + - id: pkgs_http_profile_pub_upgrade + name: pkgs/http_profile; dart pub upgrade run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" - working-directory: pkgs/http - - name: "pkgs/http; dart test --platform vm" + working-directory: pkgs/http_profile + - name: "pkgs/http_profile; dart test --platform vm" run: dart test --platform vm - if: "always() && steps.pkgs_http_pub_upgrade.conclusion == 'success'" - working-directory: pkgs/http + if: "always() && steps.pkgs_http_profile_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/http_profile needs: - job_001 - job_002 diff --git a/pkgs/http_profile/lib/http_profile.dart b/pkgs/http_profile/lib/http_profile.dart index ea27665fb1..b0b7a87677 100644 --- a/pkgs/http_profile/lib/http_profile.dart +++ b/pkgs/http_profile/lib/http_profile.dart @@ -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 _toJson() => { + '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 _toJson() => { + 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 _toJson() => { + 'statusCode': _statusCode, + 'method': _method, + 'location': _location, + }; +} + +/// Describes details about an HTTP request. +final class HttpProfileRequestData { + final Map _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 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 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> 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 _data; + + final void Function() _updated; + + /// Records a redirect that the connection went through. + void addRedirect(HttpProfileRedirectData redirect) { + (_data['redirects'] as List>).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 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 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> 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'] = >[]; + } +} /// 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 = {}; + + /// 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>).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> _requestBody = + StreamController>(); + + /// The body of the request. + StreamSink> get requestBodySink { + _updated(); + return _requestBody.sink; + } + + /// Details about the response. + late final HttpProfileResponseData responseData; + + final StreamController> _responseBody = + StreamController>(); + + /// The body of the response. + StreamSink> 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'] = >[]; + _data['requestData'] = {}; + requestData = HttpProfileRequestData._( + _data['requestData'] as Map, _updated); + _data['responseData'] = {}; + responseData = HttpProfileResponseData._( + _data['responseData'] as Map, _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; } } diff --git a/pkgs/http_profile/pubspec.yaml b/pkgs/http_profile/pubspec.yaml index 1b5891e2c1..93b55f296c 100644 --- a/pkgs/http_profile/pubspec.yaml +++ b/pkgs/http_profile/pubspec.yaml @@ -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 diff --git a/pkgs/http_profile/test/populating_profiles_test.dart b/pkgs/http_profile/test/populating_profiles_test.dart new file mode 100644 index 0000000000..a1cbbb4b30 --- /dev/null +++ b/pkgs/http_profile/test/populating_profiles_test.dart @@ -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 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>; + 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; + expect(requestData['connectionInfo'], isNull); + + profile.requestData.connectionInfo = { + 'localPort': 1285, + 'remotePort': 443, + 'connectionPoolId': '21x23' + }; + + final connectionInfo = + requestData['connectionInfo'] as Map; + expect(connectionInfo['localPort'], 1285); + expect(connectionInfo['remotePort'], 443); + expect(connectionInfo['connectionPoolId'], '21x23'); + }); + + test('populating HttpClientRequestProfile.requestData.contentLength', + () async { + final requestData = backingMap['requestData'] as Map; + expect(requestData['contentLength'], isNull); + + profile.requestData.contentLength = 1200; + + expect(requestData['contentLength'], 1200); + }); + + test('populating HttpClientRequestProfile.requestData.cookies', () async { + final requestData = backingMap['requestData'] as Map; + expect(requestData['cookies'], isNull); + + profile.requestData.cookies = [ + 'sessionId=abc123', + 'csrftoken=def456', + ]; + + final cookies = requestData['cookies'] as List; + 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; + expect(requestData['error'], isNull); + + profile.requestData.error = 'failed'; + + expect(requestData['error'], 'failed'); + }); + + test('populating HttpClientRequestProfile.requestData.followRedirects', + () async { + final requestData = backingMap['requestData'] as Map; + expect(requestData['followRedirects'], isNull); + + profile.requestData.followRedirects = true; + + expect(requestData['followRedirects'], true); + }); + + test('populating HttpClientRequestProfile.requestData.headers', () async { + final requestData = backingMap['requestData'] as Map; + expect(requestData['headers'], isNull); + + profile.requestData.headers = { + 'content-length': ['0'], + }; + + final headers = requestData['headers'] as Map>; + expect(headers['content-length']!.length, 1); + expect(headers['content-length']![0], '0'); + }); + + test('populating HttpClientRequestProfile.requestData.maxRedirects', + () async { + final requestData = backingMap['requestData'] as Map; + expect(requestData['maxRedirects'], isNull); + + profile.requestData.maxRedirects = 5; + + expect(requestData['maxRedirects'], 5); + }); + + test('populating HttpClientRequestProfile.requestData.persistentConnection', + () async { + final requestData = backingMap['requestData'] as Map; + expect(requestData['persistentConnection'], isNull); + + profile.requestData.persistentConnection = true; + + expect(requestData['persistentConnection'], true); + }); + + test('populating HttpClientRequestProfile.requestData.proxyDetails', + () async { + final requestData = backingMap['requestData'] as Map; + 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; + 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; + final redirects = responseData['redirects'] as List>; + 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; + expect(responseData['cookies'], isNull); + + profile.responseData.cookies = [ + 'sessionId=abc123', + 'id=def456; Max-Age=2592000; Domain=example.com', + ]; + + final cookies = responseData['cookies'] as List; + 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; + expect(responseData['connectionInfo'], isNull); + + profile.responseData.connectionInfo = { + 'localPort': 1285, + 'remotePort': 443, + 'connectionPoolId': '21x23' + }; + + final connectionInfo = + responseData['connectionInfo'] as Map; + expect(connectionInfo['localPort'], 1285); + expect(connectionInfo['remotePort'], 443); + expect(connectionInfo['connectionPoolId'], '21x23'); + }); + + test('populating HttpClientRequestProfile.responseData.headers', () async { + final responseData = backingMap['responseData'] as Map; + 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>; + 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; + 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; + expect(responseData['reasonPhrase'], isNull); + + profile.responseData.reasonPhrase = 'OK'; + + expect(responseData['reasonPhrase'], 'OK'); + }); + + test('populating HttpClientRequestProfile.responseData.isRedirect', () async { + final responseData = backingMap['responseData'] as Map; + expect(responseData['isRedirect'], isNull); + + profile.responseData.isRedirect = true; + + expect(responseData['isRedirect'], true); + }); + + test('populating HttpClientRequestProfile.responseData.persistentConnection', + () async { + final responseData = backingMap['responseData'] as Map; + expect(responseData['persistentConnection'], isNull); + + profile.responseData.persistentConnection = true; + + expect(responseData['persistentConnection'], true); + }); + + test('populating HttpClientRequestProfile.responseData.contentLength', + () async { + final responseData = backingMap['responseData'] as Map; + expect(responseData['contentLength'], isNull); + + profile.responseData.contentLength = 1200; + + expect(responseData['contentLength'], 1200); + }); + + test('populating HttpClientRequestProfile.responseData.statusCode', () async { + final responseData = backingMap['responseData'] as Map; + expect(responseData['statusCode'], isNull); + + profile.responseData.statusCode = 200; + + expect(responseData['statusCode'], 200); + }); + + test('populating HttpClientRequestProfile.responseData.startTime', () async { + final responseData = backingMap['responseData'] as Map; + 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; + 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; + expect(responseData['error'], isNull); + + profile.responseData.error = 'failed'; + + expect(responseData['error'], 'failed'); + }); + + test('using HttpClientRequestProfile.requestBodySink', () async { + final requestBodyStream = + backingMap['_requestBodyStream'] as Stream>; + + 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>; + + 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(), + ]); + }); +} diff --git a/pkgs/http_profile/test/profiling_enabled_test.dart b/pkgs/http_profile/test/profiling_enabled_test.dart index 6336da6ee4..3062c79719 100644 --- a/pkgs/http_profile/test/profiling_enabled_test.dart +++ b/pkgs/http_profile/test/profiling_enabled_test.dart @@ -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, + ); }); }