crash_reporting.dart 4.63 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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

/// Sends crash reports to Google.
30
///
31
/// There are two ways to override the behavior of this class:
32
///
33 34 35 36 37 38 39
/// * 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.
/// * In tests call [initializeWith] and provide a mock implementation of
///   [http.Client].
40 41 42
class CrashReportSender {
  CrashReportSender._(this._client);

43 44
  static CrashReportSender _instance;

45
  static CrashReportSender get instance => _instance ?? CrashReportSender._(http.Client());
46

47 48
  bool _crashReportSent = false;

49 50 51
  /// Overrides the default [http.Client] with [client] for testing purposes.
  @visibleForTesting
  static void initializeWith(http.Client client) {
52
    _instance = CrashReportSender._(client);
53 54 55 56 57
  }

  final http.Client _client;
  final Usage _usage = Usage.instance;

58 59 60 61 62 63
  Uri get _baseUrl {
    final String overrideUrl = platform.environment['FLUTTER_CRASH_SERVER_BASE_URL'];

    if (overrideUrl != null) {
      return Uri.parse(overrideUrl);
    }
64
    return Uri(
65 66 67 68 69 70 71
      scheme: 'https',
      host: _kCrashServerHost,
      port: 443,
      path: _kCrashEndpointPath,
    );
  }

72 73 74
  /// Sends one crash report.
  ///
  /// The report is populated from data in [error] and [stackTrace].
75
  Future<void> sendReport({
76
    @required dynamic error,
77
    @required StackTrace stackTrace,
78
    @required String getFlutterVersion(),
79
    @required String command,
80
  }) async {
81 82 83 84
    // Only send one crash report per run.
    if (_crashReportSent) {
      return;
    }
85
    try {
86 87 88 89
      final String flutterVersion = getFlutterVersion();

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

      printStatus('Sending crash report to Google.');

95
      final Uri uri = _baseUrl.replace(
96 97 98 99 100 101
        queryParameters: <String, String>{
          'product': _kProductId,
          'version': flutterVersion,
        },
      );

102
      final http.MultipartRequest req = http.MultipartRequest('POST', uri);
103
      req.fields['uuid'] = _usage.clientId;
104 105
      req.fields['product'] = _kProductId;
      req.fields['version'] = flutterVersion;
106
      req.fields['osName'] = platform.operatingSystem;
107
      req.fields['osVersion'] = os.name; // this actually includes version
108 109
      req.fields['type'] = _kDartTypeId;
      req.fields['error_runtime_type'] = '${error.runtimeType}';
110
      req.fields['error_message'] = '$error';
111
      req.fields['comments'] = command;
112

113
      req.files.add(http.MultipartFile.fromString(
114
        _kStackTraceFileField,
115
        stackTrace.toString(),
116 117 118
        filename: _kStackTraceFilename,
      ));

119
      final http.StreamedResponse resp = await _client.send(req);
120 121

      if (resp.statusCode == 200) {
122
        final String reportId = await http.ByteStream(resp.stream)
123 124
            .bytesToString();
        printStatus('Crash report sent (report ID: $reportId)');
125
        _crashReportSent = true;
126 127 128 129
      } else {
        printError('Failed to send crash report. Server responded with HTTP status code ${resp.statusCode}');
      }
    } catch (sendError, sendStackTrace) {
130
      if (sendError is SocketException || sendError is HttpException) {
131 132 133 134 135 136 137 138
        printError('Failed to send crash report due to a network error: $sendError');
      } else {
        // If the sender itself crashes, just print. We did our best.
        printError('Crash report sender itself crashed: $sendError\n$sendStackTrace');
      }
    }
  }
}