Added more analytics (#1420)

* Added more analytics

Helps: https://github.com/dart-lang/webdev/issues/1419

* Update HTTP_REQUEST_EXCEPTION event to match other events

* Addressed CR comments, added event on fullReload as well
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index 9447f17..f960e1b 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -6,7 +6,11 @@
 - Use default constant port for debug service.
   - If we fail binding to the port, fall back to previous strategy
     of finding unbound ports.
-- Add metrics measuring DevTools Initial Page Load time.
+- Add metrics measuring
+  - DevTools Initial Page Load time
+  - Various VM API
+  - Hot restart
+  - Http request handling exceptions
 - Add `ext.dwds.sendEvent` service extension to dwds so other tools
   can send events to the debugger.
   Event format:
diff --git a/dwds/lib/dwds.dart b/dwds/lib/dwds.dart
index 0a0c3fd..f3c3d58 100644
--- a/dwds/lib/dwds.dart
+++ b/dwds/lib/dwds.dart
@@ -86,7 +86,9 @@
 
   Future<DebugConnection> debugConnection(AppConnection appConnection) async {
     if (!_enableDebugging) throw StateError('Debugging is not enabled.');
-    var appDebugServices = await _devHandler.loadAppServices(appConnection);
+    final dwdsStats = DwdsStats(DateTime.now());
+    var appDebugServices =
+        await _devHandler.loadAppServices(appConnection, dwdsStats);
     await appDebugServices.chromeProxyService.isInitialized;
     return DebugConnection(appDebugServices);
   }
diff --git a/dwds/lib/src/dwds_vm_client.dart b/dwds/lib/src/dwds_vm_client.dart
index 1c82609..8aaa165 100644
--- a/dwds/lib/src/dwds_vm_client.dart
+++ b/dwds/lib/src/dwds_vm_client.dart
@@ -80,71 +80,19 @@
         }
       };
     });
-    await client.registerService('_flutter.listViews', 'DWDS listViews');
+    await client.registerService('_flutter.listViews', 'DWDS');
 
-    client.registerServiceCallback('hotRestart', (request) async {
-      _logger.info('Attempting a hot restart');
+    client.registerServiceCallback(
+        'hotRestart',
+        (request) => captureElapsedTime(
+            () => _hotRestart(chromeProxyService, client),
+            (_) => DwdsEvent.hotRestart()));
+    await client.registerService('hotRestart', 'DWDS');
 
-      chromeProxyService.terminatingIsolates = true;
-      await _disableBreakpointsAndResume(client, chromeProxyService);
-      int context;
-      try {
-        _logger.info('Attempting to get execution context ID.');
-        context = await chromeProxyService.executionContext.id;
-        _logger.info('Got execution context ID.');
-      } on StateError catch (e) {
-        // We couldn't find the execution context. `hotRestart` may have been
-        // triggered in the middle of a full reload.
-        return {
-          'error': {
-            'code': RPCError.kInternalError,
-            'message': e.message,
-          }
-        };
-      }
-      // Start listening for isolate create events before issuing a hot
-      // restart. Only return success after the isolate has fully started.
-      var stream = chromeProxyService.onEvent('Isolate');
-      try {
-        _logger.info('Issuing \$dartHotRestart request.');
-        await chromeProxyService.remoteDebugger
-            .sendCommand('Runtime.evaluate', params: {
-          'expression': r'$dartHotRestart();',
-          'awaitPromise': true,
-          'contextId': context,
-        });
-        _logger.info('\$dartHotRestart request complete.');
-      } on WipError catch (exception) {
-        var code = exception.error['code'];
-        // This corresponds to `Execution context was destroyed` which can
-        // occur during a hot restart that must fall back to a full reload.
-        if (code != RPCError.kServerError) {
-          return {
-            'error': {
-              'code': exception.error['code'],
-              'message': exception.error['message'],
-              'data': exception,
-            }
-          };
-        }
-      }
-
-      _logger.info('Waiting for Isolate Start event.');
-      await stream.firstWhere((event) => event.kind == EventKind.kIsolateStart);
-      chromeProxyService.terminatingIsolates = false;
-
-      _logger.info('Successful hot restart');
-      return {'result': Success().toJson()};
-    });
-    await client.registerService('hotRestart', 'DWDS fullReload');
-
-    client.registerServiceCallback('fullReload', (_) async {
-      _logger.info('Attempting a full reload');
-      await chromeProxyService.remoteDebugger.enablePage();
-      await chromeProxyService.remoteDebugger.pageReload();
-      _logger.info('Successful full reload');
-      return {'result': Success().toJson()};
-    });
+    client.registerServiceCallback(
+        'fullReload',
+        (request) => captureElapsedTime(() => _fullReload(chromeProxyService),
+            (_) => DwdsEvent.fullReload()));
     await client.registerService('fullReload', 'DWDS');
 
     client.registerServiceCallback('ext.dwds.screenshot', (_) async {
@@ -205,6 +153,9 @@
         var action = payload == null ? null : payload['action'];
         if (screen == 'debugger' && action == 'pageReady') {
           if (dwdsStats.isFirstDebuggerReady()) {
+            emitEvent(DwdsEvent.devToolsLoad(DateTime.now()
+                .difference(dwdsStats.devToolsStart)
+                .inMilliseconds));
             emitEvent(DwdsEvent.debuggerReady(DateTime.now()
                 .difference(dwdsStats.debuggerStart)
                 .inMilliseconds));
@@ -218,6 +169,71 @@
   }
 }
 
+Future<Map<String, dynamic>> _hotRestart(
+    ChromeProxyService chromeProxyService, VmService client) async {
+  _logger.info('Attempting a hot restart');
+
+  chromeProxyService.terminatingIsolates = true;
+  await _disableBreakpointsAndResume(client, chromeProxyService);
+  int context;
+  try {
+    _logger.info('Attempting to get execution context ID.');
+    context = await chromeProxyService.executionContext.id;
+    _logger.info('Got execution context ID.');
+  } on StateError catch (e) {
+    // We couldn't find the execution context. `hotRestart` may have been
+    // triggered in the middle of a full reload.
+    return {
+      'error': {
+        'code': RPCError.kInternalError,
+        'message': e.message,
+      }
+    };
+  }
+  // Start listening for isolate create events before issuing a hot
+  // restart. Only return success after the isolate has fully started.
+  var stream = chromeProxyService.onEvent('Isolate');
+  try {
+    _logger.info('Issuing \$dartHotRestart request.');
+    await chromeProxyService.remoteDebugger
+        .sendCommand('Runtime.evaluate', params: {
+      'expression': r'$dartHotRestart();',
+      'awaitPromise': true,
+      'contextId': context,
+    });
+    _logger.info('\$dartHotRestart request complete.');
+  } on WipError catch (exception) {
+    var code = exception.error['code'];
+    // This corresponds to `Execution context was destroyed` which can
+    // occur during a hot restart that must fall back to a full reload.
+    if (code != RPCError.kServerError) {
+      return {
+        'error': {
+          'code': exception.error['code'],
+          'message': exception.error['message'],
+          'data': exception,
+        }
+      };
+    }
+  }
+
+  _logger.info('Waiting for Isolate Start event.');
+  await stream.firstWhere((event) => event.kind == EventKind.kIsolateStart);
+  chromeProxyService.terminatingIsolates = false;
+
+  _logger.info('Successful hot restart');
+  return {'result': Success().toJson()};
+}
+
+Future<Map<String, dynamic>> _fullReload(
+    ChromeProxyService chromeProxyService) async {
+  _logger.info('Attempting a full reload');
+  await chromeProxyService.remoteDebugger.enablePage();
+  await chromeProxyService.remoteDebugger.pageReload();
+  _logger.info('Successful full reload');
+  return {'result': Success().toJson()};
+}
+
 Future<void> _disableBreakpointsAndResume(
     VmService client, ChromeProxyService chromeProxyService) async {
   _logger.info('Attempting to disable breakpoints and resume the isolate');
diff --git a/dwds/lib/src/events.dart b/dwds/lib/src/events.dart
index 36e5ad6..f7bb86d 100644
--- a/dwds/lib/src/events.dart
+++ b/dwds/lib/src/events.dart
@@ -12,6 +12,9 @@
   /// The time when the user starts the debugger.
   final DateTime debuggerStart;
 
+  /// The time when dwds launches DevTools.
+  DateTime devToolsStart;
+
   var _isDebuggerReady = false;
 
   /// Records and returns whether the debugger became ready.
@@ -28,13 +31,17 @@
   static const String compilerUpdateDependencies =
       'COMPILER_UPDATE_DEPENDENCIES';
   static const String devtoolsLaunch = 'DEVTOOLS_LAUNCH';
+  static const String devToolsLoad = 'DEVTOOLS_LOAD';
+  static const String debuggerReady = 'DEBUGGER_READY';
   static const String evaluate = 'EVALUATE';
   static const String evaluateInFrame = 'EVALUATE_IN_FRAME';
+  static const String fullReload = 'FULL_RELOAD';
   static const String getIsolate = 'GET_ISOLATE';
   static const String getScripts = 'GET_SCRIPTS';
   static const String getSourceReport = 'GET_SOURCE_REPORT';
-  static const String debuggerReady = 'DEBUGGER_READY';
   static const String getVM = 'GET_VM';
+  static const String hotRestart = 'HOT_RESTART';
+  static const String httpRequestException = 'HTTP_REQUEST_EXCEPTION';
   static const String resume = 'RESUME';
 
   DwdsEventKind._();
@@ -77,11 +84,26 @@
 
   DwdsEvent.getSourceReport() : this(DwdsEventKind.getSourceReport, {});
 
+  DwdsEvent.hotRestart() : this(DwdsEventKind.hotRestart, {});
+
+  DwdsEvent.fullReload() : this(DwdsEventKind.fullReload, {});
+
   DwdsEvent.debuggerReady(int elapsedMilliseconds)
       : this(DwdsEventKind.debuggerReady, {
           'elapsedMilliseconds': elapsedMilliseconds,
         });
 
+  DwdsEvent.devToolsLoad(int elapsedMilliseconds)
+      : this(DwdsEventKind.devToolsLoad, {
+          'elapsedMilliseconds': elapsedMilliseconds,
+        });
+
+  DwdsEvent.httpRequestException(String server, String exception)
+      : this(DwdsEventKind.httpRequestException, {
+          'server': server,
+          'exception': exception,
+        });
+
   void addException(dynamic exception) {
     payload['exception'] = exception;
   }
@@ -103,3 +125,24 @@
 
 /// A global stream of [DwdsEvent]s.
 Stream<DwdsEvent> get eventStream => _eventController.stream;
+
+/// Call [function] and record its execution time.
+///
+/// Calls [event] to create the event to be recorded,
+/// and appends time and exception details to it if
+/// available.
+Future<T> captureElapsedTime<T>(
+    Future<T> Function() function, DwdsEvent Function(T result) event) async {
+  var stopwatch = Stopwatch()..start();
+  T result;
+  try {
+    return result = await function();
+  } catch (e) {
+    emitEvent(event(result)
+      ..addException(e)
+      ..addElapsedTime(stopwatch.elapsedMilliseconds));
+    rethrow;
+  } finally {
+    emitEvent(event(result)..addElapsedTime(stopwatch.elapsedMilliseconds));
+  }
+}
diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart
index 39324bf..922bf23 100644
--- a/dwds/lib/src/handlers/dev_handler.dart
+++ b/dwds/lib/src/handlers/dev_handler.dart
@@ -221,8 +221,8 @@
     );
   }
 
-  Future<AppDebugServices> loadAppServices(AppConnection appConnection) async {
-    var dwdsStats = DwdsStats(DateTime.now());
+  Future<AppDebugServices> loadAppServices(
+      AppConnection appConnection, DwdsStats dwdsStats) async {
     var appId = appConnection.request.appId;
     if (_servicesByAppId[appId] == null) {
       var debugService = await _startLocalDebugService(
@@ -321,9 +321,10 @@
       return;
     }
 
+    var dwdsStats = DwdsStats(DateTime.now());
     AppDebugServices appServices;
     try {
-      appServices = await loadAppServices(appConnection);
+      appServices = await loadAppServices(appConnection, dwdsStats);
     } catch (_) {
       var error = 'Unable to connect debug services to your '
           'application. Most likely this means you are trying to '
@@ -364,6 +365,7 @@
           ..promptExtension = false))));
 
     appServices.connectedInstanceId = appConnection.request.instanceId;
+    dwdsStats.devToolsStart = DateTime.now();
     await _launchDevTools(appServices.chromeProxyService.remoteDebugger,
         appServices.debugService.uri);
   }
@@ -511,6 +513,7 @@
         extensionDebugConnections.add(DebugConnection(appServices));
         _servicesByAppId[appId] = appServices;
       }
+      dwdsStats.devToolsStart = DateTime.now();
       await _launchDevTools(extensionDebugger,
           await _servicesByAppId[appId].debugService.encodedUri);
     });
diff --git a/dwds/lib/src/servers/extension_backend.dart b/dwds/lib/src/servers/extension_backend.dart
index b6aa03a..85aba76 100644
--- a/dwds/lib/src/servers/extension_backend.dart
+++ b/dwds/lib/src/servers/extension_backend.dart
@@ -8,12 +8,12 @@
 import 'dart:io';
 
 import 'package:async/async.dart';
-
 import 'package:http_multi_server/http_multi_server.dart';
 import 'package:logging/logging.dart';
 import 'package:shelf/shelf.dart';
 
 import '../../data/extension_request.dart';
+import '../events.dart';
 import '../handlers/socket_connections.dart';
 import '../utilities/shared.dart';
 import 'extension_debugger.dart';
@@ -21,7 +21,7 @@
 const authenticationResponse = 'Dart Debug Authentication Success!\n\n'
     'You can close this tab and launch the Dart Debug Extension again.';
 
-Logger _logger = Logger('ExtensiobBackend');
+Logger _logger = Logger('ExtensionBackend');
 
 /// A backend for the Dart Debug Extension.
 ///
@@ -56,7 +56,8 @@
     }).add(_socketHandler.handler);
     var server = await HttpMultiServer.bind(hostname, 0);
     serveHttpRequests(server, cascade.handler, (e, s) {
-      _logger.warning('Error serving requests', e, s);
+      _logger.warning('Error serving requests', e);
+      emitEvent(DwdsEvent.httpRequestException('ExtensionBackend', '$e:$s'));
     });
     return ExtensionBackend._(
         _socketHandler, server.address.host, server.port, server);
diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart
index 3a71651..7f2231a 100644
--- a/dwds/lib/src/services/chrome_proxy_service.dart
+++ b/dwds/lib/src/services/chrome_proxy_service.dart
@@ -184,7 +184,7 @@
           moduleFormat: moduleFormat, soundNullSafety: soundNullSafety);
       var dependencies =
           await globalLoadStrategy.moduleInfoForEntrypoint(entrypoint);
-      await _captureElapsedTime(() async {
+      await captureElapsedTime(() async {
         var result = await _compiler.updateDependencies(dependencies);
         // Expression evaluation is ready after dependencies are updated.
         if (!_compilerCompleter.isCompleted) _compilerCompleter.complete();
@@ -422,7 +422,7 @@
     bool disableBreakpoints,
   }) async {
     // TODO(798) - respect disableBreakpoints.
-    return _captureElapsedTime(() async {
+    return captureElapsedTime(() async {
       await isInitialized;
       if (_expressionEvaluator != null) {
         await isCompilerInitialized;
@@ -447,7 +447,7 @@
       {Map<String, String> scope, bool disableBreakpoints}) async {
     // TODO(798) - respect disableBreakpoints.
 
-    return _captureElapsedTime(() async {
+    return captureElapsedTime(() async {
       await isInitialized;
       if (_expressionEvaluator != null) {
         await isCompilerInitialized;
@@ -509,7 +509,7 @@
 
   @override
   Future<Isolate> getIsolate(String isolateId) async {
-    return _captureElapsedTime(() async {
+    return captureElapsedTime(() async {
       await isInitialized;
       return _getIsolate(isolateId);
     }, (result) => DwdsEvent.getIsolate());
@@ -531,7 +531,7 @@
 
   @override
   Future<ScriptList> getScripts(String isolateId) async {
-    return await _captureElapsedTime(() async {
+    return await captureElapsedTime(() async {
       await isInitialized;
       return _inspector?.getScripts(isolateId);
     }, (result) => DwdsEvent.getScripts());
@@ -544,7 +544,7 @@
       int endTokenPos,
       bool forceCompile,
       bool reportLines}) async {
-    return await _captureElapsedTime(() async {
+    return await captureElapsedTime(() async {
       await isInitialized;
       return await _inspector?.getSourceReport(isolateId, reports,
           scriptId: scriptId,
@@ -568,7 +568,7 @@
 
   @override
   Future<VM> getVM() async {
-    return _captureElapsedTime(() async {
+    return captureElapsedTime(() async {
       await isInitialized;
       return _vm;
     }, (result) => DwdsEvent.getVM());
@@ -685,7 +685,7 @@
       {String step, int frameIndex}) async {
     if (_inspector == null) throw StateError('No running isolate.');
     if (_inspector.appConnection.isStarted) {
-      return _captureElapsedTime(() async {
+      return captureElapsedTime(() async {
         await isInitialized;
         return await (await _debugger)
             .resume(isolateId, step: step, frameIndex: frameIndex);
@@ -1037,27 +1037,6 @@
   Future<Breakpoint> setBreakpointState(
           String isolateId, String breakpointId, bool enable) =>
       throw UnimplementedError();
-
-  /// Call [function] and record its execution time.
-  ///
-  /// Calls [event] to create the event to be recorded,
-  /// and appends time and exception details to it if
-  /// available.
-  Future<T> _captureElapsedTime<T>(
-      Future<T> Function() function, DwdsEvent Function(T result) event) async {
-    var stopwatch = Stopwatch()..start();
-    T result;
-    try {
-      return result = await function();
-    } catch (e) {
-      emitEvent(event(result)
-        ..addException(e)
-        ..addElapsedTime(stopwatch.elapsedMilliseconds));
-      rethrow;
-    } finally {
-      emitEvent(event(result)..addElapsedTime(stopwatch.elapsedMilliseconds));
-    }
-  }
 }
 
 /// The `type`s of [ConsoleAPIEvent]s that are treated as `stderr` logs.
diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart
index 3b06d6e..9987c5e 100644
--- a/dwds/lib/src/services/debug_service.dart
+++ b/dwds/lib/src/services/debug_service.dart
@@ -23,6 +23,7 @@
 import '../../dwds.dart';
 import '../debugging/execution_context.dart';
 import '../debugging/remote_debugger.dart';
+import '../events.dart';
 import '../utilities/shared.dart';
 import 'chrome_proxy_service.dart';
 
@@ -256,7 +257,8 @@
     }
     var server = await startHttpServer(hostname, port: 44456);
     serveHttpRequests(server, handler, (e, s) {
-      _logger.warning('Error serving requests', e, s);
+      _logger.warning('Error serving requests', e);
+      emitEvent(DwdsEvent.httpRequestException('DebugService', '$e:$s'));
     });
     return DebugService._(
       chromeProxyService,
diff --git a/dwds/test/events_test.dart b/dwds/test/events_test.dart
index 61bcce6..7ad33dd 100644
--- a/dwds/test/events_test.dart
+++ b/dwds/test/events_test.dart
@@ -5,10 +5,13 @@
 // @dart = 2.9
 
 import 'dart:async';
+import 'dart:io';
 
 import 'package:dwds/src/connections/debug_connection.dart';
 import 'package:dwds/src/events.dart';
 import 'package:dwds/src/services/chrome_proxy_service.dart';
+import 'package:dwds/src/utilities/shared.dart';
+import 'package:http_multi_server/http_multi_server.dart';
 import 'package:test/test.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:webdriver/async_core.dart';
@@ -25,323 +28,417 @@
 final context = TestContext();
 
 void main() {
-  Future initialEvents;
-  VmService vmService;
-  Keyboard keyboard;
-  Stream<DwdsEvent> events;
-
-  /// Runs [action] and waits for an event matching [eventMatcher].
-  Future<T> expectEventDuring<T>(
-      Matcher eventMatcher, Future<T> Function() action,
-      {Timeout timeout}) async {
-    // The events stream is a broadcast stream so start listening
-    // before the action.
-    final events = expectLater(
-        pipe(context.testServer.dwds.events, timeout: timeout),
-        emitsThrough(eventMatcher));
-    final result = await action();
-    await events;
-    return result;
-  }
-
-  setUpAll(() async {
-    setCurrentLogWriter();
-    initialEvents = expectLater(
-        pipe(eventStream, timeout: const Timeout.factor(5)),
-        emitsThrough(matchesEvent(DwdsEventKind.compilerUpdateDependencies, {
-          'entrypoint': 'hello_world/main.dart.bootstrap.js',
-          'elapsedMilliseconds': isNotNull
-        })));
-    await context.setUp(
-      serveDevTools: true,
-      enableExpressionEvaluation: true,
-    );
-    vmService = context.debugConnection.vmService;
-    keyboard = context.webDriver.driver.keyboard;
-    events = context.testServer.dwds.events;
-  });
-
-  tearDownAll(() async {
-    await context.tearDown();
-  });
-
-  test('emits DEVTOOLS_LAUNCH event', () async {
-    await expectEventDuring(
-      matchesEvent(DwdsEventKind.devtoolsLaunch, {}),
-      () => keyboard.sendChord([Keyboard.alt, 'd']),
-    );
-  });
-
-  test('emits DEBUGGER_READY event', () async {
-    await expectEventDuring(
-      matchesEvent(DwdsEventKind.debuggerReady, {
-        'elapsedMilliseconds': isNotNull,
-      }),
-      () => keyboard.sendChord([Keyboard.alt, 'd']),
-    );
-  },
-      skip: 'Enable after publishing of '
-          'https://github.com/flutter/devtools/pull/3346');
-
-  test('events can be listened to multiple times', () async {
-    events.listen((_) {});
-    events.listen((_) {});
-  });
-
-  test('can emit event through service extension', () async {
-    final response = await expectEventDuring(
-        matchesEvent('foo-event', {'data': 1234}),
-        () => vmService.callServiceExtension('ext.dwds.emitEvent', args: {
-              'type': 'foo-event',
-              'payload': {'data': 1234},
-            }));
-    expect(response.type, 'Success');
-  });
-
-  group('evaluate', () {
-    Isolate isolate;
-    LibraryRef bootstrap;
-
-    setUpAll(() async {
-      setCurrentLogWriter();
-      final vm = await service.getVM();
-      isolate = await service.getIsolate(vm.isolates.first.id);
-      bootstrap = isolate.rootLib;
-    });
+  group('serve requests', () {
+    HttpServer server;
 
     setUp(() async {
       setCurrentLogWriter();
-    });
-
-    test('emits EVALUATE events on evaluation success', () async {
-      final expression = "helloString('world')";
-      await expectEventDuring(
-          matchesEvent(DwdsEventKind.evaluate, {
-            'expression': expression,
-            'success': isTrue,
-            'elapsedMilliseconds': isNotNull,
-          }),
-          () => service.evaluate(isolate.id, bootstrap.id, expression));
-    });
-
-    test('emits COMPILER_UPDATE_DEPENDENCIES event', () async {
-      await initialEvents;
-    });
-
-    test('emits EVALUATE events on evaluation failure', () async {
-      final expression = 'some-bad-expression';
-      await expectEventDuring(
-          matchesEvent(DwdsEventKind.evaluate, {
-            'expression': expression,
-            'success': isFalse,
-            'error': isA<ErrorRef>(),
-            'elapsedMilliseconds': isNotNull,
-          }),
-          () => service.evaluate(isolate.id, bootstrap.id, expression));
-    });
-  });
-
-  group('evaluateInFrame', () {
-    String isolateId;
-    Stream<Event> stream;
-    ScriptList scripts;
-    ScriptRef mainScript;
-
-    setUpAll(() async {
-      setCurrentLogWriter();
-      final vm = await service.getVM();
-
-      isolateId = vm.isolates.first.id;
-      scripts = await service.getScripts(isolateId);
-      await service.streamListen('Debug');
-      stream = service.onEvent('Debug');
-      mainScript = scripts.scripts
-          .firstWhere((script) => script.uri.contains('main.dart'));
-    });
-
-    setUp(() async {
-      setCurrentLogWriter();
-    });
-
-    test('emits EVALUATE_IN_FRAME events on RPC error', () async {
-      final expression = 'some-bad-expression';
-      await expectEventDuring(
-          matchesEvent(DwdsEventKind.evaluateInFrame, {
-            'expression': expression,
-            'success': isFalse,
-            'exception': isA<RPCError>().having(
-                (e) => e.message, 'message', contains('program is not paused')),
-            'elapsedMilliseconds': isNotNull,
-          }),
-          () => service
-              .evaluateInFrame(isolateId, 0, expression)
-              .catchError((_) {}));
-    });
-
-    test('emits EVALUATE_IN_FRAME events on evaluation error', () async {
-      final line = await context.findBreakpointLine(
-          'callPrintCount', isolateId, mainScript);
-      final bp = await service.addBreakpoint(isolateId, mainScript.id, line);
-      // Wait for breakpoint to trigger.
-      await stream
-          .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
-
-      // Evaluation succeeds and return ErrorRef containing compilation error,
-      // so event is marked as success.
-      final expression = 'some-bad-expression';
-      await expectEventDuring(
-          matchesEvent(DwdsEventKind.evaluateInFrame, {
-            'expression': expression,
-            'success': isFalse,
-            'error': isA<ErrorRef>(),
-            'elapsedMilliseconds': isNotNull,
-          }),
-          () => service
-              .evaluateInFrame(isolateId, 0, expression)
-              .catchError((_) {}));
-
-      await service.removeBreakpoint(isolateId, bp.id);
-      await service.resume(isolateId);
-    });
-
-    test('emits EVALUATE_IN_FRAME events on evaluation success', () async {
-      final line = await context.findBreakpointLine(
-          'callPrintCount', isolateId, mainScript);
-      final bp = await service.addBreakpoint(isolateId, mainScript.id, line);
-      // Wait for breakpoint to trigger.
-      await stream
-          .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
-
-      // Evaluation succeeds and return InstanceRef,
-      // so event is marked as success.
-      final expression = 'true';
-      await expectEventDuring(
-          matchesEvent(DwdsEventKind.evaluateInFrame, {
-            'expression': expression,
-            'success': isTrue,
-            'elapsedMilliseconds': isNotNull,
-          }),
-          () => service
-              .evaluateInFrame(isolateId, 0, expression)
-              .catchError((_) {}));
-
-      await service.removeBreakpoint(isolateId, bp.id);
-      await service.resume(isolateId);
-    });
-  });
-
-  group('getSourceReport', () {
-    String isolateId;
-    ScriptList scripts;
-    ScriptRef mainScript;
-
-    setUp(() async {
-      setCurrentLogWriter();
-      final vm = await service.getVM();
-      isolateId = vm.isolates.first.id;
-      scripts = await service.getScripts(isolateId);
-
-      mainScript = scripts.scripts
-          .firstWhere((script) => script.uri.contains('main.dart'));
-    });
-
-    test('emits GET_SOURCE_REPORT events', () async {
-      await expectEventDuring(
-          matchesEvent(DwdsEventKind.getSourceReport, {
-            'elapsedMilliseconds': isNotNull,
-          }),
-          () => service.getSourceReport(
-              isolateId, [SourceReportKind.kPossibleBreakpoints],
-              scriptId: mainScript.id));
-    });
-  });
-
-  group('getSripts', () {
-    String isolateId;
-
-    setUp(() async {
-      setCurrentLogWriter();
-      final vm = await service.getVM();
-      isolateId = vm.isolates.first.id;
-    });
-
-    test('emits GET_SCRIPTS events', () async {
-      await expectEventDuring(
-          matchesEvent(DwdsEventKind.getScripts, {
-            'elapsedMilliseconds': isNotNull,
-          }),
-          () => service.getScripts(isolateId));
-    });
-  });
-
-  group('getIsolate', () {
-    String isolateId;
-
-    setUp(() async {
-      setCurrentLogWriter();
-      final vm = await service.getVM();
-      isolateId = vm.isolates.first.id;
-    });
-
-    test('emits GET_ISOLATE events', () async {
-      await expectEventDuring(
-          matchesEvent(DwdsEventKind.getIsolate, {
-            'elapsedMilliseconds': isNotNull,
-          }),
-          () => service.getIsolate(isolateId));
-    });
-  });
-
-  group('getVM', () {
-    setUp(() async {
-      setCurrentLogWriter();
-    });
-
-    test('emits GET_VM events', () async {
-      await expectEventDuring(
-          matchesEvent(DwdsEventKind.getVM, {
-            'elapsedMilliseconds': isNotNull,
-          }),
-          () => service.getVM());
-    });
-  });
-
-  group('resume', () {
-    String isolateId;
-    Stream<Event> stream;
-    ScriptList scripts;
-    ScriptRef mainScript;
-
-    setUp(() async {
-      setCurrentLogWriter();
-      final vm = await service.getVM();
-      isolateId = vm.isolates.first.id;
-      scripts = await service.getScripts(isolateId);
-      await service.streamListen('Debug');
-      stream = service.onEvent('Debug');
-      mainScript = scripts.scripts
-          .firstWhere((script) => script.uri.contains('main.dart'));
-      final line = await context.findBreakpointLine(
-          'callPrintCount', isolateId, mainScript);
-      final bp = await service.addBreakpoint(isolateId, mainScript.id, line);
-      // Wait for breakpoint to trigger.
-      await stream
-          .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
-      await service.removeBreakpoint(isolateId, bp.id);
+      server = await HttpMultiServer.bind('localhost', 0);
     });
 
     tearDown(() async {
-      // Resume execution to not impact other tests.
-      await service.resume(isolateId);
+      await server?.close();
     });
 
-    test('emits RESUME events', () async {
+    test('emits HTTP_REQUEST_EXCEPTION event', () async {
+      final throwAsyncException = () async {
+        await Future.delayed(const Duration(milliseconds: 100));
+        throw Exception('async error');
+      };
+
+      // The events stream is a broadcast stream so start listening
+      // before the action.
+      final events = expectLater(
+          pipe(eventStream),
+          emitsThrough(matchesEvent(DwdsEventKind.httpRequestException, {
+            'server': 'FakeServer',
+            'exception': startsWith('Exception: async error'),
+          })));
+
+      // Start serving requests with a failing handler in an error zone.
+      serveHttpRequests(server, (request) async {
+        unawaited(throwAsyncException());
+        return null;
+      }, (e, s) {
+        emitEvent(DwdsEvent.httpRequestException('FakeServer', '$e:$s'));
+      });
+
+      // Send a request.
+      final client = HttpClient();
+      var request =
+          await client.getUrl(Uri.parse('http://localhost:${server.port}/foo'));
+
+      // Ignore the response.
+      var response = await request.close();
+      await response.drain();
+
+      // Wait for expected events.
+      await events;
+    });
+  });
+
+  group('with dwds', () {
+    Future initialEvents;
+    VmService vmService;
+    Keyboard keyboard;
+    Stream<DwdsEvent> events;
+
+    /// Runs [action] and waits for an event matching [eventMatcher].
+    Future<T> expectEventDuring<T>(
+        Matcher eventMatcher, Future<T> Function() action,
+        {Timeout timeout}) async {
+      // The events stream is a broadcast stream so start listening
+      // before the action.
+      final events = expectLater(
+          pipe(context.testServer.dwds.events, timeout: timeout),
+          emitsThrough(eventMatcher));
+      final result = await action();
+      await events;
+      return result;
+    }
+
+    setUpAll(() async {
+      setCurrentLogWriter();
+      initialEvents = expectLater(
+          pipe(eventStream, timeout: const Timeout.factor(5)),
+          emitsThrough(matchesEvent(DwdsEventKind.compilerUpdateDependencies, {
+            'entrypoint': 'hello_world/main.dart.bootstrap.js',
+            'elapsedMilliseconds': isNotNull
+          })));
+      await context.setUp(
+        serveDevTools: true,
+        enableExpressionEvaluation: true,
+      );
+      vmService = context.debugConnection.vmService;
+      keyboard = context.webDriver.driver.keyboard;
+      events = context.testServer.dwds.events;
+    });
+
+    tearDownAll(() async {
+      await context.tearDown();
+    });
+
+    test('emits DEVTOOLS_LAUNCH event', () async {
       await expectEventDuring(
-          matchesEvent(DwdsEventKind.resume, {
-            'step': 'Into',
-            'elapsedMilliseconds': isNotNull,
-          }),
-          () => service.resume(isolateId, step: 'Into'));
+        matchesEvent(DwdsEventKind.devtoolsLaunch, {}),
+        () => keyboard.sendChord([Keyboard.alt, 'd']),
+      );
+    });
+
+    test('emits DEBUGGER_READY event', () async {
+      await expectEventDuring(
+        matchesEvent(DwdsEventKind.debuggerReady, {
+          'elapsedMilliseconds': isNotNull,
+        }),
+        () => keyboard.sendChord([Keyboard.alt, 'd']),
+      );
+    },
+        skip: 'Enable after publishing of '
+            'https://github.com/flutter/devtools/pull/3346');
+
+    test('emits DEVTOOLS_LOAD events', () async {
+      await expectEventDuring(
+        matchesEvent(DwdsEventKind.devToolsLoad, {
+          'elapsedMilliseconds': isNotNull,
+        }),
+        () => keyboard.sendChord([Keyboard.alt, 'd']),
+      );
+    },
+        skip: 'Enable after publishing of '
+            'https://github.com/flutter/devtools/pull/3346');
+
+    test('events can be listened to multiple times', () async {
+      events.listen((_) {});
+      events.listen((_) {});
+    });
+
+    test('can emit event through service extension', () async {
+      final response = await expectEventDuring(
+          matchesEvent('foo-event', {'data': 1234}),
+          () => vmService.callServiceExtension('ext.dwds.emitEvent', args: {
+                'type': 'foo-event',
+                'payload': {'data': 1234},
+              }));
+      expect(response.type, 'Success');
+    });
+
+    group('evaluate', () {
+      Isolate isolate;
+      LibraryRef bootstrap;
+
+      setUpAll(() async {
+        setCurrentLogWriter();
+        final vm = await service.getVM();
+        isolate = await service.getIsolate(vm.isolates.first.id);
+        bootstrap = isolate.rootLib;
+      });
+
+      setUp(() async {
+        setCurrentLogWriter();
+      });
+
+      test('emits EVALUATE events on evaluation success', () async {
+        final expression = "helloString('world')";
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.evaluate, {
+              'expression': expression,
+              'success': isTrue,
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => service.evaluate(isolate.id, bootstrap.id, expression));
+      });
+
+      test('emits COMPILER_UPDATE_DEPENDENCIES event', () async {
+        await initialEvents;
+      });
+
+      test('emits EVALUATE events on evaluation failure', () async {
+        final expression = 'some-bad-expression';
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.evaluate, {
+              'expression': expression,
+              'success': isFalse,
+              'error': isA<ErrorRef>(),
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => service.evaluate(isolate.id, bootstrap.id, expression));
+      });
+    });
+
+    group('evaluateInFrame', () {
+      String isolateId;
+      Stream<Event> stream;
+      ScriptList scripts;
+      ScriptRef mainScript;
+
+      setUpAll(() async {
+        setCurrentLogWriter();
+        final vm = await service.getVM();
+
+        isolateId = vm.isolates.first.id;
+        scripts = await service.getScripts(isolateId);
+        await service.streamListen('Debug');
+        stream = service.onEvent('Debug');
+        mainScript = scripts.scripts
+            .firstWhere((script) => script.uri.contains('main.dart'));
+      });
+
+      setUp(() async {
+        setCurrentLogWriter();
+      });
+
+      test('emits EVALUATE_IN_FRAME events on RPC error', () async {
+        final expression = 'some-bad-expression';
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.evaluateInFrame, {
+              'expression': expression,
+              'success': isFalse,
+              'exception': isA<RPCError>().having((e) => e.message, 'message',
+                  contains('program is not paused')),
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => service
+                .evaluateInFrame(isolateId, 0, expression)
+                .catchError((_) {}));
+      });
+
+      test('emits EVALUATE_IN_FRAME events on evaluation error', () async {
+        final line = await context.findBreakpointLine(
+            'callPrintCount', isolateId, mainScript);
+        final bp = await service.addBreakpoint(isolateId, mainScript.id, line);
+        // Wait for breakpoint to trigger.
+        await stream
+            .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
+
+        // Evaluation succeeds and return ErrorRef containing compilation error,
+        // so event is marked as success.
+        final expression = 'some-bad-expression';
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.evaluateInFrame, {
+              'expression': expression,
+              'success': isFalse,
+              'error': isA<ErrorRef>(),
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => service
+                .evaluateInFrame(isolateId, 0, expression)
+                .catchError((_) {}));
+
+        await service.removeBreakpoint(isolateId, bp.id);
+        await service.resume(isolateId);
+      });
+
+      test('emits EVALUATE_IN_FRAME events on evaluation success', () async {
+        final line = await context.findBreakpointLine(
+            'callPrintCount', isolateId, mainScript);
+        final bp = await service.addBreakpoint(isolateId, mainScript.id, line);
+        // Wait for breakpoint to trigger.
+        await stream
+            .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
+
+        // Evaluation succeeds and return InstanceRef,
+        // so event is marked as success.
+        final expression = 'true';
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.evaluateInFrame, {
+              'expression': expression,
+              'success': isTrue,
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => service
+                .evaluateInFrame(isolateId, 0, expression)
+                .catchError((_) {}));
+
+        await service.removeBreakpoint(isolateId, bp.id);
+        await service.resume(isolateId);
+      });
+    });
+
+    group('getSourceReport', () {
+      String isolateId;
+      ScriptList scripts;
+      ScriptRef mainScript;
+
+      setUp(() async {
+        setCurrentLogWriter();
+        final vm = await service.getVM();
+        isolateId = vm.isolates.first.id;
+        scripts = await service.getScripts(isolateId);
+
+        mainScript = scripts.scripts
+            .firstWhere((script) => script.uri.contains('main.dart'));
+      });
+
+      test('emits GET_SOURCE_REPORT events', () async {
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.getSourceReport, {
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => service.getSourceReport(
+                isolateId, [SourceReportKind.kPossibleBreakpoints],
+                scriptId: mainScript.id));
+      });
+    });
+
+    group('getSripts', () {
+      String isolateId;
+
+      setUp(() async {
+        setCurrentLogWriter();
+        final vm = await service.getVM();
+        isolateId = vm.isolates.first.id;
+      });
+
+      test('emits GET_SCRIPTS events', () async {
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.getScripts, {
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => service.getScripts(isolateId));
+      });
+    });
+
+    group('getIsolate', () {
+      String isolateId;
+
+      setUp(() async {
+        setCurrentLogWriter();
+        final vm = await service.getVM();
+        isolateId = vm.isolates.first.id;
+      });
+
+      test('emits GET_ISOLATE events', () async {
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.getIsolate, {
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => service.getIsolate(isolateId));
+      });
+    });
+
+    group('getVM', () {
+      setUp(() async {
+        setCurrentLogWriter();
+      });
+
+      test('emits GET_VM events', () async {
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.getVM, {
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => service.getVM());
+      });
+    });
+
+    group('hotRestart', () {
+      setUp(() async {
+        setCurrentLogWriter();
+      });
+
+      test('emits HOT_RESTART event', () async {
+        var client = context.debugConnection.vmService;
+
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.hotRestart, {
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => client.callServiceExtension('hotRestart'));
+      });
+    });
+
+    group('resume', () {
+      String isolateId;
+      Stream<Event> stream;
+      ScriptList scripts;
+      ScriptRef mainScript;
+
+      setUp(() async {
+        setCurrentLogWriter();
+        final vm = await service.getVM();
+        isolateId = vm.isolates.first.id;
+        scripts = await service.getScripts(isolateId);
+        await service.streamListen('Debug');
+        stream = service.onEvent('Debug');
+        mainScript = scripts.scripts
+            .firstWhere((script) => script.uri.contains('main.dart'));
+        final line = await context.findBreakpointLine(
+            'callPrintCount', isolateId, mainScript);
+        final bp = await service.addBreakpoint(isolateId, mainScript.id, line);
+        // Wait for breakpoint to trigger.
+        await stream
+            .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
+        await service.removeBreakpoint(isolateId, bp.id);
+      });
+
+      tearDown(() async {
+        // Resume execution to not impact other tests.
+        await service.resume(isolateId);
+      });
+
+      test('emits RESUME events', () async {
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.resume, {
+              'step': 'Into',
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => service.resume(isolateId, step: 'Into'));
+      });
+    });
+
+    group('fullReload', () {
+      setUp(() async {
+        setCurrentLogWriter();
+      });
+
+      test('emits FULL_RELOAD event', () async {
+        var client = context.debugConnection.vmService;
+
+        await expectEventDuring(
+            matchesEvent(DwdsEventKind.fullReload, {
+              'elapsedMilliseconds': isNotNull,
+            }),
+            () => client.callServiceExtension('fullReload'));
+      });
     });
   });
 }