Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
03abf66
Added size support to network tab
Victowolf Mar 27, 2026
28a31c7
Added release notes entry for network response size feature
Victowolf Mar 27, 2026
f32b2e0
Resolve merge conflict in release notes
Victowolf Mar 27, 2026
c34ee25
Added documentation for byte formatting
Victowolf Mar 27, 2026
4372092
Fix lint: match parameter names with base class
Victowolf Mar 27, 2026
7f50b84
Fix lint: correct parameter name in AddressColumn
Victowolf Mar 27, 2026
c1547a2
Fix type handling for content-length header in responseBytes
Victowolf Mar 27, 2026
d6cf3e6
Fix layout spacing to ensure consistent widget tree
Victowolf Mar 27, 2026
eef762c
Fix variable placement for response size rendering
Victowolf Mar 27, 2026
b084c62
Fix parameter name to match ColumnData override
Victowolf Mar 29, 2026
46ddb13
Addressed Requested changes: update size units, refactor column defin…
Victowolf Mar 31, 2026
cb115cd
Addressed Rquested Changes: refactor formatBytes into http_utils, upd…
Victowolf Mar 31, 2026
fef7e5f
fix : formatBytes test fail
Victowolf Mar 31, 2026
3643367
fix: failed tests
Victowolf Apr 8, 2026
ba633b0
fix: baseJson missing parameter
Victowolf Apr 8, 2026
504a9dc
NEXT_RELEASE_NOTES.md sync
Victowolf Apr 14, 2026
7238ff0
Clean release notes before rebase
Victowolf Apr 14, 2026
47c4050
Revert unintended changes to pubspec.lock
Victowolf Apr 14, 2026
d6ce75d
Merge remote-tracking branch 'upstream/master' into feature/network-r…
Victowolf Apr 16, 2026
8d836b9
Fix: make responseBytes non-intrusive to request lifecycle
Victowolf Apr 20, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ abstract class NetworkRequest

int? get port;

int? get requestBytes => null;
Comment thread
srawlins marked this conversation as resolved.
int? get responseBytes => null;

bool get didFail;

/// True if the request hasn't completed yet.
Expand Down Expand Up @@ -160,6 +163,12 @@ class Socket extends NetworkRequest {
@override
int get port => _socket.port;

@override
int get requestBytes => writeBytes;

@override
int get responseBytes => readBytes;

// TODO(kenz): what determines a web socket request failure?
@override
bool get didFail => false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import '../../shared/ui/colors.dart';
import '../../shared/ui/common_widgets.dart';
import 'network_controller.dart';
import 'network_model.dart';
import 'utils/http_utils.dart';

// Approximately double the indent of the expandable tile's title.
const _rowIndentPadding = 30.0;
Expand Down Expand Up @@ -625,6 +626,7 @@ class NetworkRequestOverviewView extends StatelessWidget {
}

List<Widget> _buildGeneralRows(BuildContext context) {
final bytes = data.responseBytes;
return [
// TODO(kenz): show preview for requests (png, response body, proto)
_buildRow(
Expand Down Expand Up @@ -658,6 +660,14 @@ class NetworkRequestOverviewView extends StatelessWidget {
),
const SizedBox(height: defaultSpacing),
],

_buildRow(
context: context,
title: 'Response Size',
child: _valueText(bytes != null ? formatBytes(bytes) : '-'),
),
const SizedBox(height: defaultSpacing),

if (data.contentType != null) ...[
_buildRow(
context: context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import '../../shared/ui/utils.dart';
import 'network_controller.dart';
import 'network_model.dart';
import 'network_request_inspector.dart';
import 'utils/http_utils.dart';

class NetworkScreen extends Screen {
NetworkScreen() : super.fromMetaData(ScreenMetaData.network);
Expand Down Expand Up @@ -352,6 +353,7 @@ class NetworkRequestsTable extends StatelessWidget {
static const statusColumn = StatusColumn();
static const typeColumn = TypeColumn();
static const durationColumn = DurationColumn();
static const responseSizeColumn = ResponseSizeColumn();
static final timestampColumn = TimestampColumn();
static const actionsColumn = ActionsColumn();
static final columns = <ColumnData<NetworkRequest>>[
Expand All @@ -360,6 +362,7 @@ class NetworkRequestsTable extends StatelessWidget {
statusColumn,
typeColumn,
durationColumn,
responseSizeColumn,
timestampColumn,
actionsColumn,
];
Expand Down Expand Up @@ -394,6 +397,20 @@ class NetworkRequestsTable extends StatelessWidget {
}
}

class ResponseSizeColumn extends ColumnData<NetworkRequest> {
const ResponseSizeColumn()
: super('Size', alignment: ColumnAlignment.right, fixedWidthPx: 90);

@override
int? getValue(NetworkRequest dataObject) => dataObject.responseBytes;

@override
String getDisplayValue(NetworkRequest dataObject) {
final bytes = dataObject.responseBytes;
return bytes != null ? formatBytes(bytes) : '-';
}
}

class AddressColumn extends ColumnData<NetworkRequest>
implements ColumnRenderer<NetworkRequest> {
AddressColumn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,18 @@ int calculateHeadersSize(Map<String, Object?>? headers) {
// Calculate the byte length of the headers string
return utf8.encode(headersString).length;
}

// Output Formats:
// - 512 → "512 B"
// - 2000 → "2.0 kB"
// - 1000000 → "1.0 MB"
// Values are rounded to one decimal place for kB and MB.
// Uses decimal (base-10) units to match Chrome DevTools.
String formatBytes(int? bytes) {
if (bytes == null || bytes < 0) return '-';
if (bytes < 1000) return '$bytes B';
if (bytes < 1000 * 1000) {
return '${(bytes / 1000).toStringAsFixed(1)} kB';
}
return '${(bytes / (1000 * 1000)).toStringAsFixed(1)} MB';
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,28 @@ class DartIOHttpRequestData extends NetworkRequest {
return connectionInfo != null ? connectionInfo[_localPortKey] : null;
}

@override
int? get responseBytes {
Comment thread
Victowolf marked this conversation as resolved.
final headers = _request.response?.headers;
if (headers == null) return null;

final contentLength = headers['content-length'];

if (contentLength is String) {
return int.tryParse(contentLength);
}
if (contentLength is List && contentLength.isNotEmpty) {
final first = contentLength.first;

if (first is int) return first;
if (first is String) return int.tryParse(first);
}
return null;
}

/// True if the HTTP request hasn't completed yet, determined by
/// `isRequestComplete` / `isResponseComplete` from the profile data.

@override
bool get inProgress {
if (_isCancelled) return false;
Expand Down
6 changes: 6 additions & 0 deletions packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ TODO: Remove this section if there are not any updates.

## Network profiler updates

- Added response size column to the Network tab and displayed response size in the request inspector overview.
[#9744](https://github.com/flutter/devtools/pull/9744)

- Improved HTTP request status classification in the Network tab to better distinguish cancelled, completed, and in-flight requests (for example, avoiding some cases where cancelled requests appeared as pending). [#9683](https://github.com/flutter/devtools/pull/9683)

- Added a filter setting to hide HTTP-profiler socket data.
[#9698](https://github.com/flutter/devtools/pull/9698)

## Logging updates

TODO: Remove this section if there are not any updates.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'package:devtools_app/src/shared/http/http_request_data.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('responseBytes', () {
Map<String, Object?> baseJson(Map<String, Object?> responseHeaders) {
return {
'isolateId': 'isolate-1',
'id': 'request-1',
'method': 'GET',
'uri': 'https://example.com',
'events': <Object?>[],
'startTime': DateTime.now().microsecondsSinceEpoch,
'endTime': DateTime.now().microsecondsSinceEpoch,
'request': {
'headers': <String, Object?>{},
'connectionInfo': null,
'contentLength': null,
'cookies': <Object?>[],
'followRedirects': true,
'maxRedirects': 5,
'persistentConnection': true,
},
'response': {
'headers': responseHeaders,
'connectionInfo': null,
'contentLength': null,
'cookies': <Object?>[],
'compressionState': 'ResponseBodyCompressionState.notCompressed',
'isRedirect': false,
'persistentConnection': true,
'reasonPhrase': 'OK',
'redirects': <Map<String, dynamic>>[],
'statusCode': 200,
'startTime': DateTime.now().microsecondsSinceEpoch,
},
};
}

// Verifies parsing when content-length is a string value.
test('parses content-length from string', () {
final request = DartIOHttpRequestData.fromJson(
baseJson({'content-length': '1234'}),
null,
null,
);

expect(request.responseBytes, 1234);
});

// Verifies parsing when content-length is a list of strings.
test('parses content-length from list of strings', () {
final request = DartIOHttpRequestData.fromJson(
baseJson({
'content-length': ['5678'],
}),
null,
null,
);

expect(request.responseBytes, 5678);
});

// Ensures integer values inside a list are handled correctly.
test('handles integer in list', () {
final request = DartIOHttpRequestData.fromJson(
baseJson({
'content-length': [91011],
}),
null,
null,
);

expect(request.responseBytes, 91011);
});

// Returns null when header is missing.
test('returns null for missing header', () {
final request = DartIOHttpRequestData.fromJson(baseJson({}), null, null);

expect(request.responseBytes, null);
});

// Returns null when parsing fails.
test('returns null for invalid value', () {
final request = DartIOHttpRequestData.fromJson(
baseJson({'content-length': 'invalid'}),
null,
null,
);

expect(request.responseBytes, null);
});
});
}
19 changes: 19 additions & 0 deletions packages/devtools_app/test/shared/http/http_utils_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:devtools_app/src/screens/network/utils/http_utils.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('formatBytes', () {
// Verifies correct formatting across different unit ranges.
test('formats bytes correctly', () {
expect(formatBytes(512), '512 B'); // bytes
expect(formatBytes(2000), '2.0 kB'); // kilobytes (base-10)
expect(formatBytes(1000000), '1.0 MB'); // megabytes (base-10)
});

// Ensures handling of invalid or missing values.
test('handles null and negative values', () {
expect(formatBytes(null), '-');
expect(formatBytes(-1), '-');
});
});
}
Loading