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', () {