183 lines
6.7 KiB
Dart
183 lines
6.7 KiB
Dart
// 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.
|
|
|
|
// @dart = 2.8
|
|
|
|
part of reporting;
|
|
|
|
/// Provide suggested GitHub issue templates to user when Flutter encounters an error.
|
|
class GitHubTemplateCreator {
|
|
GitHubTemplateCreator({
|
|
@required FileSystem fileSystem,
|
|
@required Logger logger,
|
|
@required FlutterProjectFactory flutterProjectFactory,
|
|
@required HttpClient client,
|
|
}) : _fileSystem = fileSystem,
|
|
_logger = logger,
|
|
_flutterProjectFactory = flutterProjectFactory,
|
|
_client = client;
|
|
|
|
final FileSystem _fileSystem;
|
|
final Logger _logger;
|
|
final FlutterProjectFactory _flutterProjectFactory;
|
|
final HttpClient _client;
|
|
|
|
static String toolCrashSimilarIssuesURL(String errorString) {
|
|
return 'https://github.com/flutter/flutter/issues?q=is%3Aissue+${Uri.encodeQueryComponent(errorString)}';
|
|
}
|
|
|
|
/// Restricts exception object strings to contain only information about tool internals.
|
|
static String sanitizedCrashException(dynamic error) {
|
|
if (error is ProcessException) {
|
|
// Suppress args.
|
|
return 'ProcessException: ${error.message} Command: ${error.executable}, OS error code: ${error.errorCode}';
|
|
} else if (error is FileSystemException) {
|
|
// Suppress path.
|
|
return 'FileSystemException: ${error.message}, ${error.osError}';
|
|
} else if (error is SocketException) {
|
|
// Suppress address and port.
|
|
return 'SocketException: ${error.message}, ${error.osError}';
|
|
} else if (error is DevFSException) {
|
|
// Suppress underlying error.
|
|
return 'DevFSException: ${error.message}';
|
|
} else if (error is NoSuchMethodError
|
|
|| error is ArgumentError
|
|
|| error is VersionCheckError
|
|
|| error is MissingDefineException
|
|
|| error is UnsupportedError
|
|
|| error is UnimplementedError
|
|
|| error is StateError
|
|
|| error is ProcessExit
|
|
|| error is OSError) {
|
|
// These exception objects only reference tool internals, print the whole error.
|
|
return '${error.runtimeType}: $error';
|
|
} else if (error is Error) {
|
|
return '${error.runtimeType}: ${LineSplitter.split(error.stackTrace.toString()).take(1)}';
|
|
} else if (error is String) {
|
|
// Force comma separator to standardize.
|
|
return 'String: <${NumberFormat(null, 'en_US').format(error.length)} characters>';
|
|
}
|
|
// Exception, other.
|
|
return error.runtimeType.toString();
|
|
}
|
|
|
|
/// GitHub URL to present to the user containing encoded suggested template.
|
|
///
|
|
/// Shorten the URL, if possible.
|
|
Future<String> toolCrashIssueTemplateGitHubURL(
|
|
String command,
|
|
dynamic error,
|
|
StackTrace stackTrace,
|
|
String doctorText
|
|
) async {
|
|
final String errorString = sanitizedCrashException(error);
|
|
final String title = '[tool_crash] $errorString';
|
|
final String body = '''
|
|
## Command
|
|
```
|
|
$command
|
|
```
|
|
|
|
## Steps to Reproduce
|
|
1. ...
|
|
2. ...
|
|
3. ...
|
|
|
|
## Logs
|
|
$errorString
|
|
```
|
|
${LineSplitter.split(stackTrace.toString()).take(25).join('\n')}
|
|
```
|
|
```
|
|
$doctorText
|
|
```
|
|
|
|
## Flutter Application Metadata
|
|
${_projectMetadataInformation()}
|
|
''';
|
|
|
|
final String fullURL = 'https://github.com/flutter/flutter/issues/new?'
|
|
'title=${Uri.encodeQueryComponent(title)}'
|
|
'&body=${Uri.encodeQueryComponent(body)}'
|
|
'&labels=${Uri.encodeQueryComponent('tool,severe: crash')}';
|
|
|
|
return await _shortURL(fullURL);
|
|
}
|
|
|
|
/// Provide information about the Flutter project in the working directory, if present.
|
|
String _projectMetadataInformation() {
|
|
FlutterProject project;
|
|
try {
|
|
project = _flutterProjectFactory.fromDirectory(_fileSystem.currentDirectory);
|
|
} on Exception catch (exception) {
|
|
// pubspec may be malformed.
|
|
return exception.toString();
|
|
}
|
|
try {
|
|
final FlutterManifest manifest = project?.manifest;
|
|
if (project == null || manifest == null || manifest.isEmpty) {
|
|
return 'No pubspec in working directory.';
|
|
}
|
|
final FlutterProjectMetadata metadata = FlutterProjectMetadata(project.metadataFile, _logger);
|
|
final StringBuffer description = StringBuffer()
|
|
..writeln('**Type**: ${flutterProjectTypeToString(metadata.projectType)}')
|
|
..writeln('**Version**: ${manifest.appVersion}')
|
|
..writeln('**Material**: ${manifest.usesMaterialDesign}')
|
|
..writeln('**Android X**: ${manifest.usesAndroidX}')
|
|
..writeln('**Module**: ${manifest.isModule}')
|
|
..writeln('**Plugin**: ${manifest.isPlugin}')
|
|
..writeln('**Android package**: ${manifest.androidPackage}')
|
|
..writeln('**iOS bundle identifier**: ${manifest.iosBundleIdentifier}')
|
|
..writeln('**Creation channel**: ${metadata.versionChannel}')
|
|
..writeln('**Creation framework version**: ${metadata.versionRevision}');
|
|
|
|
final File file = project.flutterPluginsFile;
|
|
if (file.existsSync()) {
|
|
description.writeln('### Plugins');
|
|
// Format is:
|
|
// camera=/path/to/.pub-cache/hosted/pub.dartlang.org/camera-0.5.7+2/
|
|
for (final String plugin in project.flutterPluginsFile.readAsLinesSync()) {
|
|
final List<String> pluginParts = plugin.split('=');
|
|
if (pluginParts.length != 2) {
|
|
continue;
|
|
}
|
|
// Write the last part of the path, which includes the plugin name and version.
|
|
// Example: camera-0.5.7+2
|
|
final List<String> pathParts = _fileSystem.path.split(pluginParts[1]);
|
|
description.writeln(pathParts.isEmpty ? pluginParts.first : pathParts.last);
|
|
}
|
|
}
|
|
|
|
return description.toString();
|
|
} on Exception catch (exception) {
|
|
return exception.toString();
|
|
}
|
|
}
|
|
|
|
/// Shorten GitHub URL with git.io API.
|
|
///
|
|
/// See https://github.blog/2011-11-10-git-io-github-url-shortener.
|
|
Future<String> _shortURL(String fullURL) async {
|
|
String url;
|
|
try {
|
|
_logger.printTrace('Attempting git.io shortener: $fullURL');
|
|
final List<int> bodyBytes = utf8.encode('url=${Uri.encodeQueryComponent(fullURL)}');
|
|
final HttpClientRequest request = await _client.postUrl(Uri.parse('https://git.io'));
|
|
request.headers.set(HttpHeaders.contentLengthHeader, bodyBytes.length.toString());
|
|
request.add(bodyBytes);
|
|
final HttpClientResponse response = await request.close();
|
|
|
|
if (response.statusCode == 201) {
|
|
url = response.headers[HttpHeaders.locationHeader]?.first;
|
|
} else {
|
|
_logger.printTrace('Failed to shorten GitHub template URL. Server responded with HTTP status code ${response.statusCode}');
|
|
}
|
|
} on Exception catch (sendError) {
|
|
_logger.printTrace('Failed to shorten GitHub template URL: $sendError');
|
|
}
|
|
|
|
return url ?? fullURL;
|
|
}
|
|
}
|