Skip to content

Commit 69fa777

Browse files
authored
Adds WebSocket support to Network panel (GSoC'26) (#9968)
1 parent dcb810a commit 69fa777

15 files changed

Lines changed: 1406 additions & 22 deletions

‎packages/devtools_app/lib/src/screens/network/network_controller.dart‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ class NetworkController extends DevToolsScreenController
246246
..updateOrAddAll(
247247
requests: httpProfileData,
248248
sockets: socketStatsData,
249+
webSockets: const [],
249250
timelineMicrosOffset: offlineData.timelineMicrosOffset ?? 0,
250251
);
251252
_filterAndRefreshSearchMatches();
@@ -265,12 +266,14 @@ class NetworkController extends DevToolsScreenController
265266
void processNetworkTrafficHelper(
266267
List<SocketStatistic> sockets,
267268
List<HttpProfileRequest>? httpRequests,
269+
List<WebSocketConnection> webSockets,
268270
int timelineMicrosOffset, {
269271
required CurrentNetworkRequests currentRequests,
270272
}) {
271273
currentRequests.updateOrAddAll(
272274
requests: httpRequests!,
273275
sockets: sockets,
276+
webSockets: webSockets,
274277
timelineMicrosOffset: timelineMicrosOffset,
275278
);
276279

@@ -287,11 +290,13 @@ class NetworkController extends DevToolsScreenController
287290
void processNetworkTraffic({
288291
required List<SocketStatistic> sockets,
289292
required List<HttpProfileRequest>? httpRequests,
293+
required List<WebSocketConnection> webSockets,
290294
}) {
291295
// Trigger refresh.
292296
processNetworkTrafficHelper(
293297
sockets,
294298
httpRequests,
299+
webSockets,
295300
_timelineMicrosOffset,
296301
currentRequests: _currentNetworkRequests,
297302
);
@@ -339,6 +344,11 @@ class NetworkController extends DevToolsScreenController
339344
await networkService.updateLastHttpDataRefreshTime(
340345
alreadyRecordingHttp: alreadyRecordingHttp,
341346
);
347+
unawaited(
348+
networkService.updateLastWebSocketDataRefreshTime(
349+
alreadyRecordingWebSocket: alreadyRecordingHttp,
350+
),
351+
);
342352
final timestamp = await networkService.updateLastSocketDataRefreshTime(
343353
alreadyRecordingSocketData: alreadyRecordingSocketData,
344354
);
@@ -391,10 +401,11 @@ class NetworkController extends DevToolsScreenController
391401
_recordingNotifier.value = state;
392402
}
393403

394-
/// Updates the last refresh time of the socket and http data refresh times.
404+
/// Updates the last refresh time of the HTTP, WebSocket, and Socket data.
395405
///
396-
/// This will ensure that future fetches for http and socket requests will at
397-
/// most fetch requests since [updateLastRefreshTime] was called.
406+
/// This will ensure that future fetches for http, socket and websocket
407+
/// requests will atmost fetch requests since [updateLastRefreshTime] was
408+
/// called.
398409
Future<void> updateLastRefreshTime() async {
399410
await networkService.updateLastHttpDataRefreshTime();
400411
await networkService.updateLastSocketDataRefreshTime();
@@ -520,17 +531,21 @@ class NetworkController extends DevToolsScreenController
520531
OfflineScreenData prepareOfflineScreenData() {
521532
final httpRequestData = <DartIOHttpRequestData>[];
522533
final socketData = <Socket>[];
534+
final webSocketData = <WebSocket>[];
523535
for (final request in _currentNetworkRequests.value) {
524536
if (request is DartIOHttpRequestData) {
525537
httpRequestData.add(request);
526538
} else if (request is Socket) {
527539
socketData.add(request);
540+
} else if (request is WebSocket) {
541+
webSocketData.add(request);
528542
}
529543
}
530544

531545
final offlineData = OfflineNetworkData(
532546
httpRequestData: httpRequestData,
533547
socketData: socketData,
548+
webSocketData: webSocketData,
534549
selectedRequestId: selectedRequest.value?.id,
535550
timelineMicrosOffset: _timelineMicrosOffset,
536551
);
@@ -571,10 +586,12 @@ class CurrentNetworkRequests extends ValueNotifier<List<NetworkRequest>> {
571586
void updateOrAddAll({
572587
required List<HttpProfileRequest> requests,
573588
required List<SocketStatistic> sockets,
589+
required List<WebSocketConnection> webSockets,
574590
required int timelineMicrosOffset,
575591
}) {
576592
_updateOrAddRequests(requests);
577593
_updateSocketProfiles(sockets, timelineMicrosOffset);
594+
_updateWebSocketProfiles(webSockets);
578595
notifyListeners();
579596
}
580597

@@ -631,6 +648,20 @@ class CurrentNetworkRequests extends ValueNotifier<List<NetworkRequest>> {
631648
}
632649
}
633650

651+
void _updateWebSocketProfiles(List<WebSocketConnection> webSockets) {
652+
for (final connection in webSockets) {
653+
final webSocket = WebSocket(connection);
654+
final existingRequest = _requestsById[webSocket.id];
655+
656+
if (existingRequest == null) {
657+
_requestsById[webSocket.id] = webSocket;
658+
value.add(webSocket);
659+
} else {
660+
(existingRequest as WebSocket).update(webSocket);
661+
}
662+
}
663+
}
664+
634665
void clear({bool partial = false}) {
635666
if (partial) {
636667
_requestsById.keys

‎packages/devtools_app/lib/src/screens/network/network_model.dart‎

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import 'package:vm_service/vm_service.dart';
99
import '../../shared/primitives/utils.dart';
1010
import '../../shared/ui/search.dart';
1111

12+
export 'package:vm_service/vm_service.dart' show WebSocketEvent;
13+
1214
abstract class NetworkRequest
1315
with ChangeNotifier, SearchableDataMixin, Serializable {
1416
String get method;
@@ -239,3 +241,153 @@ extension SocketExtension on List<Socket> {
239241
return map((socket) => socket._socket).toList();
240242
}
241243
}
244+
245+
class WebSocket extends NetworkRequest {
246+
WebSocket(this._connection);
247+
248+
factory WebSocket.fromJson(Map<String, Object?> json) {
249+
return WebSocket(
250+
WebSocketConnection(
251+
isolateId: json['isolateId'] as String,
252+
id: json['connectionId'] as String,
253+
uri: Uri.parse(json['uri'] as String),
254+
protocol: json['protocol'] as String?,
255+
state: json['state'] as String,
256+
connectTimestamp: DateTime.parse(json['connectTimestamp'] as String),
257+
openTimestamp: json['openTimestamp'] != null
258+
? DateTime.parse(json['openTimestamp'] as String)
259+
: null,
260+
closeTimestamp: json['closeTimestamp'] != null
261+
? DateTime.parse(json['closeTimestamp'] as String)
262+
: null,
263+
bytesSent: json['bytesSent'] as int,
264+
bytesReceived: json['bytesReceived'] as int,
265+
framesSent: json['framesSent'] as int,
266+
framesReceived: json['framesReceived'] as int,
267+
pingCount: json['pingCount'] as int,
268+
pongCount: json['pongCount'] as int,
269+
closeCode: json['closeCode'] as int?,
270+
closeReason: json['closeReason'] as String?,
271+
error: json['error'] as String?,
272+
lastUpdated: DateTime.parse(json['lastUpdated'] as String),
273+
events: (json['events'] as List<Object?>? ?? [])
274+
.whereType<Map<String, Object?>>()
275+
.map(WebSocketEvent.parse)
276+
.whereType<WebSocketEvent>()
277+
.toList(),
278+
),
279+
);
280+
}
281+
282+
WebSocketConnection _connection;
283+
List<WebSocketEvent> get events => _connection.events;
284+
285+
String get connectionId => _connection.id;
286+
String? get protocol => _connection.protocol;
287+
String get state => _connection.state;
288+
289+
DateTime get lastUpdated => _connection.lastUpdated;
290+
291+
int get bytesSent => _connection.bytesSent;
292+
int get bytesReceived => _connection.bytesReceived;
293+
int get framesSent => _connection.framesSent;
294+
int get framesReceived => _connection.framesReceived;
295+
int get pingCount => _connection.pingCount;
296+
int get pongCount => _connection.pongCount;
297+
int? get closeCode => _connection.closeCode;
298+
String? get closeReason => _connection.closeReason;
299+
String? get error => _connection.error;
300+
301+
void update(WebSocket other) {
302+
_connection = other._connection;
303+
notifyListeners();
304+
}
305+
306+
@override
307+
String get id => 'websocket:${_connection.isolateId}:${_connection.id}';
308+
309+
@override
310+
String get method => 'WEBSOCKET';
311+
312+
@override
313+
String get uri => _connection.uri.toString();
314+
315+
@override
316+
String? get contentType => 'websocket';
317+
318+
@override
319+
String get type => 'WebSocket';
320+
321+
@override
322+
Duration? get duration =>
323+
_connection.closeTimestamp?.difference(_connection.connectTimestamp);
324+
325+
@override
326+
DateTime get startTimestamp => _connection.connectTimestamp;
327+
328+
@override
329+
DateTime? get endTimestamp => _connection.closeTimestamp;
330+
331+
@override
332+
String get status => _connection.state;
333+
334+
@override
335+
int? get port => _connection.uri.hasPort ? _connection.uri.port : null;
336+
337+
@override
338+
int get requestBytes => _connection.bytesSent;
339+
340+
@override
341+
int get responseBytes => _connection.bytesReceived;
342+
343+
@override
344+
bool get didFail => _connection.state == 'error';
345+
346+
@override
347+
bool get inProgress =>
348+
_connection.state != 'closed' && _connection.state != 'error';
349+
350+
@override
351+
bool operator ==(Object other) => other is WebSocket && id == other.id;
352+
353+
@override
354+
int get hashCode => id.hashCode;
355+
356+
@override
357+
Map<String, Object?> toJson() {
358+
return {
359+
'isolateId': _connection.isolateId,
360+
'connectionId': connectionId,
361+
'uri': uri,
362+
'protocol': protocol,
363+
'state': state,
364+
'connectTimestamp': startTimestamp.toIso8601String(),
365+
'openTimestamp': _connection.openTimestamp?.toIso8601String(),
366+
'closeTimestamp': endTimestamp?.toIso8601String(),
367+
'bytesSent': bytesSent,
368+
'bytesReceived': bytesReceived,
369+
'framesSent': framesSent,
370+
'framesReceived': framesReceived,
371+
'pingCount': pingCount,
372+
'pongCount': pongCount,
373+
'closeCode': closeCode,
374+
'closeReason': closeReason,
375+
'error': error,
376+
'lastUpdated': lastUpdated.toIso8601String(),
377+
'events': events
378+
.map(
379+
(event) => {
380+
'timestamp': event.timestamp.microsecondsSinceEpoch,
381+
'event': event.event,
382+
'frameNumber': event.frameNumber,
383+
'direction': event.direction,
384+
'opcode': event.opcode,
385+
'payloadSize': event.payloadSize,
386+
'errorType': event.errorType,
387+
'errorMessage': event.errorMessage,
388+
},
389+
)
390+
.toList(),
391+
};
392+
}
393+
}

‎packages/devtools_app/lib/src/screens/network/network_request_inspector.dart‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class NetworkRequestInspector extends StatelessWidget {
1818
const NetworkRequestInspector({super.key});
1919

2020
static const _overviewTabTitle = 'Overview';
21+
static const _framesTabTitle = 'Frames';
2122
static const _headersTabTitle = 'Headers';
2223
static const _requestTabTitle = 'Request';
2324
static const _responseTabTitle = 'Response';
@@ -68,6 +69,11 @@ class NetworkRequestInspector extends StatelessWidget {
6869
tab: _buildTab(tabName: NetworkRequestInspector._overviewTabTitle),
6970
tabView: NetworkRequestOverviewView(data),
7071
),
72+
if (data is WebSocket)
73+
(
74+
tab: _buildTab(tabName: NetworkRequestInspector._framesTabTitle),
75+
tabView: WebSocketFramesView(data),
76+
),
7177
if (data is DartIOHttpRequestData) ...[
7278
(
7379
tab: _buildTab(tabName: NetworkRequestInspector._headersTabTitle),

0 commit comments

Comments
 (0)