fix long log details (#10011)
diff --git a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart
index 3c57084..d0ca370 100644
--- a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart
+++ b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart
@@ -538,23 +538,23 @@
         _isNotNull(stackTrace)) {
       detailsComputer = () async {
         // Get the full string value of the message.
-        String result = await _retrieveFullStringValue(
+        final fullMessage = await _retrieveFullStringValue(
           service,
           e.isolate!,
           messageRef,
         );
 
+        String? fullError;
         // Get information about the error object. Some users of the
         // dart:developer log call may pass a data payload in the `error`
         // field, encoded as a json encoded string, so handle that case.
         if (_isNotNull(error)) {
           if (error!.valueAsString != null) {
-            final errorString = await _retrieveFullStringValue(
+            fullError = await _retrieveFullStringValue(
               service,
               e.isolate!,
               error,
             );
-            result += '\n\n$errorString';
           } else {
             // Call `toString()` on the error object and display that.
             final toStringResult = await service!.invoke(
@@ -566,25 +566,28 @@
             );
 
             if (toStringResult is ErrorRef) {
-              final errorString = _valueAsString(error);
-              result += '\n\n$errorString';
+              fullError = _valueAsString(error);
             } else if (toStringResult is InstanceRef) {
-              final str = await _retrieveFullStringValue(
+              fullError = await _retrieveFullStringValue(
                 service,
                 e.isolate!,
                 toStringResult,
               );
-              result += '\n\n$str';
             }
           }
         }
 
         // Get info about the stackTrace object.
-        if (_isNotNull(stackTrace)) {
-          result += '\n\n${_valueAsString(stackTrace)}';
-        }
+        final fullStackTrace = _isNotNull(stackTrace)
+            ? _valueAsString(stackTrace)
+            : null;
 
-        return result;
+        return computeDeveloperLogDetailsJson(
+          e.json!,
+          fullMessage: fullMessage,
+          fullError: fullError,
+          fullStackTrace: fullStackTrace,
+        );
       };
     }
 
@@ -966,6 +969,66 @@
       : ref.valueAsString;
 }
 
+/// Returns a JSON-encoded Logging event with full string values applied.
+@visibleForTesting
+String computeDeveloperLogDetailsJson(
+  Map<String, dynamic> eventJson, {
+  required String fullMessage,
+  String? fullError,
+  String? fullStackTrace,
+}) {
+  final detailsJson = Map<String, dynamic>.of(eventJson);
+  final logRecord = detailsJson['logRecord'];
+  if (logRecord is Map<String, dynamic>) {
+    final logRecordCopy = Map<String, dynamic>.of(logRecord);
+
+    logRecordCopy['message'] = _applyFullStringToInstanceJson(
+      logRecordCopy['message'],
+      fullMessage,
+    );
+
+    if (fullError != null) {
+      final errorJson = logRecordCopy['error'];
+      if (errorJson is Map<String, dynamic> &&
+          errorJson['valueAsString'] != null) {
+        logRecordCopy['error'] = _applyFullStringToInstanceJson(
+          errorJson,
+          fullError,
+        );
+      } else {
+        logRecordCopy['errorAsString'] = fullError;
+      }
+    }
+
+    if (fullStackTrace != null) {
+      final stackJson = logRecordCopy['stackTrace'];
+      if (stackJson is Map<String, dynamic> &&
+          stackJson['valueAsString'] != null) {
+        logRecordCopy['stackTrace'] = _applyFullStringToInstanceJson(
+          stackJson,
+          fullStackTrace,
+        );
+      } else {
+        logRecordCopy['stackTraceAsString'] = fullStackTrace;
+      }
+    }
+
+    detailsJson['logRecord'] = logRecordCopy;
+  }
+
+  return jsonEncode(detailsJson);
+}
+
+Object? _applyFullStringToInstanceJson(Object? instanceJson, String fullValue) {
+  if (instanceJson is! Map<String, dynamic>) return instanceJson;
+  return <String, dynamic>{
+    ...instanceJson,
+    'valueAsString': fullValue,
+    'valueAsStringIsTruncated': false,
+    'length': fullValue.length,
+  };
+}
+
 /// A log data object that includes optional summary information about whether
 /// the log entry represents an error entry, the log entry kind, and more
 /// detailed data for the entry.
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index 9d13816..1e4cda3 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -72,6 +72,9 @@
 
 * Correct time units and cumulative nature of GC events.
   [#9890](https://github.com/flutter/devtools/pull/9890)
+* Fixed a bug where long `dart:developer` log messages lost their structured
+  Details view. -
+  [#10011](https://github.com/flutter/devtools/pull/10011),
 
 ## App size tool updates
 
diff --git a/packages/devtools_app/test/screens/logging/logging_controller_test.dart b/packages/devtools_app/test/screens/logging/logging_controller_test.dart
index f906aff..f047fad 100644
--- a/packages/devtools_app/test/screens/logging/logging_controller_test.dart
+++ b/packages/devtools_app/test/screens/logging/logging_controller_test.dart
@@ -504,6 +504,93 @@
         expect(nullDetails.prettyPrinted(), null);
       },
     );
+
+    test('computeDeveloperLogDetailsJson keeps structured details', () async {
+      const truncatedPreview = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.';
+      const fullMessage =
+          'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
+          'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.'
+          'signature-padding-to-make-this-longer-than-vm-preview';
+
+      final eventJson = <String, dynamic>{
+        'type': 'Event',
+        'kind': 'Logging',
+        'logRecord': {
+          'type': 'LogRecord',
+          'level': 800,
+          'message': {
+            'type': '@Instance',
+            'kind': 'String',
+            'valueAsString': truncatedPreview,
+            'valueAsStringIsTruncated': true,
+            'length': fullMessage.length,
+          },
+          'error': {'type': '@Instance', 'kind': 'Null'},
+          'stackTrace': {'type': '@Instance', 'kind': 'Null'},
+        },
+      };
+
+      final details = computeDeveloperLogDetailsJson(
+        eventJson,
+        fullMessage: fullMessage,
+      );
+      final log = LogData('log', details, 1);
+      await log.compute();
+
+      final decoded = jsonDecode(log.details!) as Map<String, dynamic>;
+      expect(decoded['kind'], 'Logging');
+      expect(decoded['logRecord'], isA<Map>());
+
+      final message =
+          (decoded['logRecord'] as Map)['message'] as Map<String, dynamic>;
+      expect(message['valueAsString'], fullMessage);
+      expect(message['valueAsStringIsTruncated'], false);
+      expect(message['length'], fullMessage.length);
+      expect(jsonDecode(log.encodedDetails), isA<Map>());
+    });
+
+    test('computeDeveloperLogDetailsJson updates error and stackTrace', () {
+      final eventJson = <String, dynamic>{
+        'type': 'Event',
+        'kind': 'Logging',
+        'logRecord': {
+          'message': {
+            'valueAsString': 'short',
+            'valueAsStringIsTruncated': false,
+            'length': 5,
+          },
+          'error': {
+            'valueAsString': 'err...',
+            'valueAsStringIsTruncated': true,
+            'length': 20,
+          },
+          'stackTrace': {
+            'valueAsString': '#0 foo',
+            'valueAsStringIsTruncated': false,
+            'length': 6,
+          },
+        },
+      };
+
+      final details = computeDeveloperLogDetailsJson(
+        eventJson,
+        fullMessage: 'short',
+        fullError: 'error-full-message',
+        fullStackTrace: '#0 foo\n#1 bar',
+      );
+      final decoded = jsonDecode(details) as Map<String, dynamic>;
+      final logRecord = decoded['logRecord'] as Map<String, dynamic>;
+
+      expect(
+        (logRecord['error'] as Map)['valueAsString'],
+        'error-full-message',
+      );
+      expect((logRecord['error'] as Map)['valueAsStringIsTruncated'], false);
+      expect(
+        (logRecord['stackTrace'] as Map)['valueAsString'],
+        '#0 foo\n#1 bar',
+      );
+    });
   });
 
   group('StdoutEventHandler', () {