github_template.dart 6.62 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
part of reporting;
6 7 8

/// Provide suggested GitHub issue templates to user when Flutter encounters an error.
class GitHubTemplateCreator {
9 10 11 12 13 14 15 16 17 18 19 20 21
  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;
22 23
  final HttpClient _client;

24 25
  static String toolCrashSimilarIssuesURL(String errorString) {
    return 'https://github.com/flutter/flutter/issues?q=is%3Aissue+${Uri.encodeQueryComponent(errorString)}';
26 27
  }

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
  /// 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();
  }

63 64 65 66 67
  /// GitHub URL to present to the user containing encoded suggested template.
  ///
  /// Shorten the URL, if possible.
  Future<String> toolCrashIssueTemplateGitHubURL(
      String command,
68
      dynamic error,
69 70 71
      StackTrace stackTrace,
      String doctorText
    ) async {
72
    final String errorString = sanitizedCrashException(error);
73
    final String title = '[tool_crash] $errorString';
74 75
    final String body = '''
## Command
76 77 78
```
$command
```
79

80 81 82 83
## Steps to Reproduce
1. ...
2. ...
3. ...
84

85
## Logs
86
$errorString
87
```
88
${LineSplitter.split(stackTrace.toString()).take(25).join('\n')}
89 90 91 92
```
```
$doctorText
```
93

94 95 96
## Flutter Application Metadata
${_projectMetadataInformation()}
''';
97 98 99 100 101 102 103 104 105 106 107 108 109

    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 {
110
      project = _flutterProjectFactory.fromDirectory(_fileSystem.currentDirectory);
111 112 113 114 115 116 117 118 119
    } 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.';
      }
120
      final FlutterProjectMetadata metadata = FlutterProjectMetadata(project.metadataFile, _logger);
121
      final StringBuffer description = StringBuffer()
122
        ..writeln('**Type**: ${metadata.projectType?.name}')
123 124 125 126 127 128
        ..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}')
129 130 131
        ..writeln('**iOS bundle identifier**: ${manifest.iosBundleIdentifier}')
        ..writeln('**Creation channel**: ${metadata.versionChannel}')
        ..writeln('**Creation framework version**: ${metadata.versionRevision}');
132

133 134
      final File file = project.flutterPluginsFile;
      if (file.existsSync()) {
135 136 137
        description.writeln('### Plugins');
        // Format is:
        // camera=/path/to/.pub-cache/hosted/pub.dartlang.org/camera-0.5.7+2/
138
        for (final String plugin in project.flutterPluginsFile.readAsLinesSync()) {
139 140 141 142
          final List<String> pluginParts = plugin.split('=');
          if (pluginParts.length != 2) {
            continue;
          }
143 144
          // Write the last part of the path, which includes the plugin name and version.
          // Example: camera-0.5.7+2
145
          final List<String> pathParts = _fileSystem.path.split(pluginParts[1]);
146
          description.writeln(pathParts.isEmpty ? pluginParts.first : pathParts.last);
147 148 149
        }
      }

150
      return description.toString();
151 152 153 154 155 156 157 158 159 160 161
    } 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 {
162
      _logger.printTrace('Attempting git.io shortener: $fullURL');
163 164 165 166 167 168 169 170 171
      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 {
172
        _logger.printTrace('Failed to shorten GitHub template URL. Server responded with HTTP status code ${response.statusCode}');
173
      }
174
    } on Exception catch (sendError) {
175
      _logger.printTrace('Failed to shorten GitHub template URL: $sendError');
176 177 178 179 180
    }

    return url ?? fullURL;
  }
}