Adds WebSocket support to Network panel (GSoC'26) (#9968)
diff --git a/packages/devtools_app/lib/src/screens/network/network_controller.dart b/packages/devtools_app/lib/src/screens/network/network_controller.dart
index 086703f..88490a2 100644
--- a/packages/devtools_app/lib/src/screens/network/network_controller.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_controller.dart
@@ -246,6 +246,7 @@
       ..updateOrAddAll(
         requests: httpProfileData,
         sockets: socketStatsData,
+        webSockets: const [],
         timelineMicrosOffset: offlineData.timelineMicrosOffset ?? 0,
       );
     _filterAndRefreshSearchMatches();
@@ -265,12 +266,14 @@
   void processNetworkTrafficHelper(
     List<SocketStatistic> sockets,
     List<HttpProfileRequest>? httpRequests,
+    List<WebSocketConnection> webSockets,
     int timelineMicrosOffset, {
     required CurrentNetworkRequests currentRequests,
   }) {
     currentRequests.updateOrAddAll(
       requests: httpRequests!,
       sockets: sockets,
+      webSockets: webSockets,
       timelineMicrosOffset: timelineMicrosOffset,
     );
 
@@ -287,11 +290,13 @@
   void processNetworkTraffic({
     required List<SocketStatistic> sockets,
     required List<HttpProfileRequest>? httpRequests,
+    required List<WebSocketConnection> webSockets,
   }) {
     // Trigger refresh.
     processNetworkTrafficHelper(
       sockets,
       httpRequests,
+      webSockets,
       _timelineMicrosOffset,
       currentRequests: _currentNetworkRequests,
     );
@@ -339,6 +344,11 @@
     await networkService.updateLastHttpDataRefreshTime(
       alreadyRecordingHttp: alreadyRecordingHttp,
     );
+    unawaited(
+      networkService.updateLastWebSocketDataRefreshTime(
+        alreadyRecordingWebSocket: alreadyRecordingHttp,
+      ),
+    );
     final timestamp = await networkService.updateLastSocketDataRefreshTime(
       alreadyRecordingSocketData: alreadyRecordingSocketData,
     );
@@ -391,10 +401,11 @@
     _recordingNotifier.value = state;
   }
 
-  /// Updates the last refresh time of the socket and http data refresh times.
+  /// Updates the last refresh time of the HTTP, WebSocket, and Socket data.
   ///
-  /// This will ensure that future fetches for http and socket requests will at
-  /// most fetch requests since [updateLastRefreshTime] was called.
+  /// This will ensure that future fetches for http, socket and websocket
+  /// requests will atmost fetch requests since [updateLastRefreshTime] was
+  /// called.
   Future<void> updateLastRefreshTime() async {
     await networkService.updateLastHttpDataRefreshTime();
     await networkService.updateLastSocketDataRefreshTime();
@@ -520,17 +531,21 @@
   OfflineScreenData prepareOfflineScreenData() {
     final httpRequestData = <DartIOHttpRequestData>[];
     final socketData = <Socket>[];
+    final webSocketData = <WebSocket>[];
     for (final request in _currentNetworkRequests.value) {
       if (request is DartIOHttpRequestData) {
         httpRequestData.add(request);
       } else if (request is Socket) {
         socketData.add(request);
+      } else if (request is WebSocket) {
+        webSocketData.add(request);
       }
     }
 
     final offlineData = OfflineNetworkData(
       httpRequestData: httpRequestData,
       socketData: socketData,
+      webSocketData: webSocketData,
       selectedRequestId: selectedRequest.value?.id,
       timelineMicrosOffset: _timelineMicrosOffset,
     );
@@ -571,10 +586,12 @@
   void updateOrAddAll({
     required List<HttpProfileRequest> requests,
     required List<SocketStatistic> sockets,
+    required List<WebSocketConnection> webSockets,
     required int timelineMicrosOffset,
   }) {
     _updateOrAddRequests(requests);
     _updateSocketProfiles(sockets, timelineMicrosOffset);
+    _updateWebSocketProfiles(webSockets);
     notifyListeners();
   }
 
@@ -631,6 +648,20 @@
     }
   }
 
+  void _updateWebSocketProfiles(List<WebSocketConnection> webSockets) {
+    for (final connection in webSockets) {
+      final webSocket = WebSocket(connection);
+      final existingRequest = _requestsById[webSocket.id];
+
+      if (existingRequest == null) {
+        _requestsById[webSocket.id] = webSocket;
+        value.add(webSocket);
+      } else {
+        (existingRequest as WebSocket).update(webSocket);
+      }
+    }
+  }
+
   void clear({bool partial = false}) {
     if (partial) {
       _requestsById.keys
diff --git a/packages/devtools_app/lib/src/screens/network/network_model.dart b/packages/devtools_app/lib/src/screens/network/network_model.dart
index 3d7fa03..f101106 100644
--- a/packages/devtools_app/lib/src/screens/network/network_model.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_model.dart
@@ -9,6 +9,8 @@
 import '../../shared/primitives/utils.dart';
 import '../../shared/ui/search.dart';
 
+export 'package:vm_service/vm_service.dart' show WebSocketEvent;
+
 abstract class NetworkRequest
     with ChangeNotifier, SearchableDataMixin, Serializable {
   String get method;
@@ -239,3 +241,153 @@
     return map((socket) => socket._socket).toList();
   }
 }
+
+class WebSocket extends NetworkRequest {
+  WebSocket(this._connection);
+
+  factory WebSocket.fromJson(Map<String, Object?> json) {
+    return WebSocket(
+      WebSocketConnection(
+        isolateId: json['isolateId'] as String,
+        id: json['connectionId'] as String,
+        uri: Uri.parse(json['uri'] as String),
+        protocol: json['protocol'] as String?,
+        state: json['state'] as String,
+        connectTimestamp: DateTime.parse(json['connectTimestamp'] as String),
+        openTimestamp: json['openTimestamp'] != null
+            ? DateTime.parse(json['openTimestamp'] as String)
+            : null,
+        closeTimestamp: json['closeTimestamp'] != null
+            ? DateTime.parse(json['closeTimestamp'] as String)
+            : null,
+        bytesSent: json['bytesSent'] as int,
+        bytesReceived: json['bytesReceived'] as int,
+        framesSent: json['framesSent'] as int,
+        framesReceived: json['framesReceived'] as int,
+        pingCount: json['pingCount'] as int,
+        pongCount: json['pongCount'] as int,
+        closeCode: json['closeCode'] as int?,
+        closeReason: json['closeReason'] as String?,
+        error: json['error'] as String?,
+        lastUpdated: DateTime.parse(json['lastUpdated'] as String),
+        events: (json['events'] as List<Object?>? ?? [])
+            .whereType<Map<String, Object?>>()
+            .map(WebSocketEvent.parse)
+            .whereType<WebSocketEvent>()
+            .toList(),
+      ),
+    );
+  }
+
+  WebSocketConnection _connection;
+  List<WebSocketEvent> get events => _connection.events;
+
+  String get connectionId => _connection.id;
+  String? get protocol => _connection.protocol;
+  String get state => _connection.state;
+
+  DateTime get lastUpdated => _connection.lastUpdated;
+
+  int get bytesSent => _connection.bytesSent;
+  int get bytesReceived => _connection.bytesReceived;
+  int get framesSent => _connection.framesSent;
+  int get framesReceived => _connection.framesReceived;
+  int get pingCount => _connection.pingCount;
+  int get pongCount => _connection.pongCount;
+  int? get closeCode => _connection.closeCode;
+  String? get closeReason => _connection.closeReason;
+  String? get error => _connection.error;
+
+  void update(WebSocket other) {
+    _connection = other._connection;
+    notifyListeners();
+  }
+
+  @override
+  String get id => 'websocket:${_connection.isolateId}:${_connection.id}';
+
+  @override
+  String get method => 'WEBSOCKET';
+
+  @override
+  String get uri => _connection.uri.toString();
+
+  @override
+  String? get contentType => 'websocket';
+
+  @override
+  String get type => 'WebSocket';
+
+  @override
+  Duration? get duration =>
+      _connection.closeTimestamp?.difference(_connection.connectTimestamp);
+
+  @override
+  DateTime get startTimestamp => _connection.connectTimestamp;
+
+  @override
+  DateTime? get endTimestamp => _connection.closeTimestamp;
+
+  @override
+  String get status => _connection.state;
+
+  @override
+  int? get port => _connection.uri.hasPort ? _connection.uri.port : null;
+
+  @override
+  int get requestBytes => _connection.bytesSent;
+
+  @override
+  int get responseBytes => _connection.bytesReceived;
+
+  @override
+  bool get didFail => _connection.state == 'error';
+
+  @override
+  bool get inProgress =>
+      _connection.state != 'closed' && _connection.state != 'error';
+
+  @override
+  bool operator ==(Object other) => other is WebSocket && id == other.id;
+
+  @override
+  int get hashCode => id.hashCode;
+
+  @override
+  Map<String, Object?> toJson() {
+    return {
+      'isolateId': _connection.isolateId,
+      'connectionId': connectionId,
+      'uri': uri,
+      'protocol': protocol,
+      'state': state,
+      'connectTimestamp': startTimestamp.toIso8601String(),
+      'openTimestamp': _connection.openTimestamp?.toIso8601String(),
+      'closeTimestamp': endTimestamp?.toIso8601String(),
+      'bytesSent': bytesSent,
+      'bytesReceived': bytesReceived,
+      'framesSent': framesSent,
+      'framesReceived': framesReceived,
+      'pingCount': pingCount,
+      'pongCount': pongCount,
+      'closeCode': closeCode,
+      'closeReason': closeReason,
+      'error': error,
+      'lastUpdated': lastUpdated.toIso8601String(),
+      'events': events
+          .map(
+            (event) => {
+              'timestamp': event.timestamp.microsecondsSinceEpoch,
+              'event': event.event,
+              'frameNumber': event.frameNumber,
+              'direction': event.direction,
+              'opcode': event.opcode,
+              'payloadSize': event.payloadSize,
+              'errorType': event.errorType,
+              'errorMessage': event.errorMessage,
+            },
+          )
+          .toList(),
+    };
+  }
+}
diff --git a/packages/devtools_app/lib/src/screens/network/network_request_inspector.dart b/packages/devtools_app/lib/src/screens/network/network_request_inspector.dart
index a2f4764..5297d7b 100644
--- a/packages/devtools_app/lib/src/screens/network/network_request_inspector.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_request_inspector.dart
@@ -18,6 +18,7 @@
   const NetworkRequestInspector({super.key});
 
   static const _overviewTabTitle = 'Overview';
+  static const _framesTabTitle = 'Frames';
   static const _headersTabTitle = 'Headers';
   static const _requestTabTitle = 'Request';
   static const _responseTabTitle = 'Response';
@@ -68,6 +69,11 @@
         tab: _buildTab(tabName: NetworkRequestInspector._overviewTabTitle),
         tabView: NetworkRequestOverviewView(data),
       ),
+      if (data is WebSocket)
+        (
+          tab: _buildTab(tabName: NetworkRequestInspector._framesTabTitle),
+          tabView: WebSocketFramesView(data),
+        ),
       if (data is DartIOHttpRequestData) ...[
         (
           tab: _buildTab(tabName: NetworkRequestInspector._headersTabTitle),
diff --git a/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart b/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
index 118f76c..1567b4f 100644
--- a/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_request_inspector_views.dart
@@ -13,6 +13,8 @@
 import '../../shared/http/http_request_data.dart';
 import '../../shared/primitives/byte_utils.dart';
 import '../../shared/primitives/utils.dart';
+import '../../shared/table/table.dart';
+import '../../shared/table/table_data.dart';
 import '../../shared/ui/colors.dart';
 import '../../shared/ui/common_widgets.dart';
 import 'network_controller.dart';
@@ -596,6 +598,129 @@
   }
 }
 
+class _WebSocketFrameColumn extends ColumnData<WebSocketEvent> {
+  const _WebSocketFrameColumn() : super('Frame', fixedWidthPx: 60);
+
+  @override
+  int? getValue(WebSocketEvent dataObject) => dataObject.frameNumber;
+
+  @override
+  String getDisplayValue(WebSocketEvent dataObject) =>
+      dataObject.frameNumber?.toString() ?? '--';
+}
+
+class _WebSocketTimestampColumn extends ColumnData<WebSocketEvent> {
+  const _WebSocketTimestampColumn() : super('Timestamp', fixedWidthPx: 150);
+
+  @override
+  DateTime getValue(WebSocketEvent dataObject) => dataObject.timestamp;
+
+  @override
+  String getDisplayValue(WebSocketEvent dataObject) =>
+      formatDateTime(dataObject.timestamp);
+}
+
+class _WebSocketDirectionColumn extends ColumnData<WebSocketEvent> {
+  const _WebSocketDirectionColumn() : super('Direction', fixedWidthPx: 100);
+
+  @override
+  String getValue(WebSocketEvent dataObject) => dataObject.direction ?? '--';
+
+  @override
+  String getDisplayValue(WebSocketEvent dataObject) =>
+      dataObject.direction ?? '--';
+}
+
+class _WebSocketEventColumn extends ColumnData<WebSocketEvent> {
+  const _WebSocketEventColumn() : super('Event', fixedWidthPx: 120);
+
+  @override
+  String getValue(WebSocketEvent dataObject) {
+    final event = dataObject.event;
+    return event.startsWith('WebSocket.')
+        ? event.substring('WebSocket.'.length)
+        : event;
+  }
+
+  @override
+  String getDisplayValue(WebSocketEvent dataObject) => getValue(dataObject);
+}
+
+class _WebSocketOpcodeColumn extends ColumnData<WebSocketEvent> {
+  const _WebSocketOpcodeColumn() : super('Opcode', fixedWidthPx: 100);
+
+  @override
+  String getValue(WebSocketEvent dataObject) =>
+      dataObject.opcode?.toString() ?? '--';
+
+  @override
+  String getDisplayValue(WebSocketEvent dataObject) =>
+      dataObject.opcode?.toString() ?? '--';
+}
+
+class _WebSocketSizeColumn extends ColumnData<WebSocketEvent> {
+  const _WebSocketSizeColumn() : super('Size', fixedWidthPx: 100);
+
+  @override
+  int? getValue(WebSocketEvent dataObject) => dataObject.payloadSize;
+
+  @override
+  String getDisplayValue(WebSocketEvent dataObject) =>
+      dataObject.payloadSize != null
+      ? formatBytes(dataObject.payloadSize!)
+      : '--';
+}
+
+class _WebSocketErrorColumn extends ColumnData<WebSocketEvent> {
+  const _WebSocketErrorColumn() : super('Error', fixedWidthPx: 180);
+
+  @override
+  String getValue(WebSocketEvent dataObject) =>
+      dataObject.errorMessage ?? dataObject.errorType ?? '--';
+
+  @override
+  String getDisplayValue(WebSocketEvent dataObject) =>
+      dataObject.errorMessage ?? dataObject.errorType ?? '--';
+}
+
+class WebSocketFramesView extends StatelessWidget {
+  const WebSocketFramesView(this.data, {super.key});
+
+  final WebSocket data;
+
+  static const _columns = <ColumnData<WebSocketEvent>>[
+    _WebSocketFrameColumn(),
+    _WebSocketTimestampColumn(),
+    _WebSocketDirectionColumn(),
+    _WebSocketEventColumn(),
+    _WebSocketOpcodeColumn(),
+    _WebSocketSizeColumn(),
+    _WebSocketErrorColumn(),
+  ];
+
+  @override
+  Widget build(BuildContext context) {
+    final events = data.events;
+
+    if (events.isEmpty) {
+      return const Center(child: Text('No WebSocket events'));
+    }
+
+    return Padding(
+      padding: const EdgeInsets.all(defaultSpacing),
+      child: FlatTable<WebSocketEvent>(
+        keyFactory: (event) => ValueKey<WebSocketEvent>(event),
+        data: events,
+        dataKey: 'websocket-frames',
+        columns: _columns,
+        defaultSortColumn: _columns.first,
+        defaultSortDirection: SortDirection.ascending,
+        sizeColumnsToFit: false,
+      ),
+    );
+  }
+}
+
 class NetworkRequestOverviewView extends StatelessWidget {
   const NetworkRequestOverviewView(this.data, {super.key});
 
@@ -605,6 +730,8 @@
   static const httpTimingGraphKey = Key('Http Timing Graph Key');
   @visibleForTesting
   static const socketTimingGraphKey = Key('Socket Timing Graph Key');
+  @visibleForTesting
+  static const webSocketTimingGraphKey = Key('WebSocket Timing Graph Key');
 
   final NetworkRequest data;
 
@@ -615,7 +742,10 @@
         padding: const EdgeInsets.all(defaultSpacing),
         children: [
           ..._buildGeneralRows(context),
-          if (data is Socket) ..._buildSocketOverviewRows(context),
+          if (data is Socket)
+            ..._buildSocketOverviewRows(context)
+          else if (data is WebSocket)
+            ..._buildWebSocketOverviewRows(context),
           const PaddedDivider(
             padding: EdgeInsets.only(bottom: denseRowSpacing),
           ),
@@ -661,14 +791,16 @@
         const SizedBox(height: defaultSpacing),
       ],
 
-      _buildRow(
-        context: context,
-        title: 'Response Size',
-        child: _valueText(bytes != null ? formatBytes(bytes) : '-'),
-      ),
-      const SizedBox(height: defaultSpacing),
+      if (data is! WebSocket) ...[
+        _buildRow(
+          context: context,
+          title: 'Response Size',
+          child: _valueText(bytes != null ? formatBytes(bytes) : '-'),
+        ),
+        const SizedBox(height: defaultSpacing),
+      ],
 
-      if (data.contentType != null) ...[
+      if (data.contentType != null && data is! WebSocket) ...[
         _buildRow(
           context: context,
           title: 'Content type',
@@ -684,20 +816,28 @@
       _buildRow(
         context: context,
         title: 'Timing',
-        child: data is Socket
-            ? _buildSocketTimeGraph(context)
-            : _buildHttpTimeGraph(),
+        child: switch (data) {
+          Socket() => _buildSocketTimeGraph(context),
+          WebSocket() => _buildWebSocketTimeGraph(context),
+          _ => _buildHttpTimeGraph(),
+        },
       ),
       const SizedBox(height: denseSpacing),
       _buildRow(
         context: context,
         title: '',
-        child: _valueText(data.durationDisplay),
+        child: _valueText(
+          data.duration != null
+              ? durationText(data.duration!, fractionDigits: 0)
+              : 'Pending',
+        ),
       ),
       const SizedBox(height: defaultSpacing),
-      ...data is Socket
-          ? _buildSocketTimingRows(context)
-          : _buildHttpTimingRows(context),
+      ...switch (data) {
+        Socket() => _buildSocketTimingRows(context),
+        WebSocket() => _buildWebSocketTimingRows(context),
+        _ => _buildHttpTimingRows(context),
+      },
       const SizedBox(height: defaultSpacing),
       _buildRow(
         context: context,
@@ -717,7 +857,7 @@
     ];
   }
 
-  Duration? get _totalDuration => (data as DartIOHttpRequestData).duration;
+  Duration? get _totalDuration => data.duration;
 
   Widget _buildTimingRow(
     Color color,
@@ -839,6 +979,116 @@
     ];
   }
 
+  List<Widget> _buildWebSocketOverviewRows(BuildContext context) {
+    final webSocket = data as WebSocket;
+
+    return [
+      _buildRow(
+        context: context,
+        title: 'Connection id',
+        child: _valueText(webSocket.connectionId),
+      ),
+      const SizedBox(height: defaultSpacing),
+      _buildRow(
+        context: context,
+        title: 'Protocol',
+        child: _valueText(webSocket.protocol ?? '--'),
+      ),
+      const SizedBox(height: defaultSpacing),
+      _buildRow(
+        context: context,
+        title: 'Bytes sent',
+        child: _valueText(formatBytes(webSocket.bytesSent)),
+      ),
+      const SizedBox(height: defaultSpacing),
+      _buildRow(
+        context: context,
+        title: 'Bytes received',
+        child: _valueText(formatBytes(webSocket.bytesReceived)),
+      ),
+      const SizedBox(height: defaultSpacing),
+      _buildRow(
+        context: context,
+        title: 'Frames sent',
+        child: _valueText('${webSocket.framesSent}'),
+      ),
+      const SizedBox(height: defaultSpacing),
+      _buildRow(
+        context: context,
+        title: 'Frames received',
+        child: _valueText('${webSocket.framesReceived}'),
+      ),
+      const SizedBox(height: defaultSpacing),
+      _buildRow(
+        context: context,
+        title: 'Ping count',
+        child: _valueText('${webSocket.pingCount}'),
+      ),
+      const SizedBox(height: defaultSpacing),
+      _buildRow(
+        context: context,
+        title: 'Pong count',
+        child: _valueText('${webSocket.pongCount}'),
+      ),
+      if (webSocket.closeCode case final closeCode?) ...[
+        const SizedBox(height: defaultSpacing),
+        _buildRow(
+          context: context,
+          title: 'Close code',
+          child: _valueText('$closeCode'),
+        ),
+      ],
+      if (webSocket.closeReason case final closeReason?) ...[
+        const SizedBox(height: defaultSpacing),
+        _buildRow(
+          context: context,
+          title: 'Close reason',
+          child: _valueText(closeReason),
+        ),
+      ],
+      if (webSocket.error case final error?) ...[
+        const SizedBox(height: defaultSpacing),
+        _buildRow(
+          context: context,
+          title: 'Error',
+          child: _valueText(
+            error,
+            TextStyle(color: Theme.of(context).colorScheme.error),
+          ),
+        ),
+      ],
+    ];
+  }
+
+  List<Widget> _buildWebSocketTimingRows(BuildContext context) {
+    final webSocket = data as WebSocket;
+
+    return [
+      _buildRow(
+        context: context,
+        title: 'Last updated',
+        child: _valueText(formatDateTime(webSocket.lastUpdated)),
+      ),
+      const SizedBox(height: defaultSpacing),
+      _buildRow(
+        context: context,
+        title: 'Frames',
+        child: _valueText(
+          '${webSocket.framesSent} sent, '
+          '${webSocket.framesReceived} received',
+        ),
+      ),
+    ];
+  }
+
+  Widget _buildWebSocketTimeGraph(BuildContext context) {
+    return Container(
+      key: webSocketTimingGraphKey,
+      height: _timingGraphHeight,
+      color: Theme.of(context).colorScheme.primary,
+    );
+  }
+
   Widget _buildSocketTimeGraph(BuildContext context) {
     return Container(
       key: socketTimingGraphKey,
diff --git a/packages/devtools_app/lib/src/screens/network/network_service.dart b/packages/devtools_app/lib/src/screens/network/network_service.dart
index 59fbb64..34132bb 100644
--- a/packages/devtools_app/lib/src/screens/network/network_service.dart
+++ b/packages/devtools_app/lib/src/screens/network/network_service.dart
@@ -21,6 +21,10 @@
   /// use the VM's monotonic timeline clock — not wall-clock time.
   final lastHttpDataRefreshTimePerIsolate = <String, int>{};
 
+  /// Tracks the time (microseconds since epoch) that the WebSocket profile was
+  /// last retrieved for a given isolate ID.
+  final lastWebSocketDataRefreshTimePerIsolate = <String, int>{};
+
   /// Updates the last Socket data refresh time to the current time.
   ///
   /// If [alreadyRecordingSocketData] is true, it's unclear when the last
@@ -67,6 +71,26 @@
     }
   }
 
+  /// Updates the last WebSocket data refresh time to the current time.
+  ///
+  /// WebSocket profiling is controlled by HttpClient.enableTimelineLogging,
+  /// so this timestamp follows the HTTP timeline logging lifecycle.
+  Future<void> updateLastWebSocketDataRefreshTime({
+    bool alreadyRecordingWebSocket = false,
+  }) async {
+    if (!alreadyRecordingWebSocket) {
+      final service = serviceConnection.serviceManager.service;
+      if (service == null) return;
+
+      final timestamp = (await service.getVMTimelineMicros()).timestamp!;
+
+      for (final isolateId
+          in lastWebSocketDataRefreshTimePerIsolate.keys.toList()) {
+        lastWebSocketDataRefreshTimePerIsolate[isolateId] = timestamp;
+      }
+    }
+  }
+
   /// Force refreshes the HTTP requests logged to the timeline as well as any
   /// recorded Socket traffic.
   ///
@@ -86,6 +110,10 @@
       if (cancelledCallback?.call() ?? false) return;
 
       networkController.lastSocketDataRefreshMicros = timestamp;
+
+      final webSockets = await _refreshWebSocketProfile();
+      if (cancelledCallback?.call() ?? false) return;
+
       List<HttpProfileRequest>? httpRequests;
       httpRequests = await _refreshHttpProfile();
       if (cancelledCallback?.call() ?? false) return;
@@ -93,6 +121,7 @@
       networkController.processNetworkTraffic(
         sockets: sockets,
         httpRequests: httpRequests,
+        webSockets: webSockets,
       );
     } on RPCError catch (e) {
       if (!e.isServiceDisposedError) {
@@ -145,6 +174,67 @@
     });
   }
 
+  Future<List<WebSocketConnection>> _refreshWebSocketProfile() async {
+    final service = serviceConnection.serviceManager.service;
+    if (service == null) return const [];
+
+    final connections = <WebSocketConnection>[];
+
+    await service.forEachIsolate((isolate) async {
+      final isolateId = isolate.id!;
+
+      if (!await service.isWebSocketProfilingAvailable(isolateId)) {
+        return;
+      }
+
+      final profile = await service.getWebSocketProfile(
+        isolateId,
+        updatedSince: DateTime.fromMicrosecondsSinceEpoch(
+          lastWebSocketDataRefreshTimePerIsolate.putIfAbsent(
+            isolateId,
+            // If a new isolate has spawned, request all WebSocket connections
+            // from the start of the profile.
+            () => 0,
+          ),
+        ),
+      );
+
+      final fullConnections = await Future.wait(
+        profile.connections.map(
+          (connection) =>
+              service.getWebSocketConnection(isolateId, connection.id),
+        ),
+      );
+
+      connections.addAll(fullConnections);
+
+      // Use the profile timestamp rather than DateTime.now() so that we don't
+      // miss updates between the profile snapshot and this assignment.
+      lastWebSocketDataRefreshTimePerIsolate[isolateId] =
+          profile.timestamp.microsecondsSinceEpoch;
+    });
+
+    return connections;
+  }
+
+  Future<void> _clearWebSocketProfile() async {
+    final service = serviceConnection.serviceManager.service;
+    if (service == null) return;
+
+    await service.forEachIsolate((isolate) async {
+      final isolateId = isolate.id!;
+
+      if (!await service.isWebSocketProfilingAvailable(isolateId)) {
+        return;
+      }
+
+      final future = service.clearWebSocketProfile(isolateId);
+
+      // The call may not complete immediately if the isolate is paused.
+      await timeout(future, 500);
+    });
+  }
+
   Future<List<SocketStatistic>> _refreshSockets() async {
     final service = serviceConnection.serviceManager.service;
     if (service == null) return [];
@@ -214,6 +304,7 @@
       await updateLastHttpDataRefreshTime();
       await _clearSocketProfile();
       await _clearHttpProfile();
+      await _clearWebSocketProfile();
     } on RPCError catch (e) {
       if (!e.isServiceDisposedError) {
         rethrow;
diff --git a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart
index 2a7cc91..09c12a0 100644
--- a/packages/devtools_app/lib/src/screens/network/offline_network_data.dart
+++ b/packages/devtools_app/lib/src/screens/network/offline_network_data.dart
@@ -16,6 +16,7 @@
   OfflineNetworkData({
     required this.httpRequestData,
     required this.socketData,
+    required this.webSocketData,
     this.selectedRequestId,
     required this.timelineMicrosOffset,
   });
@@ -62,12 +63,24 @@
       selectedRequestId:
           json[_OfflineDataKeys.selectedRequestId.name] as String?,
       socketData: socketData,
+      webSocketData:
+          (json[_OfflineDataKeys.webSocketData.name] as List<Object?>?)
+              ?.map((e) {
+                if (e is Map<String, Object?>) {
+                  return WebSocket.fromJson(e);
+                }
+                return null;
+              })
+              .whereType<WebSocket>()
+              .toList() ??
+          [],
       timelineMicrosOffset: timelineMicrosOffset as int? ?? 0,
     );
   }
 
   @visibleForTesting
-  bool get isEmpty => httpRequestData.isEmpty && socketData.isEmpty;
+  bool get isEmpty =>
+      httpRequestData.isEmpty && socketData.isEmpty && webSocketData.isEmpty;
 
   /// List of current [DartIOHttpRequestData] network requests.
   final List<DartIOHttpRequestData> httpRequestData;
@@ -81,6 +94,9 @@
   /// The list of socket statistics for the offline network data.
   final List<Socket> socketData;
 
+  /// The list of WebSocket connections for the offline network data.
+  final List<WebSocket> webSocketData;
+
   /// Converts the current offline data to a JSON format.
   @override
   Map<String, Object?> toJson() {
@@ -92,6 +108,9 @@
       _OfflineDataKeys.socketData.name: socketData
           .map((e) => e.toJson())
           .toList(),
+      _OfflineDataKeys.webSocketData.name: webSocketData
+          .map((e) => e.toJson())
+          .toList(),
       _OfflineDataKeys.timelineMicrosOffset.name: timelineMicrosOffset,
     };
   }
@@ -101,6 +120,7 @@
   httpRequestData,
   selectedRequestId,
   socketData,
+  webSocketData,
   request,
   timelineMicrosOffset,
 }
diff --git a/packages/devtools_app/pubspec.yaml b/packages/devtools_app/pubspec.yaml
index c7e55d7..a95b396 100644
--- a/packages/devtools_app/pubspec.yaml
+++ b/packages/devtools_app/pubspec.yaml
@@ -54,7 +54,7 @@
   stack_trace: ^1.12.0
   string_scanner: ^1.4.0
   unified_analytics: ^7.0.0
-  vm_service: ^15.0.2
+  vm_service: ^15.3.0
   vm_service_protos: ^2.0.0
   vm_snapshot_analysis: ^0.7.6
   web: ^1.0.0
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index 998ed94..01ad0a7 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -53,6 +53,9 @@
 
 ## Network profiler updates
 
+* Added WebSocket support to the Network profiler, including WebSocket
+  connection details, lifecycle events, frame-level inspection, and connection
+  timing information. [#9968](https://github.com/flutter/devtools/pull/9968)
 * Fixed an issue where the Network tab would stop capturing HTTP requests after
   a hot restart. -
   [#9856](https://github.com/flutter/devtools/pull/9856)
diff --git a/packages/devtools_app/test/screens/network/network_controller_test.dart b/packages/devtools_app/test/screens/network/network_controller_test.dart
index 09553eb..2da78f0 100644
--- a/packages/devtools_app/test/screens/network/network_controller_test.dart
+++ b/packages/devtools_app/test/screens/network/network_controller_test.dart
@@ -420,6 +420,7 @@
           currentNetworkRequests.updateOrAddAll(
             requests: reqs,
             sockets: sockets,
+            webSockets: const [],
             timelineMicrosOffset: 0,
           );
           expect(notifyCount, 1);
@@ -439,6 +440,7 @@
           currentNetworkRequests.updateOrAddAll(
             requests: [request1Done],
             sockets: [socketStats1Done],
+            webSockets: const [],
             timelineMicrosOffset: 0,
           );
           expect(notifyCount, 2);
@@ -462,6 +464,7 @@
         currentNetworkRequests.updateOrAddAll(
           requests: [request1Done],
           sockets: const [],
+          webSockets: const [],
           timelineMicrosOffset: 0,
         );
 
@@ -473,6 +476,7 @@
         currentNetworkRequests.updateOrAddAll(
           requests: [request1CancelledWithStatusCode],
           sockets: const [],
+          webSockets: const [],
           timelineMicrosOffset: 0,
         );
 
@@ -488,6 +492,7 @@
         currentNetworkRequests.updateOrAddAll(
           requests: reqs,
           sockets: sockets,
+          webSockets: const [],
           timelineMicrosOffset: 0,
         );
 
@@ -513,6 +518,7 @@
         currentNetworkRequests.updateOrAddAll(
           requests: reqs,
           sockets: sockets,
+          webSockets: const [],
           timelineMicrosOffset: 0,
         );
 
@@ -539,5 +545,207 @@
         );
       });
     });
+    group('websocket', () {
+      final connectTime = DateTime(2026, 8, 18, 10).toUtc();
+      final openTime = connectTime.add(const Duration(seconds: 1));
+      final lastUpdated = openTime.add(const Duration(seconds: 2));
+
+      final events = [
+        WebSocketEvent(event: 'WebSocket.Connect', timestamp: connectTime),
+        WebSocketEvent(
+          event: 'WebSocket.Send',
+          timestamp: openTime,
+          frameNumber: 1,
+          direction: 'out',
+          opcode: 'text',
+          payloadSize: 5,
+        ),
+        WebSocketEvent(
+          event: 'WebSocket.Receive',
+          timestamp: lastUpdated,
+          frameNumber: 2,
+          direction: 'in',
+          opcode: 'text',
+          payloadSize: 7,
+        ),
+      ];
+
+      WebSocketConnection createConnection({
+        String id = '42',
+        String state = 'open',
+        int bytesSent = 5,
+        int bytesReceived = 7,
+        int framesSent = 1,
+        int framesReceived = 1,
+        List<WebSocketEvent>? connectionEvents,
+      }) {
+        return WebSocketConnection(
+          isolateId: 'isolate-1',
+          id: id,
+          uri: Uri.parse('wss://example.com/socket'),
+          state: state,
+          protocol: 'chat',
+          connectTimestamp: connectTime,
+          openTimestamp: openTime,
+          bytesSent: bytesSent,
+          bytesReceived: bytesReceived,
+          framesSent: framesSent,
+          framesReceived: framesReceived,
+          pingCount: 2,
+          pongCount: 2,
+          lastUpdated: lastUpdated,
+          events: connectionEvents ?? events,
+        );
+      }
+
+      test('adds WebSocket connection', () {
+        final connection = createConnection();
+
+        currentNetworkRequests.updateOrAddAll(
+          requests: const [],
+          sockets: const [],
+          webSockets: [connection],
+          timelineMicrosOffset: 0,
+        );
+
+        expect(currentNetworkRequests.value, hasLength(1));
+
+        final request = currentNetworkRequests.getRequest(
+          'websocket:isolate-1:42',
+        );
+
+        expect(request, isA<WebSocket>());
+
+        final webSocket = request! as WebSocket;
+        expect(webSocket.connectionId, '42');
+        expect(webSocket.uri, 'wss://example.com/socket');
+        expect(webSocket.protocol, 'chat');
+        expect(webSocket.state, 'open');
+        expect(webSocket.bytesSent, 5);
+        expect(webSocket.bytesReceived, 7);
+        expect(webSocket.framesSent, 1);
+        expect(webSocket.framesReceived, 1);
+        expect(webSocket.pingCount, 2);
+        expect(webSocket.pongCount, 2);
+        expect(webSocket.events, hasLength(3));
+      });
+
+      test('updates existing WebSocket instead of adding duplicate', () {
+        final initialConnection = createConnection();
+
+        currentNetworkRequests.updateOrAddAll(
+          requests: const [],
+          sockets: const [],
+          webSockets: [initialConnection],
+          timelineMicrosOffset: 0,
+        );
+
+        final updatedConnection = createConnection(
+          bytesSent: 15,
+          bytesReceived: 27,
+          framesSent: 3,
+          framesReceived: 4,
+          connectionEvents: [
+            ...events,
+            WebSocketEvent(
+              event: 'WebSocket.Send',
+              timestamp: lastUpdated,
+              frameNumber: 3,
+              direction: 'out',
+              opcode: 'binary',
+              payloadSize: 10,
+            ),
+          ],
+        );
+
+        currentNetworkRequests.updateOrAddAll(
+          requests: const [],
+          sockets: const [],
+          webSockets: [updatedConnection],
+          timelineMicrosOffset: 0,
+        );
+
+        expect(currentNetworkRequests.value, hasLength(1));
+
+        final request = currentNetworkRequests.getRequest(
+          'websocket:isolate-1:42',
+        );
+
+        expect(request, isA<WebSocket>());
+
+        final webSocket = request! as WebSocket;
+        expect(webSocket.bytesSent, 15);
+        expect(webSocket.bytesReceived, 27);
+        expect(webSocket.framesSent, 3);
+        expect(webSocket.framesReceived, 4);
+        expect(webSocket.events, hasLength(4));
+      });
+
+      test('notifies listeners once for multiple WebSocket connections', () {
+        final connection1 = createConnection();
+
+        final connection2 = createConnection(id: '43');
+
+        currentNetworkRequests.updateOrAddAll(
+          requests: const [],
+          sockets: const [],
+          webSockets: [connection1, connection2],
+          timelineMicrosOffset: 0,
+        );
+
+        expect(currentNetworkRequests.value, hasLength(2));
+        expect(notifyCount, 1);
+      });
+
+      test('maps closed WebSocket state and duration', () {
+        final closeTime = openTime.add(const Duration(seconds: 5));
+
+        final connection = WebSocketConnection(
+          isolateId: 'isolate-1',
+          id: '44',
+          uri: Uri.parse('wss://example.com/socket'),
+          state: 'closed',
+          protocol: 'chat',
+          connectTimestamp: connectTime,
+          openTimestamp: openTime,
+          closeTimestamp: closeTime,
+          closeCode: 1000,
+          closeReason: 'Normal closure',
+          lastUpdated: closeTime,
+          events: const [],
+        );
+
+        final webSocket = WebSocket(connection);
+
+        expect(webSocket.state, 'closed');
+        expect(webSocket.inProgress, false);
+        expect(webSocket.didFail, false);
+        expect(webSocket.endTimestamp, closeTime);
+        expect(webSocket.duration, closeTime.difference(connectTime));
+        expect(webSocket.closeCode, 1000);
+        expect(webSocket.closeReason, 'Normal closure');
+      });
+
+      test('maps WebSocket error state', () {
+        final connection = WebSocketConnection(
+          isolateId: 'isolate-1',
+          id: '45',
+          uri: Uri.parse('wss://example.com/socket'),
+          state: 'error',
+          connectTimestamp: connectTime,
+          closeTimestamp: lastUpdated,
+          error: 'Connection failed',
+          lastUpdated: lastUpdated,
+          events: const [],
+        );
+
+        final webSocket = WebSocket(connection);
+
+        expect(webSocket.state, 'error');
+        expect(webSocket.didFail, true);
+        expect(webSocket.inProgress, false);
+        expect(webSocket.error, 'Connection failed');
+      });
+    });
   });
 }
diff --git a/packages/devtools_app/test/screens/network/network_model_test.dart b/packages/devtools_app/test/screens/network/network_model_test.dart
index 5ecdbfb..42d9396 100644
--- a/packages/devtools_app/test/screens/network/network_model_test.dart
+++ b/packages/devtools_app/test/screens/network/network_model_test.dart
@@ -68,6 +68,221 @@
     });
   });
 
+  group('WebSocketNetworkRequest', () {
+    final connectTimestamp = DateTime.utc(2026, 8, 18, 10);
+    final openTimestamp = connectTimestamp.add(const Duration(seconds: 1));
+    final lastUpdated = openTimestamp.add(const Duration(seconds: 2));
+
+    WebSocketConnection createConnection({
+      String id = '42',
+      String state = 'open',
+      int bytesSent = 100,
+      int bytesReceived = 200,
+      int framesSent = 10,
+      int framesReceived = 20,
+      int pingCount = 3,
+      int pongCount = 3,
+      DateTime? closeTimestamp,
+      int? closeCode,
+      String? closeReason,
+      String? error,
+      List<WebSocketEvent>? events,
+    }) {
+      return WebSocketConnection(
+        isolateId: 'isolate-1',
+        id: id,
+        uri: Uri.parse('wss://example.com/socket'),
+        state: state,
+        protocol: 'chat',
+        connectTimestamp: connectTimestamp,
+        openTimestamp: openTimestamp,
+        closeTimestamp: closeTimestamp,
+        bytesSent: bytesSent,
+        bytesReceived: bytesReceived,
+        framesSent: framesSent,
+        framesReceived: framesReceived,
+        pingCount: pingCount,
+        pongCount: pongCount,
+        closeCode: closeCode,
+        closeReason: closeReason,
+        error: error,
+        lastUpdated: lastUpdated,
+        events: events ?? const [],
+      );
+    }
+
+    test('maps connection properties correctly', () {
+      final connection = createConnection();
+
+      final webSocket = WebSocket(connection);
+
+      expect(webSocket.connectionId, '42');
+      expect(webSocket.uri, 'wss://example.com/socket');
+      expect(webSocket.protocol, 'chat');
+      expect(webSocket.state, 'open');
+
+      expect(webSocket.bytesSent, 100);
+      expect(webSocket.bytesReceived, 200);
+      expect(webSocket.framesSent, 10);
+      expect(webSocket.framesReceived, 20);
+      expect(webSocket.pingCount, 3);
+      expect(webSocket.pongCount, 3);
+
+      expect(webSocket.closeCode, isNull);
+      expect(webSocket.closeReason, isNull);
+      expect(webSocket.error, isNull);
+      expect(webSocket.lastUpdated, lastUpdated);
+    });
+
+    test('maps NetworkRequest properties correctly', () {
+      final connection = createConnection();
+
+      final webSocket = WebSocket(connection);
+
+      expect(webSocket.id, 'websocket:isolate-1:42');
+      expect(webSocket.method, 'WEBSOCKET');
+      expect(webSocket.uri, 'wss://example.com/socket');
+      expect(webSocket.contentType, 'websocket');
+      expect(webSocket.type, 'WebSocket');
+
+      expect(webSocket.startTimestamp, connectTimestamp);
+      expect(webSocket.endTimestamp, isNull);
+      expect(webSocket.duration, isNull);
+
+      expect(webSocket.status, 'open');
+      expect(webSocket.port, isNull);
+
+      expect(webSocket.requestBytes, 100);
+      expect(webSocket.responseBytes, 200);
+
+      expect(webSocket.didFail, false);
+      expect(webSocket.inProgress, true);
+    });
+
+    test('maps WebSocket events correctly', () {
+      final events = [
+        WebSocketEvent(event: 'WebSocket.Connect', timestamp: connectTimestamp),
+        WebSocketEvent(
+          event: 'WebSocket.Send',
+          timestamp: openTimestamp,
+          frameNumber: 1,
+          direction: 'out',
+          opcode: 'text',
+          payloadSize: 25,
+        ),
+        WebSocketEvent(
+          event: 'WebSocket.Receive',
+          timestamp: lastUpdated,
+          frameNumber: 2,
+          direction: 'in',
+          opcode: 'text',
+          payloadSize: 40,
+        ),
+      ];
+
+      final webSocket = WebSocket(createConnection(events: events));
+
+      expect(webSocket.events, hasLength(3));
+
+      expect(webSocket.events[0].event, 'WebSocket.Connect');
+
+      expect(webSocket.events[1].frameNumber, 1);
+      expect(webSocket.events[1].direction, 'out');
+      expect(webSocket.events[1].opcode, 'text');
+      expect(webSocket.events[1].payloadSize, 25);
+
+      expect(webSocket.events[2].frameNumber, 2);
+      expect(webSocket.events[2].direction, 'in');
+      expect(webSocket.events[2].opcode, 'text');
+      expect(webSocket.events[2].payloadSize, 40);
+    });
+
+    test('maps closed connection lifecycle correctly', () {
+      final closeTimestamp = openTimestamp.add(const Duration(seconds: 5));
+
+      final webSocket = WebSocket(
+        createConnection(
+          state: 'closed',
+          closeTimestamp: closeTimestamp,
+          closeCode: 1000,
+          closeReason: 'Normal closure',
+        ),
+      );
+
+      expect(webSocket.state, 'closed');
+      expect(webSocket.inProgress, false);
+      expect(webSocket.didFail, false);
+
+      expect(webSocket.endTimestamp, closeTimestamp);
+      expect(webSocket.duration, closeTimestamp.difference(connectTimestamp));
+
+      expect(webSocket.closeCode, 1000);
+      expect(webSocket.closeReason, 'Normal closure');
+    });
+
+    test('maps error connection correctly', () {
+      final webSocket = WebSocket(
+        createConnection(state: 'error', error: 'Connection failed'),
+      );
+
+      expect(webSocket.state, 'error');
+      expect(webSocket.error, 'Connection failed');
+      expect(webSocket.didFail, true);
+      expect(webSocket.inProgress, false);
+    });
+
+    test('equality and hashCode use stable connection identity', () {
+      final first = WebSocket(createConnection());
+
+      final second = WebSocket(createConnection(bytesSent: 500));
+
+      final different = WebSocket(createConnection(id: '43'));
+
+      expect(first == second, true);
+      expect(first.hashCode, second.hashCode);
+      expect(first == different, false);
+    });
+
+    test('toJson serializes connection and events', () {
+      final events = [
+        WebSocketEvent(
+          event: 'WebSocket.Send',
+          timestamp: openTimestamp,
+          frameNumber: 1,
+          direction: 'out',
+          opcode: 'text',
+          payloadSize: 25,
+        ),
+      ];
+
+      final webSocket = WebSocket(createConnection(events: events));
+
+      final json = webSocket.toJson();
+
+      expect(json['connectionId'], '42');
+      expect(json['uri'], 'wss://example.com/socket');
+      expect(json['protocol'], 'chat');
+      expect(json['state'], 'open');
+
+      expect(json['bytesSent'], 100);
+      expect(json['bytesReceived'], 200);
+      expect(json['framesSent'], 10);
+      expect(json['framesReceived'], 20);
+      expect(json['pingCount'], 3);
+      expect(json['pongCount'], 3);
+
+      expect(json['events'], hasLength(1));
+
+      final event = (json['events'] as List).first as Map<String, Object?>;
+
+      expect(event['event'], 'WebSocket.Send');
+      expect(event['frameNumber'], 1);
+      expect(event['direction'], 'out');
+      expect(event['opcode'], 'text');
+      expect(event['payloadSize'], 25);
+    });
+  });
+
   group('DartIOHttpRequestData', () {
     NetworkController controller;
     FakeServiceConnectionManager fakeServiceConnection;
diff --git a/packages/devtools_app/test/screens/network/network_profiler_test.dart b/packages/devtools_app/test/screens/network/network_profiler_test.dart
index 053e745..d955650 100644
--- a/packages/devtools_app/test/screens/network/network_profiler_test.dart
+++ b/packages/devtools_app/test/screens/network/network_profiler_test.dart
@@ -389,6 +389,14 @@
   });
 
   group('NetworkRequestOverviewView', () {
+    setUp(() {
+      setGlobal(
+        ServiceConnectionManager,
+        FakeServiceConnectionManager(
+          service: FakeServiceManager.createFakeService(),
+        ),
+      );
+    });
     Future<void> pumpView(WidgetTester tester, NetworkRequest data) async {
       final widget = wrap(NetworkRequestOverviewView(data));
       await tester.pumpWidget(widget);
@@ -558,7 +566,7 @@
         expect(find.text('Start time: '), findsOneWidget);
         expect(find.text(formatDateTime(data.startTimestamp)), findsOneWidget);
         expect(find.text('End time: '), findsOneWidget);
-        expect(find.text('Pending'), findsOneWidget);
+        expect(find.text('Pending'), findsNWidgets(2));
         expect(
           find.byKey(NetworkRequestOverviewView.socketTimingGraphKey),
           findsOneWidget,
@@ -575,5 +583,176 @@
         );
       },
     );
+    testWidgetsWithWindowSize('displays for web socket request', windowSize, (
+      tester,
+    ) async {
+      final connection = WebSocketConnection(
+        isolateId: 'isolate-1',
+        id: '42',
+        uri: Uri.parse('wss://example.com/socket'),
+        state: 'open',
+        protocol: 'chat',
+        connectTimestamp: DateTime.utc(2026, 8, 18, 10),
+        openTimestamp: DateTime.utc(2026, 8, 18, 10, 0, 1),
+        bytesSent: 100,
+        bytesReceived: 200,
+        framesSent: 10,
+        framesReceived: 20,
+        pingCount: 3,
+        pongCount: 3,
+        lastUpdated: DateTime.utc(2026, 8, 18, 10, 0, 2),
+        events: [
+          WebSocketEvent(
+            event: 'WebSocket.Send',
+            timestamp: DateTime.utc(2026, 8, 18, 10, 0, 1),
+            frameNumber: 1,
+            direction: 'out',
+            opcode: 'text',
+            payloadSize: 25,
+          ),
+        ],
+      );
+
+      final data = WebSocket(connection);
+
+      await pumpView(tester, data);
+
+      // Verify general information.
+      expect(find.text('Request uri: '), findsOneWidget);
+      expect(find.text('wss://example.com/socket'), findsOneWidget);
+      expect(find.text('Method: '), findsOneWidget);
+      expect(find.text('WEBSOCKET'), findsOneWidget);
+      expect(find.text('Status: '), findsOneWidget);
+      expect(find.text('open'), findsOneWidget);
+      expect(find.text('Port: '), findsNothing);
+      expect(find.text('Content type: '), findsNothing);
+
+      expect(find.text('Connection id: '), findsOneWidget);
+      expect(find.text('42'), findsOneWidget);
+      expect(find.text('Protocol: '), findsOneWidget);
+      expect(find.text('chat'), findsOneWidget);
+
+      expect(find.text('Bytes sent: '), findsOneWidget);
+      expect(find.text('100 B'), findsOneWidget);
+      expect(find.text('Bytes received: '), findsOneWidget);
+      expect(find.text('200 B'), findsOneWidget);
+
+      expect(find.text('Frames sent: '), findsOneWidget);
+      expect(find.text('10'), findsOneWidget);
+      expect(find.text('Frames received: '), findsOneWidget);
+      expect(find.text('20'), findsOneWidget);
+
+      expect(find.text('Ping count: '), findsOneWidget);
+      expect(find.text('3'), findsNWidgets(2));
+
+      expect(find.text('Pong count: '), findsOneWidget);
+
+      // Verify timing information.
+      expect(find.text('Timing: '), findsOneWidget);
+      expect(find.text('Start time: '), findsOneWidget);
+      expect(find.text(formatDateTime(data.startTimestamp)), findsOneWidget);
+      expect(find.text('End time: '), findsOneWidget);
+      expect(find.text('Pending'), findsNWidgets(2));
+      expect(
+        find.byKey(NetworkRequestOverviewView.webSocketTimingGraphKey),
+        findsOneWidget,
+      );
+    });
+
+    testWidgetsWithWindowSize(
+      'displays duration for closed web socket request',
+      windowSize,
+      (tester) async {
+        final connection = WebSocketConnection(
+          isolateId: 'isolate-1',
+          id: '42',
+          uri: Uri.parse('wss://example.com/socket'),
+          state: 'closed',
+          protocol: 'chat',
+          connectTimestamp: DateTime.utc(2026, 8, 18, 10),
+          openTimestamp: DateTime.utc(2026, 8, 18, 10, 0, 1),
+          closeTimestamp: DateTime.utc(2026, 8, 18, 10, 0, 6),
+          bytesSent: 100,
+          bytesReceived: 200,
+          framesSent: 10,
+          framesReceived: 20,
+          pingCount: 3,
+          pongCount: 3,
+          lastUpdated: DateTime.utc(2026, 8, 18, 10, 0, 7),
+          events: [
+            WebSocketEvent(
+              event: 'WebSocket.Send',
+              timestamp: DateTime.utc(2026, 8, 18, 10, 0, 1),
+              frameNumber: 1,
+              direction: 'out',
+              opcode: 'text',
+              payloadSize: 25,
+            ),
+            WebSocketEvent(
+              event: 'WebSocket.Close',
+              timestamp: DateTime.utc(2026, 8, 18, 10, 0, 6),
+              frameNumber: 2,
+              direction: 'out',
+              opcode: 'close',
+              payloadSize: 0,
+            ),
+          ],
+        );
+
+        final data = WebSocket(connection);
+
+        await pumpView(tester, data);
+
+        expect(find.text('Status: '), findsOneWidget);
+        expect(find.text('closed'), findsOneWidget);
+
+        expect(find.text('Connection id: '), findsOneWidget);
+        expect(find.text('42'), findsOneWidget);
+
+        expect(find.text('Protocol: '), findsOneWidget);
+        expect(find.text('chat'), findsOneWidget);
+
+        expect(find.text('Bytes sent: '), findsOneWidget);
+        expect(find.text('100 B'), findsOneWidget);
+
+        expect(find.text('Bytes received: '), findsOneWidget);
+        expect(find.text('200 B'), findsOneWidget);
+
+        expect(find.text('Frames sent: '), findsOneWidget);
+        expect(find.text('10'), findsOneWidget);
+
+        expect(find.text('Frames received: '), findsOneWidget);
+        expect(find.text('20'), findsOneWidget);
+
+        expect(find.text('Ping count: '), findsOneWidget);
+        expect(find.text('3'), findsNWidgets(2));
+
+        expect(find.text('Pong count: '), findsOneWidget);
+
+        // The connection lasted from 10:00:00 to 10:00:06.
+        expect(data.duration, const Duration(seconds: 6));
+        expect(
+          find.text(durationText(data.duration!, fractionDigits: 0)),
+          findsOneWidget,
+        );
+
+        expect(find.text('Start time: '), findsOneWidget);
+        expect(find.text(formatDateTime(data.startTimestamp)), findsOneWidget);
+
+        expect(find.text('End time: '), findsOneWidget);
+        expect(find.text(formatDateTime(data.endTimestamp!)), findsOneWidget);
+
+        expect(find.text('Last updated: '), findsOneWidget);
+        expect(
+          find.text(formatDateTime(connection.lastUpdated)),
+          findsOneWidget,
+        );
+
+        expect(
+          find.byKey(NetworkRequestOverviewView.webSocketTimingGraphKey),
+          findsOneWidget,
+        );
+      },
+    );
   });
 }
diff --git a/packages/devtools_app/test/screens/network/network_request_inspector_test.dart b/packages/devtools_app/test/screens/network/network_request_inspector_test.dart
index b2dbeca..469ec8e 100644
--- a/packages/devtools_app/test/screens/network/network_request_inspector_test.dart
+++ b/packages/devtools_app/test/screens/network/network_request_inspector_test.dart
@@ -286,5 +286,74 @@
         await tester.pumpAndSettle(const Duration(seconds: 1));
       },
     );
+    testWidgetsWithWindowSize(
+      'WebSocket inspector shows Frames',
+      const Size(1200, 800),
+      (tester) async {
+        final connection = WebSocketConnection(
+          isolateId: 'isolate-1',
+          id: '42',
+          uri: Uri.parse('wss://example.com/socket'),
+          state: 'open',
+          protocol: 'chat',
+          connectTimestamp: DateTime.utc(2026, 8, 18, 10),
+          openTimestamp: DateTime.utc(2026, 8, 18, 10, 0, 1),
+          bytesSent: 100,
+          bytesReceived: 200,
+          framesSent: 10,
+          framesReceived: 20,
+          pingCount: 3,
+          pongCount: 3,
+          lastUpdated: DateTime.utc(2026, 8, 18, 10, 0, 2),
+          events: [
+            WebSocketEvent(
+              event: 'WebSocket.Send',
+              timestamp: DateTime.utc(2026, 8, 18, 10, 0, 1),
+              frameNumber: 1,
+              direction: 'out',
+              opcode: 'text',
+              payloadSize: 25,
+            ),
+            WebSocketEvent(
+              event: 'WebSocket.Receive',
+              timestamp: DateTime.utc(2026, 8, 18, 10, 0, 2),
+              frameNumber: 2,
+              direction: 'in',
+              opcode: 'text',
+              payloadSize: 40,
+            ),
+          ],
+        );
+
+        final webSocket = WebSocket(connection);
+
+        await tester.pumpWidget(
+          wrapWithControllers(
+            const NetworkRequestInspector(),
+            network: NetworkController(),
+            debugger: createMockDebuggerControllerWithDefaults(),
+          ),
+        );
+
+        final controller = screenControllers.lookup<NetworkController>();
+        controller.selectedRequest.value = webSocket;
+
+        await tester.pumpAndSettle();
+
+        // The Frames tab should exist for a WebSocket.
+        expect(find.text('Frames'), findsOneWidget);
+
+        // Switch to the Frames tab.
+        await tester.tap(find.text('Frames'));
+        await tester.pumpAndSettle();
+
+        // The Frames view should now be visible.
+        expect(find.byType(WebSocketFramesView), findsWidgets);
+
+        // Stop polling before disposing the test.
+        await controller.stopRecording();
+        await tester.pumpAndSettle(const Duration(seconds: 1));
+      },
+    );
   });
 }
diff --git a/packages/devtools_app/test/screens/network/network_table_test.dart b/packages/devtools_app/test/screens/network/network_table_test.dart
index ebc778d..8d1e51a 100644
--- a/packages/devtools_app/test/screens/network/network_table_test.dart
+++ b/packages/devtools_app/test/screens/network/network_table_test.dart
@@ -45,6 +45,7 @@
       controller.processNetworkTrafficHelper(
         socketProfile.sockets,
         httpProfile.requests,
+        const [],
         0,
         currentRequests: currentRequests,
       );
@@ -105,5 +106,55 @@
       // timezone the test is running in (e.g. UTC vs IST).
       expect(column.getDisplayValue(getRequest), contains('26.279'));
     });
+    test('WebSocket columns', () {
+      final connection = WebSocketConnection(
+        isolateId: 'isolate-1',
+        id: '42',
+        uri: Uri.parse('wss://example.com/socket'),
+        state: 'open',
+        protocol: 'chat',
+        connectTimestamp: DateTime.utc(2026, 8, 18, 10),
+        openTimestamp: DateTime.utc(2026, 8, 18, 10, 0, 1),
+        bytesSent: 100,
+        bytesReceived: 200,
+        framesSent: 10,
+        framesReceived: 20,
+        pingCount: 3,
+        pongCount: 3,
+        lastUpdated: DateTime.utc(2026, 8, 18, 10, 0, 2),
+        events: const [],
+      );
+
+      final webSocket = WebSocket(connection);
+
+      expect(
+        NetworkRequestsTable.addressColumn.getDisplayValue(webSocket),
+        connection.uri.toString(),
+      );
+      expect(
+        NetworkRequestsTable.methodColumn.getDisplayValue(webSocket),
+        'WEBSOCKET',
+      );
+      expect(
+        NetworkRequestsTable.statusColumn.getDisplayValue(webSocket),
+        'open',
+      );
+      expect(
+        NetworkRequestsTable.typeColumn.getDisplayValue(webSocket),
+        'WebSocket',
+      );
+      expect(
+        NetworkRequestsTable.durationColumn.getDisplayValue(webSocket),
+        'Pending',
+      );
+      expect(
+        NetworkRequestsTable.timestampColumn.getDisplayValue(webSocket),
+        contains('00.000'),
+      );
+      expect(
+        NetworkRequestsTable.responseSizeColumn.getDisplayValue(webSocket),
+        '200 B',
+      );
+    });
   });
 }
diff --git a/packages/devtools_app/test/screens/network/offline_data_test.dart b/packages/devtools_app/test/screens/network/offline_data_test.dart
index 8069682..6a653d3 100644
--- a/packages/devtools_app/test/screens/network/offline_data_test.dart
+++ b/packages/devtools_app/test/screens/network/offline_data_test.dart
@@ -131,6 +131,37 @@
 
       // Validate selectedRequestId
       expect(offlineData.selectedRequestId, isNull);
+
+      // Validate webSocketData.
+      expect(offlineData.webSocketData, hasLength(1));
+
+      final webSocket = offlineData.webSocketData.single;
+
+      expect(webSocket.connectionId, '42');
+      expect(webSocket.uri.toString(), 'wss://example.com/socket');
+      expect(webSocket.state, 'open');
+      expect(webSocket.protocol, 'chat');
+
+      expect(webSocket.bytesSent, 100);
+      expect(webSocket.bytesReceived, 200);
+      expect(webSocket.framesSent, 10);
+      expect(webSocket.framesReceived, 20);
+      expect(webSocket.pingCount, 3);
+      expect(webSocket.pongCount, 3);
+
+      expect(webSocket.events, hasLength(2));
+
+      expect(webSocket.events[0].frameNumber, 1);
+      expect(webSocket.events[0].event, 'WebSocket.Send');
+      expect(webSocket.events[0].direction, 'out');
+      expect(webSocket.events[0].opcode, 'text');
+      expect(webSocket.events[0].payloadSize, 25);
+
+      expect(webSocket.events[1].frameNumber, 2);
+      expect(webSocket.events[1].event, 'WebSocket.Receive');
+      expect(webSocket.events[1].direction, 'in');
+      expect(webSocket.events[1].opcode, 'text');
+      expect(webSocket.events[1].payloadSize, 40);
     });
 
     test('OfflineNetworkData should serialize correctly', () {
@@ -150,6 +181,7 @@
         final emptyOfflineData = OfflineNetworkData(
           httpRequestData: [],
           socketData: [],
+          webSocketData: [],
           timelineMicrosOffset: 1731482170837171,
         );
 
@@ -161,6 +193,7 @@
       final populatedHttpData = OfflineNetworkData(
         httpRequestData: offlineData.httpRequestData,
         socketData: [],
+        webSocketData: [],
         timelineMicrosOffset: 1731482170837171,
       );
 
@@ -177,6 +210,42 @@
       );
       expect(restoredData.socketData.length, offlineData.socketData.length);
       expect(restoredData.selectedRequestId, offlineData.selectedRequestId);
+
+      expect(
+        restoredData.webSocketData.length,
+        offlineData.webSocketData.length,
+      );
+
+      final restoredWebSocket = restoredData.webSocketData.single;
+      final originalWebSocket = offlineData.webSocketData.single;
+
+      expect(restoredWebSocket.connectionId, originalWebSocket.connectionId);
+      expect(restoredWebSocket.uri, originalWebSocket.uri);
+      expect(restoredWebSocket.state, originalWebSocket.state);
+      expect(restoredWebSocket.protocol, originalWebSocket.protocol);
+
+      expect(restoredWebSocket.bytesSent, originalWebSocket.bytesSent);
+      expect(restoredWebSocket.bytesReceived, originalWebSocket.bytesReceived);
+      expect(restoredWebSocket.framesSent, originalWebSocket.framesSent);
+      expect(
+        restoredWebSocket.framesReceived,
+        originalWebSocket.framesReceived,
+      );
+      expect(restoredWebSocket.pingCount, originalWebSocket.pingCount);
+      expect(restoredWebSocket.pongCount, originalWebSocket.pongCount);
+
+      expect(restoredWebSocket.events, hasLength(2));
+
+      for (var i = 0; i < originalWebSocket.events.length; i++) {
+        final originalEvent = originalWebSocket.events[i];
+        final restoredEvent = restoredWebSocket.events[i];
+
+        expect(restoredEvent.frameNumber, originalEvent.frameNumber);
+        expect(restoredEvent.event, originalEvent.event);
+        expect(restoredEvent.direction, originalEvent.direction);
+        expect(restoredEvent.opcode, originalEvent.opcode);
+        expect(restoredEvent.payloadSize, originalEvent.payloadSize);
+      }
     });
 
     test('Handles serialization errors gracefully', () {
diff --git a/packages/devtools_app/test/screens/network/sample_network_offline_data.json b/packages/devtools_app/test/screens/network/sample_network_offline_data.json
index 4345951..204ef22 100644
--- a/packages/devtools_app/test/screens/network/sample_network_offline_data.json
+++ b/packages/devtools_app/test/screens/network/sample_network_offline_data.json
@@ -297,5 +297,45 @@
         "writeBytes": 18247
       }
     }
+  ],
+  "webSocketData": [
+    {
+      "isolateId": "isolate-1",
+      "connectionId": "42",
+      "uri": "wss://example.com/socket",
+      "state": "open",
+      "protocol": "chat",
+      "connectTimestamp": "2026-08-18T10:00:00.000Z",
+      "openTimestamp": "2026-08-18T10:00:01.000Z",
+      "closeTimestamp": null,
+      "bytesSent": 100,
+      "bytesReceived": 200,
+      "framesSent": 10,
+      "framesReceived": 20,
+      "pingCount": 3,
+      "pongCount": 3,
+      "closeCode": null,
+      "closeReason": null,
+      "error": null,
+      "lastUpdated": "2026-08-18T10:00:02.000Z",
+      "events": [
+        {
+          "event": "WebSocket.Send",
+          "timestamp": 1787047201000000,
+          "frameNumber": 1,
+          "direction": "out",
+          "opcode": "text",
+          "payloadSize": 25
+        },
+        {
+          "event": "WebSocket.Receive",
+          "timestamp": 1787047202000000,
+          "frameNumber": 2,
+          "direction": "in",
+          "opcode": "text",
+          "payloadSize": 40
+        }
+      ]
+    }
   ]
 }
\ No newline at end of file