| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:file/file.dart'; |
| import 'package:http/http.dart' as http; |
| |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../base/logger.dart'; |
| import '../base/os.dart'; |
| import '../base/platform.dart'; |
| import '../doctor.dart'; |
| import '../project.dart'; |
| import 'github_template.dart'; |
| import 'reporting.dart'; |
| |
| /// Tells crash backend that the error is from the Flutter CLI. |
| const String _kProductId = 'Flutter_Tools'; |
| |
| /// Tells crash backend that this is a Dart error as opposed to, say, Java. |
| const String _kDartTypeId = 'DartError'; |
| |
| /// Crash backend host. |
| const String _kCrashServerHost = 'clients2.google.com'; |
| |
| /// Path to the crash servlet. |
| const String _kCrashEndpointPath = '/cr/report'; |
| |
| /// The field corresponding to the multipart/form-data file attachment where |
| /// crash backend expects to find the Dart stack trace. |
| const String _kStackTraceFileField = 'DartError'; |
| |
| /// The name of the file attached as [_kStackTraceFileField]. |
| /// |
| /// The precise value is not important. It is ignored by the crash back end, but |
| /// it must be supplied in the request. |
| const String _kStackTraceFilename = 'stacktrace_file'; |
| |
| class CrashDetails { |
| CrashDetails({ |
| required this.command, |
| required this.error, |
| required this.stackTrace, |
| required this.doctorText, |
| }); |
| |
| final String command; |
| final Object error; |
| final StackTrace stackTrace; |
| final DoctorText doctorText; |
| } |
| |
| /// Reports information about the crash to the user. |
| class CrashReporter { |
| CrashReporter({ |
| required FileSystem fileSystem, |
| required Logger logger, |
| required FlutterProjectFactory flutterProjectFactory, |
| }) : _fileSystem = fileSystem, |
| _logger = logger, |
| _flutterProjectFactory = flutterProjectFactory; |
| |
| final FileSystem _fileSystem; |
| final Logger _logger; |
| final FlutterProjectFactory _flutterProjectFactory; |
| |
| /// Prints instructions for filing a bug about the crash. |
| Future<void> informUser(CrashDetails details, File crashFile) async { |
| _logger.printError('A crash report has been written to ${crashFile.path}.'); |
| _logger.printStatus('This crash may already be reported. Check GitHub for similar crashes.', emphasis: true); |
| |
| final String similarIssuesURL = GitHubTemplateCreator.toolCrashSimilarIssuesURL(details.error.toString()); |
| _logger.printStatus('$similarIssuesURL\n', wrap: false); |
| _logger.printStatus('To report your crash to the Flutter team, first read the guide to filing a bug.', emphasis: true); |
| _logger.printStatus('https://flutter.dev/docs/resources/bug-reports\n', wrap: false); |
| |
| _logger.printStatus('Create a new GitHub issue by pasting this link into your browser and completing the issue template. Thank you!', emphasis: true); |
| |
| final GitHubTemplateCreator gitHubTemplateCreator = GitHubTemplateCreator( |
| fileSystem: _fileSystem, |
| logger: _logger, |
| flutterProjectFactory: _flutterProjectFactory, |
| ); |
| |
| final String gitHubTemplateURL = await gitHubTemplateCreator.toolCrashIssueTemplateGitHubURL( |
| details.command, |
| details.error, |
| details.stackTrace, |
| await details.doctorText.piiStrippedText, |
| ); |
| _logger.printStatus('$gitHubTemplateURL\n', wrap: false); |
| } |
| } |
| |
| /// Sends crash reports to Google. |
| /// |
| /// To override the behavior of this class, define a |
| /// `FLUTTER_CRASH_SERVER_BASE_URL` environment variable that points to a custom |
| /// crash reporting server. This is useful if your development environment is |
| /// behind a firewall and unable to send crash reports to Google, or when you |
| /// wish to use your own server for collecting crash reports from Flutter Tools. |
| class CrashReportSender { |
| CrashReportSender({ |
| http.Client? client, |
| required Usage usage, |
| required Platform platform, |
| required Logger logger, |
| required OperatingSystemUtils operatingSystemUtils, |
| }) : _client = client ?? http.Client(), |
| _usage = usage, |
| _platform = platform, |
| _logger = logger, |
| _operatingSystemUtils = operatingSystemUtils; |
| |
| final http.Client _client; |
| final Usage _usage; |
| final Platform _platform; |
| final Logger _logger; |
| final OperatingSystemUtils _operatingSystemUtils; |
| |
| bool _crashReportSent = false; |
| |
| Uri get _baseUrl { |
| final String? overrideUrl = _platform.environment['FLUTTER_CRASH_SERVER_BASE_URL']; |
| |
| if (overrideUrl != null) { |
| return Uri.parse(overrideUrl); |
| } |
| return Uri( |
| scheme: 'https', |
| host: _kCrashServerHost, |
| port: 443, |
| path: _kCrashEndpointPath, |
| ); |
| } |
| |
| /// Sends one crash report. |
| /// |
| /// The report is populated from data in [error] and [stackTrace]. |
| Future<void> sendReport({ |
| required Object error, |
| required StackTrace stackTrace, |
| required String Function() getFlutterVersion, |
| required String command, |
| }) async { |
| // Only send one crash report per run. |
| if (_crashReportSent) { |
| return; |
| } |
| try { |
| final String flutterVersion = getFlutterVersion(); |
| |
| // We don't need to report exceptions happening on user branches |
| if (_usage.suppressAnalytics || RegExp(r'^\[user-branch\]\/').hasMatch(flutterVersion)) { |
| return; |
| } |
| |
| _logger.printTrace('Sending crash report to Google.'); |
| |
| final Uri uri = _baseUrl.replace( |
| queryParameters: <String, String>{ |
| 'product': _kProductId, |
| 'version': flutterVersion, |
| }, |
| ); |
| |
| final http.MultipartRequest req = http.MultipartRequest('POST', uri); |
| req.fields['uuid'] = _usage.clientId; |
| req.fields['product'] = _kProductId; |
| req.fields['version'] = flutterVersion; |
| req.fields['osName'] = _platform.operatingSystem; |
| req.fields['osVersion'] = _operatingSystemUtils.name; // this actually includes version |
| req.fields['type'] = _kDartTypeId; |
| req.fields['error_runtime_type'] = '${error.runtimeType}'; |
| req.fields['error_message'] = '$error'; |
| req.fields['comments'] = command; |
| |
| req.files.add(http.MultipartFile.fromString( |
| _kStackTraceFileField, |
| stackTrace.toString(), |
| filename: _kStackTraceFilename, |
| )); |
| |
| final http.StreamedResponse resp = await _client.send(req); |
| |
| if (resp.statusCode == HttpStatus.ok) { |
| final String reportId = await http.ByteStream(resp.stream) |
| .bytesToString(); |
| _logger.printTrace('Crash report sent (report ID: $reportId)'); |
| _crashReportSent = true; |
| } else { |
| _logger.printError('Failed to send crash report. Server responded with HTTP status code ${resp.statusCode}'); |
| } |
| |
| // Catch all exceptions to print the message that makes clear that the |
| // crash logger crashed. |
| } catch (sendError, sendStackTrace) { // ignore: avoid_catches_without_on_clauses |
| if (sendError is SocketException || sendError is HttpException || sendError is http.ClientException) { |
| _logger.printError('Failed to send crash report due to a network error: $sendError'); |
| } else { |
| // If the sender itself crashes, just print. We did our best. |
| _logger.printError('Crash report sender itself crashed: $sendError\n$sendStackTrace'); |
| } |
| } |
| } |
| } |