Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ void setupErrorHandling(Future Function() appStartCallback) {
() async {
WidgetsFlutterBinding.ensureInitialized();

await _initializeSourceMapping();
await _fetchSourceMapping();

final FlutterExceptionHandler? oldHandler = FlutterError.onError;

Expand Down Expand Up @@ -109,18 +109,18 @@ Future<void> _reportError(
}
}

// ignore: unused-code, TODO(https://github.com/flutter/devtools/issues/9910) seems like a bug.
SingleMapping? _cachedJsSourceMapping;

// ignore: unused-code, TODO(https://github.com/flutter/devtools/issues/9910) seems like a bug.
SingleMapping? _cachedWasmSourceMapping;
Future<SingleMapping?>? _cachedSourceMapping;

Future<SingleMapping?> _fetchSourceMapping() async {
final cachedSourceMapping = kIsWasm
? _cachedWasmSourceMapping
: _cachedJsSourceMapping;

return cachedSourceMapping ?? (await _initializeSourceMapping());
final pendingSourceMapping = _cachedSourceMapping ??=
_initializeSourceMapping();
final sourceMapping = await pendingSourceMapping;
// Retry failed loads without letting an older failure clear a newer request.
if (sourceMapping == null &&
identical(_cachedSourceMapping, pendingSourceMapping)) {
_cachedSourceMapping = null;
}
return sourceMapping;
}

Future<SingleMapping?> _initializeSourceMapping() async {
Expand Down
3 changes: 3 additions & 0 deletions packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ To learn more about DevTools, check out the

## General updates

* Reused source maps across error reports instead of fetching and parsing them
repeatedly. [#9910](https://github.com/flutter/devtools/issues/9910)

* Added guidance to run "flutter attach" when a Flutter debug app cannot
evaluate expressions and a debug-only screen is unavailable.
[#4091](https://github.com/flutter/devtools/issues/4091)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2026 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

@TestOn('vm')
library;

import 'package:devtools_app/src/shared/framework/app_error_handling.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:logging/logging.dart';

void main() {
test(
'keeps the original stack without fetching source maps on the VM',
() async {
var requests = 0;
final client = MockClient((_) async {
requests++;
return http.Response('{}', 200);
});

await http.runWithClient(() async {
final logged = Logger('app_error_handling').onRecord.first;
reportError(
'VM error',
stack: StackTrace.fromString(
'#0 original (package:devtools_app/original.dart:1:1)',
),
);
expect((await logged).message, contains('original.dart'));
expect(requests, 0);
}, () => client);
},
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2026 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

@TestOn('browser')
library;

import 'dart:async';
import 'dart:convert';

import 'package:devtools_app/src/shared/framework/app_error_handling.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:logging/logging.dart';

void main() {
test(
'shares source maps across startup and errors, retrying failed loads',
() async {
final oldFlutterErrorHandler = FlutterError.onError;
final oldPlatformErrorHandler = PlatformDispatcher.instance.onError;
addTearDown(() {
FlutterError.onError = oldFlutterErrorHandler;
PlatformDispatcher.instance.onError = oldPlatformErrorHandler;
});

var requests = 0;
final successfulResponse = Completer<http.Response>();
final sourceMap = jsonEncode({
'version': 3,
'file': 'main.dart.${kIsWasm ? 'wasm' : 'js'}',
'sources': ['original.dart'],
'names': ['originalFunction'],
'mappings': 'AAAAA',
});
final client = MockClient((request) {
expect(request.url.path, 'main.dart.${kIsWasm ? 'wasm' : 'js'}.map');
requests++;
switch (requests) {
case 1:
return Future.value(http.Response('Not found', 404));
case 2:
return Future.value(http.Response('invalid source map', 200));
case 3:
return Future.error(http.ClientException('Offline', request.url));
default:
return successfulResponse.future;
}
});

Future<void> startApp() async {
final started = Completer<void>();
setupErrorHandling(() async {
started.complete();
});
await started.future;
}

Future<LogRecord> report(String message) {
final logged = Logger('app_error_handling').onRecord.firstWhere(
(record) => record.message.startsWith('[source-map-test]: $message'),
);
reportError(
message,
errorType: 'source-map-test',
stack: StackTrace.fromString(
' at generated (main.dart.${kIsWasm ? 'wasm' : 'js'}:1:1)',
),
);
return logged;
}

await http.runWithClient(() async {
await startApp();
expect(requests, 1);
await report('invalid map');
await report('offline map');
expect(requests, 3);

final first = report('first mapped error');
final second = report('second mapped error');
successfulResponse.complete(http.Response(sourceMap, 200));
final records = await Future.wait([first, second]);
for (final record in records) {
expect(record.message, contains('original.dart'));
}
expect(
(await report('later mapped error')).message,
contains('original.dart'),
);
await startApp();
expect(requests, 4);
}, () => client);
},
);
}
Loading