Skip to content

Commit e3cd267

Browse files
Fix Network tab not capturing requests after hot restart and clear.
Re-enable HTTP logging and socket profiling when new isolates are spawned, reset clear timestamps using the VM timeline, and keep the search field enabled while recording. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent dc611a9 commit e3cd267

13 files changed

Lines changed: 1067 additions & 8 deletions

File tree

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,15 @@ class NetworkController extends DevToolsScreenController
192192
_currentNetworkRequests,
193193
_filterAndRefreshSearchMatches,
194194
);
195+
autoDisposeStreamSubscription(
196+
serviceConnection.serviceManager.isolateManager.onIsolateCreated.listen((
197+
_,
198+
) async {
199+
if (_recordingNotifier.value) {
200+
await _enableNetworkTrafficRecordingOnAllIsolates();
201+
}
202+
}),
203+
);
195204
}
196205

197206
@override
@@ -346,13 +355,16 @@ class NetworkController extends DevToolsScreenController
346355
]),
347356
);
348357

349-
// TODO(kenz): only call these if http logging and socket profiling are not
350-
// already enabled. Listen to service manager streams for this info.
358+
await _enableNetworkTrafficRecordingOnAllIsolates();
359+
await togglePolling(true);
360+
}
361+
362+
/// Enables HTTP timeline logging and socket profiling on all isolates.
363+
Future<void> _enableNetworkTrafficRecordingOnAllIsolates() async {
351364
await [
352365
http_service.toggleHttpRequestLogging(true),
353366
networkService.toggleSocketProfiling(true),
354367
].wait;
355-
await togglePolling(true);
356368
}
357369

358370
Future<void> stopRecording() async {

packages/devtools_app/lib/src/screens/network/network_screen.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,6 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls>
205205
}
206206

207207
final screenWidth = ScreenSize(context).width;
208-
final hasRequests = controller.filteredData.value.isNotEmpty;
209208
return Column(
210209
children: [
211210
Row(
@@ -233,7 +232,7 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls>
233232
Expanded(
234233
child: SearchField<NetworkController>(
235234
searchController: controller,
236-
searchFieldEnabled: hasRequests,
235+
searchFieldEnabled: _recording,
237236
searchFieldWidth: screenWidth <= MediaSize.xs
238237
? defaultSearchFieldWidth
239238
: wideSearchFieldWidth,

packages/devtools_app/lib/src/screens/network/network_service.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,14 @@ class NetworkService {
202202
}
203203

204204
Future<void> clearData() async {
205-
await updateLastSocketDataRefreshTime();
206-
updateLastHttpDataRefreshTime();
205+
final service = serviceConnection.serviceManager.service;
206+
if (service == null) return;
207+
208+
final timestamp = (await service.getVMTimelineMicros()).timestamp!;
209+
networkController.lastSocketDataRefreshMicros = timestamp;
210+
await service.forEachIsolate((isolate) async {
211+
lastHttpDataRefreshTimePerIsolate[isolate.id!] = timestamp;
212+
});
207213
await _clearSocketProfile();
208214
await _clearHttpProfile();
209215
}

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ TODO: Remove this section if there are not any updates.
4242

4343
## Network profiler updates
4444

45-
TODO: Remove this section if there are not any updates.
45+
* Fixed an issue where the Network tab would stop capturing HTTP requests after
46+
a hot restart. -
47+
[#9783](https://github.com/flutter/devtools/issues/9783)
48+
* Fixed an issue where the Network tab would stop capturing new HTTP requests
49+
after pressing Clear while recording.
50+
* Fixed an issue where the Network tab search field was disabled after pressing
51+
Clear while recording.
4652

4753
## Logging updates
4854

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
// Copyright 2025 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
@TestOn('vm')
6+
library;
7+
8+
import 'package:devtools_app/devtools_app.dart';
9+
import 'package:devtools_test/devtools_test.dart';
10+
import 'package:flutter_test/flutter_test.dart';
11+
12+
import 'utils/hot_restart_network_vm_service.dart';
13+
import 'utils/network_lifecycle_test_utils.dart';
14+
import 'utils/network_test_utils.dart';
15+
16+
void main() {
17+
group('Network View clear button', () {
18+
late HotRestartNetworkVmService vmService;
19+
late FakeServiceConnectionManager fakeServiceConnection;
20+
21+
setUp(() {
22+
vmService = HotRestartNetworkVmService();
23+
fakeServiceConnection = FakeServiceConnectionManager(service: vmService);
24+
});
25+
26+
tearDown(disposeNetworkLifecycleControllers);
27+
28+
group('request visibility after clear', () {
29+
test('displays new HTTP requests after clear while recording', () async {
30+
final isolateId = vmService.currentIsolateId;
31+
final controller = await initNetworkLifecycleController(
32+
vmService: vmService,
33+
fakeServiceConnection: fakeServiceConnection,
34+
initialProfile: [
35+
createTestHttpRequest(id: 'before-clear', method: 'GET'),
36+
],
37+
);
38+
await controller.networkService.refreshNetworkData();
39+
expect(controller.requests.value, hasLength(1));
40+
41+
await controller.clear();
42+
expect(controller.requests.value, isEmpty);
43+
44+
vmService.appendHttpRequest(
45+
isolateId,
46+
createTestHttpRequest(id: 'after-clear', method: 'POST'),
47+
);
48+
await controller.networkService.refreshNetworkData();
49+
50+
expect(
51+
controller.requests.value.whereType<DartIOHttpRequestData>().map(
52+
(request) => request.method,
53+
),
54+
contains('POST'),
55+
reason:
56+
'Network View should display new requests after Clear while '
57+
'recording is active.',
58+
);
59+
});
60+
61+
test('polling remains active after clear', () async {
62+
final controller = await initNetworkLifecycleController(
63+
vmService: vmService,
64+
fakeServiceConnection: fakeServiceConnection,
65+
initialProfile: [
66+
createTestHttpRequest(id: 'polling-test', method: 'GET'),
67+
],
68+
);
69+
await controller.networkService.refreshNetworkData();
70+
71+
await controller.clear();
72+
73+
expect(controller.isPolling, isTrue);
74+
expect(controller.recordingNotifier.value, isTrue);
75+
});
76+
77+
test('keeps HTTP logging enabled after clear', () async {
78+
final isolateId = vmService.currentIsolateId;
79+
final controller = await initNetworkLifecycleController(
80+
vmService: vmService,
81+
fakeServiceConnection: fakeServiceConnection,
82+
initialProfile: [
83+
createTestHttpRequest(id: 'logging-test', method: 'GET'),
84+
],
85+
);
86+
await controller.networkService.refreshNetworkData();
87+
88+
await controller.clear();
89+
90+
expect(vmService.isHttpLoggingEnabled(isolateId), isTrue);
91+
});
92+
93+
test('keeps socket profiling enabled after clear', () async {
94+
final isolateId = vmService.currentIsolateId;
95+
final controller = await initNetworkLifecycleController(
96+
vmService: vmService,
97+
fakeServiceConnection: fakeServiceConnection,
98+
initialProfile: [
99+
createTestHttpRequest(id: 'socket-test', method: 'GET'),
100+
],
101+
);
102+
await controller.networkService.refreshNetworkData();
103+
104+
await controller.clear();
105+
106+
expect(vmService.isSocketProfilingEnabled(isolateId), isTrue);
107+
});
108+
});
109+
110+
group('clear refresh timestamp tracking', () {
111+
test(
112+
'resets HTTP refresh timestamps to the VM timeline on clear',
113+
() async {
114+
final isolateId = vmService.currentIsolateId;
115+
final controller = await initNetworkLifecycleController(
116+
vmService: vmService,
117+
fakeServiceConnection: fakeServiceConnection,
118+
);
119+
await controller.networkService.refreshNetworkData();
120+
121+
controller
122+
.networkService
123+
.lastHttpDataRefreshTimePerIsolate[isolateId] =
124+
DateTime.now().microsecondsSinceEpoch;
125+
126+
await controller.clear();
127+
128+
final timelineMicros =
129+
(await vmService.getVMTimelineMicros()).timestamp!;
130+
expect(
131+
controller
132+
.networkService
133+
.lastHttpDataRefreshTimePerIsolate[isolateId],
134+
timelineMicros,
135+
);
136+
},
137+
);
138+
139+
test('does not show stale requests after clear', () async {
140+
final controller = await initNetworkLifecycleController(
141+
vmService: vmService,
142+
fakeServiceConnection: fakeServiceConnection,
143+
initialProfile: [
144+
createTestHttpRequest(
145+
id: 'stale-request',
146+
method: 'GET',
147+
startTime: 1500000,
148+
),
149+
],
150+
);
151+
await controller.networkService.refreshNetworkData();
152+
expect(controller.requests.value, hasLength(1));
153+
154+
await controller.clear();
155+
await controller.networkService.refreshNetworkData();
156+
157+
expect(controller.requests.value, isEmpty);
158+
});
159+
});
160+
161+
group('clear combined with hot restart', () {
162+
test('clear then hot restart then new requests', () async {
163+
final controller = await initNetworkLifecycleController(
164+
vmService: vmService,
165+
fakeServiceConnection: fakeServiceConnection,
166+
initialProfile: [
167+
createTestHttpRequest(id: 'pre-clear', method: 'GET'),
168+
],
169+
);
170+
await controller.networkService.refreshNetworkData();
171+
await controller.clear();
172+
173+
final postRestartIsolateId = vmService.simulateHotRestart();
174+
notifyMainIsolateChanged(fakeServiceConnection, postRestartIsolateId);
175+
await pumpEventQueue();
176+
177+
vmService.appendHttpRequest(
178+
postRestartIsolateId,
179+
createTestHttpRequest(
180+
id: 'after-clear-and-restart',
181+
method: 'PUT',
182+
startTime: 9000000,
183+
),
184+
);
185+
await controller.networkService.refreshNetworkData();
186+
187+
expect(
188+
controller.requests.value.whereType<DartIOHttpRequestData>().map(
189+
(request) => request.method,
190+
),
191+
contains('PUT'),
192+
);
193+
expect(vmService.isHttpLoggingEnabled(postRestartIsolateId), isTrue);
194+
});
195+
196+
test('hot restart then clear then new requests', () async {
197+
final controller = await initNetworkLifecycleController(
198+
vmService: vmService,
199+
fakeServiceConnection: fakeServiceConnection,
200+
initialProfile: [
201+
createTestHttpRequest(id: 'pre-restart', method: 'GET'),
202+
],
203+
);
204+
await controller.networkService.refreshNetworkData();
205+
206+
final postRestartIsolateId = vmService.simulateHotRestart();
207+
notifyMainIsolateChanged(fakeServiceConnection, postRestartIsolateId);
208+
await pumpEventQueue();
209+
vmService.appendHttpRequest(
210+
postRestartIsolateId,
211+
createTestHttpRequest(
212+
id: 'after-restart',
213+
method: 'POST',
214+
startTime: 8000000,
215+
),
216+
);
217+
await controller.networkService.refreshNetworkData();
218+
expect(controller.requests.value, isNotEmpty);
219+
220+
await controller.clear();
221+
expect(controller.requests.value, isEmpty);
222+
223+
vmService.appendHttpRequest(
224+
postRestartIsolateId,
225+
createTestHttpRequest(
226+
id: 'after-restart-and-clear',
227+
method: 'DELETE',
228+
startTime: 10000000,
229+
),
230+
);
231+
await controller.networkService.refreshNetworkData();
232+
233+
expect(
234+
controller.requests.value.whereType<DartIOHttpRequestData>().map(
235+
(request) => request.method,
236+
),
237+
contains('DELETE'),
238+
);
239+
});
240+
});
241+
242+
group('state after clear', () {
243+
test('clears selected request', () async {
244+
final controller = await initNetworkLifecycleController(
245+
vmService: vmService,
246+
fakeServiceConnection: fakeServiceConnection,
247+
initialProfile: [
248+
createTestHttpRequest(id: 'selected', method: 'GET'),
249+
],
250+
);
251+
await controller.networkService.refreshNetworkData();
252+
controller.selectedRequest.value = controller.requests.value.first;
253+
254+
await controller.clear();
255+
256+
expect(controller.selectedRequest.value, isNull);
257+
});
258+
259+
test('preserves active search text', () async {
260+
final controller = await initNetworkLifecycleController(
261+
vmService: vmService,
262+
fakeServiceConnection: fakeServiceConnection,
263+
initialProfile: [
264+
createTestHttpRequest(
265+
id: 'searchable',
266+
method: 'GET',
267+
uri: 'https://example.com/api',
268+
),
269+
],
270+
);
271+
await controller.networkService.refreshNetworkData();
272+
controller.search = 'example';
273+
274+
await controller.clear();
275+
276+
expect(controller.search, 'example');
277+
});
278+
279+
test('supports multiple consecutive clears', () async {
280+
final isolateId = vmService.currentIsolateId;
281+
final controller = await initNetworkLifecycleController(
282+
vmService: vmService,
283+
fakeServiceConnection: fakeServiceConnection,
284+
initialProfile: [
285+
createTestHttpRequest(id: 'multi-clear-1', method: 'GET'),
286+
],
287+
);
288+
await controller.networkService.refreshNetworkData();
289+
290+
await controller.clear();
291+
await controller.clear();
292+
293+
vmService.appendHttpRequest(
294+
isolateId,
295+
createTestHttpRequest(
296+
id: 'after-multi-clear',
297+
method: 'PATCH',
298+
startTime: 3000000,
299+
),
300+
);
301+
await controller.networkService.refreshNetworkData();
302+
303+
expect(
304+
controller.requests.value.whereType<DartIOHttpRequestData>().map(
305+
(request) => request.method,
306+
),
307+
contains('PATCH'),
308+
);
309+
});
310+
});
311+
});
312+
}

0 commit comments

Comments
 (0)