Send text error in JSON and print in tools (#59018)

diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart
index 03177fa..fe655c6 100644
--- a/packages/flutter/lib/src/widgets/widget_inspector.dart
+++ b/packages/flutter/lib/src/widgets/widget_inspector.dart
@@ -929,8 +929,17 @@
     );
 
     errorJson['errorsSinceReload'] = _errorsSinceReload;
-    _errorsSinceReload += 1;
+    if (_errorsSinceReload == 0) {
+      errorJson['renderedErrorText'] = TextTreeRenderer(
+        wrapWidth: FlutterError.wrapWidth,
+        wrapWidthProperties: FlutterError.wrapWidth,
+        maxDescendentsTruncatableNode: 5,
+      ).render(details.toDiagnosticsNode(style: DiagnosticsTreeStyle.error)).trimRight();
+    } else {
+      errorJson['renderedErrorText'] = 'Another exception was thrown: ${details.summary}';
+    }
 
+    _errorsSinceReload += 1;
     postEvent('Flutter.Error', errorJson);
   }
 
diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart
index 8cdd02a..7c467e2 100644
--- a/packages/flutter/test/widgets/widget_inspector_test.dart
+++ b/packages/flutter/test/widgets/widget_inspector_test.dart
@@ -2254,6 +2254,10 @@
 
         // Validate that we received an error count.
         expect(error['errorsSinceReload'], 0);
+        expect(
+            error['renderedErrorText'],
+            startsWith(
+                '══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞════════════'));
 
         // Send a second error.
         FlutterError.reportError(FlutterErrorDetailsForRendering(
@@ -2267,6 +2271,7 @@
         expect(flutterErrorEvents, hasLength(2));
         error = flutterErrorEvents.last;
         expect(error['errorsSinceReload'], 1);
+        expect(error['renderedErrorText'], startsWith('Another exception was thrown:'));
 
         // Reloads the app.
         final FlutterExceptionHandler oldHandler = FlutterError.onError;
diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
index e2ad8f7..a28fb74 100644
--- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart
@@ -112,6 +112,7 @@
   ConnectionResult _connectionResult;
   StreamSubscription<vmservice.Event> _stdOutSub;
   StreamSubscription<vmservice.Event> _stdErrSub;
+  StreamSubscription<vmservice.Event> _extensionEventSub;
   bool _exited = false;
   WipConnection _wipConnection;
   ChromiumLauncher _chromiumLauncher;
@@ -150,6 +151,7 @@
     }
     await _stdOutSub?.cancel();
     await _stdErrSub?.cancel();
+    await _extensionEventSub?.cancel();
     await device.device.stopApp(null);
     try {
       _generatedEntrypointDirectory?.deleteSync(recursive: true);
@@ -675,6 +677,8 @@
         final String message = utf8.decode(base64.decode(log.bytes));
         globals.printStatus(message, newline: false);
       });
+      _extensionEventSub =
+          _vmService.onExtensionEvent.listen(printStructuredErrorLog);
       try {
         await _vmService.streamListen(vmservice.EventStreams.kStdout);
       } on vmservice.RPCError {
@@ -693,6 +697,12 @@
         // It is safe to ignore this error because we expect an error to be
         // thrown if we're not already subscribed.
       }
+      try {
+        await _vmService.streamListen(vmservice.EventStreams.kExtension);
+      } on vmservice.RPCError {
+        // It is safe to ignore this error because we expect an error to be
+        // thrown if we're not already subscribed.
+      }
       unawaited(_vmService.registerService('reloadSources', 'FlutterTools'));
       _vmService.registerServiceCallback('reloadSources', (Map<String, Object> params) async {
         final bool pause = params['pause'] as bool ?? false;
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index e16a3bc..573467b 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -188,6 +188,7 @@
     CompileExpression compileExpression,
     ReloadMethod reloadMethod,
     GetSkSLMethod getSkSLMethod,
+    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
   }) {
     final Completer<void> completer = Completer<void>();
     StreamSubscription<void> subscription;
@@ -207,6 +208,7 @@
           compileExpression: compileExpression,
           reloadMethod: reloadMethod,
           getSkSLMethod: getSkSLMethod,
+          printStructuredErrorLogMethod: printStructuredErrorLogMethod,
           device: device,
         );
       } on Exception catch (exception) {
@@ -1061,11 +1063,23 @@
       final String copyPath = getDefaultCachedKernelPath(
         trackWidgetCreation: trackWidgetCreation,
       );
-      globals.fs.file(copyPath).parent.createSync(recursive: true);
+      globals.fs
+          .file(copyPath)
+          .parent
+          .createSync(recursive: true);
       outputDill.copySync(copyPath);
     }
   }
 
+  void printStructuredErrorLog(vm_service.Event event) {
+    if (event.extensionKind == 'Flutter.Error') {
+      final Map<dynamic, dynamic> json = event.extensionData?.data;
+      if (json != null && json.containsKey('renderedErrorText')) {
+        globals.printStatus('\n${json['renderedErrorText']}');
+      }
+    }
+  }
+
   /// If the [reloadSources] parameter is not null the 'reloadSources' service
   /// will be registered.
   //
@@ -1090,7 +1104,8 @@
         restart: restart,
         compileExpression: compileExpression,
         reloadMethod: reloadMethod,
-        getSkSLMethod: getSkSLMethod
+        getSkSLMethod: getSkSLMethod,
+        printStructuredErrorLogMethod: printStructuredErrorLog,
       );
       // This will wait for at least one flutter view before returning.
       final Status status = globals.logger.startProgress(
diff --git a/packages/flutter_tools/lib/src/tracing.dart b/packages/flutter_tools/lib/src/tracing.dart
index 1a32c45..7458eb0 100644
--- a/packages/flutter_tools/lib/src/tracing.dart
+++ b/packages/flutter_tools/lib/src/tracing.dart
@@ -46,7 +46,12 @@
       );
       try {
         final Completer<void> whenFirstFrameRendered = Completer<void>();
-        await vmService.streamListen('Extension');
+        try {
+          await vmService.streamListen(vm_service.EventStreams.kExtension);
+        } on vm_service.RPCError {
+          // It is safe to ignore this error because we expect an error to be
+          // thrown if we're already subscribed.
+        }
         vmService.onExtensionEvent.listen((vm_service.Event event) {
           if (event.extensionKind == 'Flutter.FirstFrame') {
             whenFirstFrameRendered.complete();
diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart
index 9fcf284..ad7218b 100644
--- a/packages/flutter_tools/lib/src/vmservice.dart
+++ b/packages/flutter_tools/lib/src/vmservice.dart
@@ -31,6 +31,8 @@
 /// for [WebSocket]s (used by tests).
 typedef WebSocketConnector = Future<io.WebSocket> Function(String url, {io.CompressionOptions compression});
 
+typedef PrintStructuredErrorLogMethod = void Function(vm_service.Event);
+
 WebSocketConnector _openChannel = _defaultOpenChannel;
 
 /// The error codes for the JSON-RPC standard.
@@ -149,6 +151,7 @@
   CompileExpression compileExpression,
   ReloadMethod reloadMethod,
   GetSkSLMethod getSkSLMethod,
+  PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
   io.CompressionOptions compression,
   Device device,
 });
@@ -175,6 +178,7 @@
   Device device,
   ReloadMethod reloadMethod,
   GetSkSLMethod skSLMethod,
+  PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
   vm_service.VmService vmService
 ) {
   if (reloadSources != null) {
@@ -293,6 +297,15 @@
     });
     vmService.registerService('flutterGetSkSL', 'Flutter Tools');
   }
+  if (printStructuredErrorLogMethod != null) {
+    try {
+      vmService.streamListen(vm_service.EventStreams.kExtension);
+    } on vm_service.RPCError {
+      // It is safe to ignore this error because we expect an error to be
+      // thrown if we're already subscribed.
+    }
+    vmService.onExtensionEvent.listen(printStructuredErrorLogMethod);
+  }
   return vmService;
 }
 
@@ -311,6 +324,7 @@
     CompileExpression compileExpression,
     ReloadMethod reloadMethod,
     GetSkSLMethod getSkSLMethod,
+    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
     io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
     Device device,
   }) async {
@@ -323,6 +337,7 @@
     device: device,
     reloadMethod: reloadMethod,
     getSkSLMethod: getSkSLMethod,
+    printStructuredErrorLogMethod: printStructuredErrorLogMethod,
   );
 }
 
@@ -333,6 +348,7 @@
   CompileExpression compileExpression,
   ReloadMethod reloadMethod,
   GetSkSLMethod getSkSLMethod,
+  PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
   io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
   Device device,
 }) async {
@@ -354,6 +370,7 @@
     device,
     reloadMethod,
     getSkSLMethod,
+    printStructuredErrorLogMethod,
     delegateService,
   );
   _httpAddressExpando[service] = httpUri;
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
index e4c8b94..c1c2722 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
@@ -705,6 +705,7 @@
     CompileExpression compileExpression,
     ReloadMethod reloadMethod,
     GetSkSLMethod getSkSLMethod,
+    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
     CompressionOptions compression,
     Device device,
   }) async {
diff --git a/packages/flutter_tools/test/general.shard/cold_test.dart b/packages/flutter_tools/test/general.shard/cold_test.dart
index 83b3d74..d9f88a9 100644
--- a/packages/flutter_tools/test/general.shard/cold_test.dart
+++ b/packages/flutter_tools/test/general.shard/cold_test.dart
@@ -135,6 +135,7 @@
     CompileExpression compileExpression,
     ReloadMethod reloadMethod,
     GetSkSLMethod getSkSLMethod,
+    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
   }) async {
     throw exception;
   }
diff --git a/packages/flutter_tools/test/general.shard/hot_test.dart b/packages/flutter_tools/test/general.shard/hot_test.dart
index 48de79b..1f78639 100644
--- a/packages/flutter_tools/test/general.shard/hot_test.dart
+++ b/packages/flutter_tools/test/general.shard/hot_test.dart
@@ -510,6 +510,7 @@
     CompileExpression compileExpression,
     ReloadMethod reloadMethod,
     GetSkSLMethod getSkSLMethod,
+    PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
   }) async {
     throw exception;
   }
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
index 2d4222e..7737f6c 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -1357,6 +1357,7 @@
       CompileExpression compileExpression,
       ReloadMethod reloadMethod,
       GetSkSLMethod getSkSLMethod,
+      PrintStructuredErrorLogMethod printStructuredErrorLogMethod,
       io.CompressionOptions compression,
       Device device,
     }) async => mockVMService,
diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
index 23285db..b93c348 100644
--- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 import 'dart:convert';
+import 'dart:io';
 
 import 'package:dwds/dwds.dart';
 import 'package:flutter_tools/src/base/common.dart';
@@ -57,6 +58,12 @@
     }
   ),
   FakeVmServiceRequest(
+    method: 'streamListen',
+    args: <String, Object>{
+      'streamId': 'Extension',
+    },
+  ),
+  FakeVmServiceRequest(
     method: 'registerService',
     args: <String, Object>{
       'service': 'reloadSources',
@@ -340,6 +347,68 @@
     expect(testLogger.statusText, contains('SO IS THIS'));
   }));
 
+  test('Listens to extension events with structured errors', () => testbed.run(() async {
+    final Map<String, String> extensionData = <String, String>{
+      'test': 'data',
+      'renderedErrorText': 'error text',
+    };
+    final Map<String, String> emptyExtensionData = <String, String>{
+      'test': 'data',
+      'renderedErrorText': '',
+    };
+    final Map<String, String> nonStructuredErrorData = <String, String>{
+      'other': 'other stuff',
+    };
+    fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
+      ...kAttachExpectations,
+      FakeVmServiceStreamResponse(
+        streamId: 'Extension',
+        event: vm_service.Event(
+          timestamp: 0,
+          extensionKind: 'Flutter.Error',
+          extensionData: vm_service.ExtensionData.parse(extensionData),
+          kind: vm_service.EventStreams.kExtension,
+        ),
+      ),
+      // Empty error text should not break anything.
+      FakeVmServiceStreamResponse(
+        streamId: 'Extension',
+        event: vm_service.Event(
+          timestamp: 0,
+          extensionKind: 'Flutter.Error',
+          extensionData: vm_service.ExtensionData.parse(emptyExtensionData),
+          kind: vm_service.EventStreams.kExtension,
+        ),
+      ),
+      // This is not Flutter.Error kind data, so it should not be logged.
+      FakeVmServiceStreamResponse(
+        streamId: 'Extension',
+        event: vm_service.Event(
+          timestamp: 0,
+          extensionKind: 'Other',
+          extensionData: vm_service.ExtensionData.parse(nonStructuredErrorData),
+          kind: vm_service.EventStreams.kExtension,
+        ),
+      ),
+    ]);
+
+    _setupMocks();
+    final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
+    unawaited(residentWebRunner.run(
+      connectionInfoCompleter: connectionInfoCompleter,
+    ));
+    await connectionInfoCompleter.future;
+
+    // Need these to run events, otherwise expect statements below run before
+    // structured errors are processed.
+    await null;
+    await null;
+    await null;
+
+    expect(testLogger.statusText, contains('\nerror text'));
+    expect(testLogger.statusText, isNot(contains('other stuff')));
+  }));
+
   test('Does not run main with --start-paused', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
     residentWebRunner = DwdsWebRunnerFactory().createWebRunner(
@@ -1073,19 +1142,7 @@
   test('Sends launched app.webLaunchUrl event for Chrome device', () => testbed.run(() async {
     fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
       ...kAttachLogExpectations,
-      const FakeVmServiceRequest(
-        method: 'streamListen',
-        args: <String, Object>{
-          'streamId': 'Isolate'
-        }
-      ),
-      const FakeVmServiceRequest(
-        method: 'registerService',
-        args: <String, Object>{
-          'service': 'reloadSources',
-          'alias': 'FlutterTools',
-        }
-      )
+      ...kAttachIsolateExpectations,
     ]);
     _setupMocks();
     final ChromiumLauncher chromiumLauncher = MockChromeLauncher();
diff --git a/packages/flutter_tools/test/general.shard/vmservice_test.dart b/packages/flutter_tools/test/general.shard/vmservice_test.dart
index 859e8b0..78bfed1 100644
--- a/packages/flutter_tools/test/general.shard/vmservice_test.dart
+++ b/packages/flutter_tools/test/general.shard/vmservice_test.dart
@@ -104,6 +104,7 @@
       null,
       null,
       null,
+      null,
       mockVMService,
     );
 
@@ -123,6 +124,7 @@
       null,
       reloadMethod,
       null,
+      null,
       mockVMService,
     );
 
@@ -142,6 +144,7 @@
       mockDevice,
       null,
       null,
+      null,
       mockVMService,
     );
 
@@ -159,6 +162,7 @@
       null,
       null,
       () async => 'hello',
+      null,
       mockVMService,
     );
 
@@ -167,6 +171,26 @@
     Logger: () => BufferLogger.test()
   });
 
+  testUsingContext('VmService registers flutterPrintStructuredErrorLogMethod', () async {
+    final MockVMService mockVMService = MockVMService();
+    when(mockVMService.onExtensionEvent).thenAnswer((Invocation invocation) {
+      return const Stream<vm_service.Event>.empty();
+    });
+    setUpVmService(
+      null,
+      null,
+      null,
+      null,
+      null,
+      null,
+      (vm_service.Event event) async => 'hello',
+      mockVMService,
+    );
+    verify(mockVMService.streamListen(vm_service.EventStreams.kExtension)).called(1);
+  }, overrides: <Type, Generator>{
+    Logger: () => BufferLogger.test()
+  });
+
   testUsingContext('VMService returns correct FlutterVersion', () async {
     final MockVMService mockVMService = MockVMService();
     setUpVmService(
@@ -176,6 +200,7 @@
       null,
       null,
       null,
+      null,
       mockVMService,
     );