crash_reporting.dart 6.88 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

/// Tells crash backend that the error is from the Flutter CLI.
8
const String _kProductId = 'Flutter_Tools';
9 10

/// Tells crash backend that this is a Dart error as opposed to, say, Java.
11
const String _kDartTypeId = 'DartError';
12 13

/// Crash backend host.
14
const String _kCrashServerHost = 'clients2.google.com';
15 16

/// Path to the crash servlet.
17
const String _kCrashEndpointPath = '/cr/report';
18 19 20

/// The field corresponding to the multipart/form-data file attachment where
/// crash backend expects to find the Dart stack trace.
21
const String _kStackTraceFileField = 'DartError';
22 23 24 25 26

/// 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.
27
const String _kStackTraceFilename = 'stacktrace_file';
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
class CrashDetails {
  CrashDetails({
    @required this.command,
    @required this.error,
    @required this.stackTrace,
    @required this.doctorText,
  });

  final String command;
  final dynamic error;
  final StackTrace stackTrace;
  final String doctorText;
}

/// Reports information about the crash to the user.
class CrashReporter {
  CrashReporter({
    @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;

  /// 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,
      client: _client,
    );

    final String gitHubTemplateURL = await gitHubTemplateCreator.toolCrashIssueTemplateGitHubURL(
      details.command,
      details.error,
      details.stackTrace,
      details.doctorText,
    );
    _logger.printStatus('$gitHubTemplateURL\n', wrap: false);
  }
}

89
/// Sends crash reports to Google.
90
///
91 92 93 94 95
/// 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.
96
class CrashReportSender {
97 98 99 100 101 102 103 104 105 106 107
  CrashReportSender({
    @required http.Client client,
    @required Usage usage,
    @required Platform platform,
    @required Logger logger,
    @required OperatingSystemUtils operatingSystemUtils,
  }) : _client = client,
      _usage = usage,
      _platform = platform,
      _logger = logger,
      _operatingSystemUtils = operatingSystemUtils;
108

109 110 111 112 113
  final http.Client _client;
  final Usage _usage;
  final Platform _platform;
  final Logger _logger;
  final OperatingSystemUtils _operatingSystemUtils;
114

115 116
  bool _crashReportSent = false;

117
  Uri get _baseUrl {
118
    final String overrideUrl = _platform.environment['FLUTTER_CRASH_SERVER_BASE_URL'];
119 120 121 122

    if (overrideUrl != null) {
      return Uri.parse(overrideUrl);
    }
123
    return Uri(
124 125 126 127 128 129 130
      scheme: 'https',
      host: _kCrashServerHost,
      port: 443,
      path: _kCrashEndpointPath,
    );
  }

131 132 133
  /// Sends one crash report.
  ///
  /// The report is populated from data in [error] and [stackTrace].
134
  Future<void> sendReport({
135
    @required dynamic error,
136
    @required StackTrace stackTrace,
137
    @required String getFlutterVersion(),
138
    @required String command,
139
  }) async {
140 141 142 143
    // Only send one crash report per run.
    if (_crashReportSent) {
      return;
    }
144
    try {
145 146 147 148
      final String flutterVersion = getFlutterVersion();

      // We don't need to report exceptions happening on user branches
      if (_usage.suppressAnalytics || RegExp(r'^\[user-branch\]\/').hasMatch(flutterVersion)) {
149
        return;
150
      }
151

152
      _logger.printTrace('Sending crash report to Google.');
153

154
      final Uri uri = _baseUrl.replace(
155 156 157 158 159 160
        queryParameters: <String, String>{
          'product': _kProductId,
          'version': flutterVersion,
        },
      );

161
      final http.MultipartRequest req = http.MultipartRequest('POST', uri);
162
      req.fields['uuid'] = _usage.clientId;
163 164
      req.fields['product'] = _kProductId;
      req.fields['version'] = flutterVersion;
165 166
      req.fields['osName'] = _platform.operatingSystem;
      req.fields['osVersion'] = _operatingSystemUtils.name; // this actually includes version
167 168
      req.fields['type'] = _kDartTypeId;
      req.fields['error_runtime_type'] = '${error.runtimeType}';
169
      req.fields['error_message'] = '$error';
170
      req.fields['comments'] = command;
171

172
      req.files.add(http.MultipartFile.fromString(
173
        _kStackTraceFileField,
174
        stackTrace.toString(),
175 176 177
        filename: _kStackTraceFilename,
      ));

178
      final http.StreamedResponse resp = await _client.send(req);
179 180

      if (resp.statusCode == 200) {
181
        final String reportId = await http.ByteStream(resp.stream)
182 183
          .bytesToString();
        _logger.printTrace('Crash report sent (report ID: $reportId)');
184
        _crashReportSent = true;
185
      } else {
186
        _logger.printError('Failed to send crash report. Server responded with HTTP status code ${resp.statusCode}');
187
      }
188 189 190
    // Catch all exceptions to print the message that makes clear that the
    // crash logger crashed.
    } catch (sendError, sendStackTrace) { // ignore: avoid_catches_without_on_clauses
191
      if (sendError is SocketException || sendError is HttpException) {
192
        _logger.printError('Failed to send crash report due to a network error: $sendError');
193 194
      } else {
        // If the sender itself crashes, just print. We did our best.
195
        _logger.printError('Crash report sender itself crashed: $sendError\n$sendStackTrace');
196 197 198 199
      }
    }
  }
}