crash_reporting.dart 7.17 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 6 7 8 9 10 11 12 13 14
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';
15
import '../doctor.dart';
16 17 18
import '../project.dart';
import 'github_template.dart';
import 'reporting.dart';
19 20

/// Tells crash backend that the error is from the Flutter CLI.
21
const String _kProductId = 'Flutter_Tools';
22 23

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

/// Crash backend host.
27
const String _kCrashServerHost = 'clients2.google.com';
28 29

/// Path to the crash servlet.
30
const String _kCrashEndpointPath = '/cr/report';
31 32 33

/// The field corresponding to the multipart/form-data file attachment where
/// crash backend expects to find the Dart stack trace.
34
const String _kStackTraceFileField = 'DartError';
35 36 37 38 39

/// 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.
40
const String _kStackTraceFilename = 'stacktrace_file';
41

42 43
class CrashDetails {
  CrashDetails({
44 45 46 47
    required this.command,
    required this.error,
    required this.stackTrace,
    required this.doctorText,
48 49 50
  });

  final String command;
51
  final Object error;
52
  final StackTrace stackTrace;
53
  final DoctorText doctorText;
54 55 56 57 58
}

/// Reports information about the crash to the user.
class CrashReporter {
  CrashReporter({
59 60 61
    required FileSystem fileSystem,
    required Logger logger,
    required FlutterProjectFactory flutterProjectFactory,
62 63
  }) : _fileSystem = fileSystem,
       _logger = logger,
64
       _flutterProjectFactory = flutterProjectFactory;
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91

  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,
92
      await details.doctorText.piiStrippedText,
93 94 95 96 97
    );
    _logger.printStatus('$gitHubTemplateURL\n', wrap: false);
  }
}

98
/// Sends crash reports to Google.
99
///
100 101 102 103 104
/// 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.
105
class CrashReportSender {
106
  CrashReportSender({
107 108 109 110 111
    http.Client? client,
    required Usage usage,
    required Platform platform,
    required Logger logger,
    required OperatingSystemUtils operatingSystemUtils,
112
  }) : _client = client ?? http.Client(),
113 114 115 116
      _usage = usage,
      _platform = platform,
      _logger = logger,
      _operatingSystemUtils = operatingSystemUtils;
117

118 119 120 121 122
  final http.Client _client;
  final Usage _usage;
  final Platform _platform;
  final Logger _logger;
  final OperatingSystemUtils _operatingSystemUtils;
123

124 125
  bool _crashReportSent = false;

126
  Uri get _baseUrl {
127
    final String? overrideUrl = _platform.environment['FLUTTER_CRASH_SERVER_BASE_URL'];
128 129 130 131

    if (overrideUrl != null) {
      return Uri.parse(overrideUrl);
    }
132
    return Uri(
133 134 135 136 137 138 139
      scheme: 'https',
      host: _kCrashServerHost,
      port: 443,
      path: _kCrashEndpointPath,
    );
  }

140 141 142
  /// Sends one crash report.
  ///
  /// The report is populated from data in [error] and [stackTrace].
143
  Future<void> sendReport({
144 145 146 147
    required Object error,
    required StackTrace stackTrace,
    required String Function() getFlutterVersion,
    required String command,
148
  }) async {
149 150 151 152
    // Only send one crash report per run.
    if (_crashReportSent) {
      return;
    }
153
    try {
154 155 156 157
      final String flutterVersion = getFlutterVersion();

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

161
      _logger.printTrace('Sending crash report to Google.');
162

163
      final Uri uri = _baseUrl.replace(
164 165 166 167 168 169
        queryParameters: <String, String>{
          'product': _kProductId,
          'version': flutterVersion,
        },
      );

170
      final http.MultipartRequest req = http.MultipartRequest('POST', uri);
171
      req.fields['uuid'] = _usage.clientId;
172 173
      req.fields['product'] = _kProductId;
      req.fields['version'] = flutterVersion;
174 175
      req.fields['osName'] = _platform.operatingSystem;
      req.fields['osVersion'] = _operatingSystemUtils.name; // this actually includes version
176 177
      req.fields['type'] = _kDartTypeId;
      req.fields['error_runtime_type'] = '${error.runtimeType}';
178
      req.fields['error_message'] = '$error';
179
      req.fields['comments'] = command;
180

181
      req.files.add(http.MultipartFile.fromString(
182
        _kStackTraceFileField,
183
        stackTrace.toString(),
184 185 186
        filename: _kStackTraceFilename,
      ));

187 188 189 190 191 192 193 194 195 196 197
      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}');
      }

198 199 200
    // Catch all exceptions to print the message that makes clear that the
    // crash logger crashed.
    } catch (sendError, sendStackTrace) { // ignore: avoid_catches_without_on_clauses
201
      if (sendError is SocketException || sendError is HttpException || sendError is http.ClientException) {
202
        _logger.printError('Failed to send crash report due to a network error: $sendError');
203 204
      } else {
        // If the sender itself crashes, just print. We did our best.
205
        _logger.printError('Crash report sender itself crashed: $sendError\n$sendStackTrace');
206 207 208 209
      }
    }
  }
}