Stepping and evaluation analytics (#1248)

diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index 96a0fee..0e3056a 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -6,6 +6,7 @@
   is paused.
 - Support keep-alive for debug service connections.
 - Depend on the latest `package:sse`.
+- Add `DwdsEvent`s around stepping and evaluation.
 
 **Breaking changes:**
 - `LoadStrategy`s now require a `moduleInfoForEntrypoint`.
diff --git a/dwds/lib/src/events.dart b/dwds/lib/src/events.dart
index 816182e..616c94d 100644
--- a/dwds/lib/src/events.dart
+++ b/dwds/lib/src/events.dart
@@ -9,6 +9,11 @@
   final Map<String, dynamic> payload;
 
   DwdsEvent(this.type, this.payload);
+
+  @override
+  String toString() {
+    return 'TYPE: $type Payload: $payload';
+  }
 }
 
 final _eventController = StreamController<DwdsEvent>.broadcast();
diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart
index 793457c..09122b1 100644
--- a/dwds/lib/src/services/chrome_proxy_service.dart
+++ b/dwds/lib/src/services/chrome_proxy_service.dart
@@ -12,6 +12,7 @@
 import 'package:vm_service/vm_service.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
+import '../../dwds.dart';
 import '../connections/app_connection.dart';
 import '../debugging/debugger.dart';
 import '../debugging/execution_context.dart';
@@ -21,6 +22,7 @@
 import '../debugging/modules.dart';
 import '../debugging/remote_debugger.dart';
 import '../debugging/skip_list.dart';
+import '../events.dart';
 import '../loaders/strategy.dart';
 import '../readers/asset_reader.dart';
 import '../utilities/dart_uri.dart';
@@ -175,7 +177,12 @@
       await _compiler.initialize(soundNullSafety: soundNullSafety);
       var dependencies =
           await globalLoadStrategy.moduleInfoForEntrypoint(entrypoint);
+      var stopwatch = Stopwatch()..start();
       await _compiler.updateDependencies(dependencies);
+      emitEvent(DwdsEvent('COMPILER_UPDATE_DEPENDENCIES', {
+        'entrypoint': entrypoint,
+        'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
+      }));
     }
   }
 
@@ -343,9 +350,23 @@
       String isolateId, String targetId, String expression,
       {Map<String, String> scope, bool disableBreakpoints}) async {
     // TODO(798) - respect disableBreakpoints.
-    var remote = await _inspector?.evaluate(isolateId, targetId, expression,
-        scope: scope);
-    return _inspector?.instanceHelper?.instanceRefFor(remote);
+    var stopwatch = Stopwatch()..start();
+    dynamic error;
+    try {
+      var remote = await _inspector?.evaluate(isolateId, targetId, expression,
+          scope: scope);
+      return _inspector?.instanceHelper?.instanceRefFor(remote);
+    } catch (e) {
+      error = e;
+      rethrow;
+    } finally {
+      emitEvent(DwdsEvent('EVALUATE', {
+        'expression': expression,
+        'success': error == null,
+        'exception': error,
+        'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
+      }));
+    }
   }
 
   @override
@@ -358,7 +379,8 @@
         throw RPCError('evaluateInFrame', RPCError.kInvalidParams,
             'Unrecognized isolate id: $isolateId. Supported isolate: ${isolate?.id}');
       }
-
+      dynamic error;
+      var stopwatch = Stopwatch()..start();
       try {
         var result = await _expressionEvaluator.evaluateExpression(
             isolateId, frameIndex, expression);
@@ -382,6 +404,7 @@
         }
         return _inspector?.instanceHelper?.instanceRefFor(result);
       } catch (e, s) {
+        error = e;
         // Handle errors that throw exceptions, such as invalid JavaScript
         // generated by the expression evaluator
         _logger.warning('Failed to evaluate expression \'$expression\'. ');
@@ -390,6 +413,13 @@
             'to file a bug.');
         _logger.info('$e:$s');
         return ErrorRef(kind: 'error', message: '<unknown>', id: createId());
+      } finally {
+        emitEvent(DwdsEvent('EVALUATE_IN_FRAME', {
+          'expression': expression,
+          'success': error == null,
+          'exception': error,
+          'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
+        }));
       }
     }
 
@@ -579,8 +609,14 @@
       {String step, int frameIndex}) async {
     if (_inspector == null) throw StateError('No running isolate.');
     if (_inspector.appConnection.isStarted) {
-      return await (await _debugger)
+      var stopwatch = Stopwatch()..start();
+      var result = await (await _debugger)
           .resume(isolateId, step: step, frameIndex: frameIndex);
+      emitEvent(DwdsEvent('RESUME', {
+        'step': step,
+        'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
+      }));
+      return result;
     } else {
       _inspector.appConnection.runMain();
       return Success();
diff --git a/dwds/test/events_test.dart b/dwds/test/events_test.dart
index 8661d8f..f31b28f 100644
--- a/dwds/test/events_test.dart
+++ b/dwds/test/events_test.dart
@@ -2,17 +2,30 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:async';
+
+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:test/test.dart';
+import 'package:vm_service/vm_service.dart';
 import 'package:webdriver/async_core.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
 import 'fixtures/context.dart';
 
+ChromeProxyService get service =>
+    fetchChromeProxyService(context.debugConnection);
+
+WipConnection get tabConnection => context.tabConnection;
+
+final context = TestContext();
+
 void main() {
-  final context = TestContext();
   setUpAll(() async {
     await context.setUp(
       serveDevTools: true,
+      enableExpressionEvaluation: true,
     );
   });
 
@@ -28,4 +41,81 @@
     context.testServer.dwds.events.listen((_) {});
     context.testServer.dwds.events.listen((_) {});
   });
+
+  group('evaluate', () {
+    Isolate isolate;
+    LibraryRef bootstrap;
+
+    setUpAll(() async {
+      var vm = await service.getVM();
+      isolate = await service.getIsolate(vm.isolates.first.id);
+      bootstrap = isolate.libraries.first;
+    });
+
+    test('emits EVALUATE events with expression', () async {
+      var expression = "helloString('world')";
+      expect(
+          context.testServer.dwds.events,
+          emits(predicate((DwdsEvent event) =>
+              event.type == 'EVALUATE' &&
+              event.payload['expression'] == expression)));
+      await service.evaluate(
+        isolate.id,
+        bootstrap.id,
+        expression,
+      );
+    });
+  });
+
+  test('emits EVALUATE_IN_FRAME events', () async {
+    var vm = await service.getVM();
+    var isolate = await service.getIsolate(vm.isolates.first.id);
+    expect(
+        context.testServer.dwds.events,
+        emits(predicate((DwdsEvent event) =>
+            event.type == 'EVALUATE_IN_FRAME' &&
+            event.payload['success'] == false)));
+    try {
+      await service.evaluateInFrame(
+        isolate.id,
+        0,
+        'some-bad-expression',
+      );
+    } catch (_) {}
+  });
+
+  group('resume', () {
+    String isolateId;
+    Stream<Event> stream;
+    ScriptList scripts;
+    ScriptRef mainScript;
+
+    setUp(() async {
+      var 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'));
+      var line = await context.findBreakpointLine(
+          'callPrintCount', isolateId, mainScript);
+      var 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 {
+      expect(context.testServer.dwds.events,
+          emits(predicate((DwdsEvent event) => event.type == 'RESUME')));
+      await service.resume(isolateId, step: 'Into');
+    });
+  });
 }