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
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;
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 @@ -30,6 +30,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 @@ -363,6 +364,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 @@ -371,6 +373,7 @@ class NetworkRequestsTable extends StatelessWidget {
statusColumn,
typeColumn,
durationColumn,
responseSizeColumn,
timestampColumn,
actionsColumn,
];
Expand Down Expand Up @@ -405,6 +408,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 @@ -227,6 +227,26 @@ class DartIOHttpRequestData extends NetworkRequest {
return connectionInfo != null ? connectionInfo[_localPortKey] : null;
}

@override
int? get responseBytes {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test for this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, added tests in /test/shared/http/http_request_data_test.dart

final contentLength = responseHeaders?['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 the lack of
/// an end time in the response data.
@override
Expand Down
3 changes: 3 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,6 +39,9 @@ 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)

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

## Logging updates
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:devtools_app/src/shared/http/http_request_data.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('responseBytes', () {
Map<String, dynamic> baseJson(Map<String, Object?> headers) {
return {
'method': 'GET',
'uri': 'https://example.com',
'status': 200,
'responseHeaders': headers,
};
}

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

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, // requestPostData not used for this test
null, // responseContent not used for this test
);

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, // requestPostData not used for this test
null, // responseContent not used for this test
);

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

// Returns null when header is missing.
test('returns null for missing header', () {
final request = DartIOHttpRequestData.fromJson(
baseJson({}), // No content-length header
null, // requestPostData not used for this test
null, // responseContent not used for this test
);

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

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

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), '-');
});
});
}