Skip to content

Commit ab0018b

Browse files
Convert JavaScript uncaught exception stack traces to Dart stacks (#1603)
* Convert JavaScript uncaught exception stack traces to Dart stacks Fixes #1265 Fixes flutter/flutter#101888 * Built
1 parent c78e7cc commit ab0018b

File tree

8 files changed

+74
-50
lines changed

8 files changed

+74
-50
lines changed

dwds/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## 14.0.3-dev
22
- Make data types null safe
33
- Update `package:vm_service` to 8.3.0.
4+
- Convert JavaScript stack traces in uncaught exceptions to Dart stack traces.
45

56
## 14.0.2
67
- Update the min SDK constraint to 2.17.0.

dwds/lib/src/debugging/debugger.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -558,8 +558,10 @@ class Debugger extends Domain {
558558
if (isNativeJsObject(exception)) {
559559
if (obj.description != null) {
560560
// Create a string exception object.
561-
exception = await inspector.instanceHelper
562-
.instanceRefFor(obj.description);
561+
var description =
562+
await inspector.mapExceptionStackTrace(obj.description);
563+
exception =
564+
await inspector.instanceHelper.instanceRefFor(description);
563565
} else {
564566
exception = null;
565567
}

dwds/lib/src/debugging/inspector.dart

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ class AppInspector extends Domain {
6767
final String _root;
6868
final SdkConfiguration _sdkConfiguration;
6969

70+
/// JavaScript expression that evaluates to the Dart stack trace mapper.
71+
static const stackTraceMapperExpression = '\$dartStackTraceUtility.mapper';
72+
73+
/// Regex used to extract the message from an exception description.
74+
static final exceptionMessageRegex = RegExp(r'^.*$', multiLine: true);
75+
7076
AppInspector._(
7177
this.appConnection,
7278
this.isolate,
@@ -223,7 +229,7 @@ class AppInspector extends Domain {
223229
/// [evalExpression] should be a JS function definition that can accept
224230
/// [arguments].
225231
Future<RemoteObject> _jsCallFunction(
226-
String evalExpression, List<RemoteObject> arguments,
232+
String evalExpression, List<Object> arguments,
227233
{bool returnByValue = false}) async {
228234
var jsArguments = arguments.map(callArgumentFor).toList();
229235
var result =
@@ -550,4 +556,23 @@ function($argsString) {
550556
handleErrorIfPresent(extensionsResult, evalContents: expression);
551557
return List.from(extensionsResult.result['result']['value'] as List);
552558
}
559+
560+
/// Convert a JS exception description into a description containing
561+
/// a Dart stack trace.
562+
Future<String> mapExceptionStackTrace(String description) async {
563+
RemoteObject mapperResult;
564+
try {
565+
mapperResult = await _jsCallFunction(
566+
stackTraceMapperExpression, <Object>[description]);
567+
} catch (_) {
568+
return description;
569+
}
570+
var mappedStack = mapperResult?.value?.toString();
571+
if (mappedStack == null || mappedStack.isEmpty) {
572+
return description;
573+
}
574+
var message = exceptionMessageRegex.firstMatch(description)?.group(0);
575+
message = (message != null) ? '$message\n' : '';
576+
return '$message$mappedStack';
577+
}
553578
}

dwds/lib/src/injected/client.js

Lines changed: 14 additions & 44 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dwds/lib/src/services/chrome_proxy_service.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -842,15 +842,19 @@ ${globalLoadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension(
842842
..timestamp = e.timestamp.toInt());
843843
});
844844
if (includeExceptions) {
845-
exceptionsSubscription = remoteDebugger.onExceptionThrown.listen((e) {
845+
exceptionsSubscription =
846+
remoteDebugger.onExceptionThrown.listen((e) async {
846847
var isolate = _inspector?.isolate;
847848
if (isolate == null) return;
849+
var description = e.exceptionDetails.exception.description;
850+
if (description != null) {
851+
description = await _inspector.mapExceptionStackTrace(description);
852+
}
848853
controller.add(Event(
849854
kind: EventKind.kWriteEvent,
850855
timestamp: DateTime.now().millisecondsSinceEpoch,
851856
isolate: _inspector.isolateRef)
852-
..bytes = base64.encode(
853-
utf8.encode(e.exceptionDetails.exception.description ?? '')));
857+
..bytes = base64.encode(utf8.encode(description ?? '')));
854858
});
855859
}
856860
});

dwds/test/chrome_proxy_service_test.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,8 @@ void main() {
11291129
var event = await stream
11301130
.firstWhere((event) => event.kind == EventKind.kPauseException);
11311131
expect(event.exception, isNotNull);
1132+
// Check that the exception stack trace has been mapped to Dart source files.
1133+
expect(event.exception.valueAsString, contains('main.dart'));
11321134

11331135
var stack = await service.getStack(isolateId);
11341136
expect(stack, isNotNull);
@@ -1660,6 +1662,18 @@ void main() {
16601662
await tabConnection.runtime.evaluate('console.error("Error");');
16611663
});
16621664

1665+
test('exception stack trace mapper', () async {
1666+
expect(service.streamListen('Stderr'), completion(_isSuccess));
1667+
var stderrStream = service.onEvent('Stderr');
1668+
expect(
1669+
stderrStream,
1670+
emitsThrough(predicate((Event event) =>
1671+
event.kind == EventKind.kWriteEvent &&
1672+
String.fromCharCodes(base64.decode(event.bytes))
1673+
.contains('main.dart'))));
1674+
await tabConnection.runtime.evaluate('throwUncaughtException();');
1675+
});
1676+
16631677
test('VM', () async {
16641678
var status = await service.streamListen('VM');
16651679
expect(status, _isSuccess);

fixtures/_test/example/hello_world/main.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ void main() async {
5555
log(message, name: 'testLogCategory');
5656
};
5757

58+
context['throwUncaughtException'] = () {
59+
scheduleMicrotask(() => throw Exception('UncaughtException'));
60+
};
61+
5862
Timer.periodic(const Duration(seconds: 1), (_) {
5963
printCount(); // Breakpoint: callPrintCount
6064
});

fixtures/_testSound/example/hello_world/main.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ void main() async {
5555
log(message, name: 'testLogCategory');
5656
};
5757

58+
context['throwUncaughtException'] = () {
59+
scheduleMicrotask(() => throw Exception('UncaughtException'));
60+
};
61+
5862
Timer.periodic(const Duration(seconds: 1), (_) {
5963
printCount(); // Breakpoint: callPrintCount
6064
});

0 commit comments

Comments
 (0)