| // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| import 'dart:math' as math; |
| |
| import 'package:http/http.dart' as http; |
| import 'package:meta/meta.dart'; |
| import 'package:stack_trace/stack_trace.dart'; |
| |
| import 'src/utils.dart'; |
| |
| /// Tells crash backend that this is a Dart error (as opposed to, say, Java). |
| const String _dartTypeId = 'DartError'; |
| |
| /// Crash backend host. |
| const String _crashServerHost = 'clients2.google.com'; |
| |
| /// Path to the staging crash servlet. |
| const String _crashEndpointPathStaging = '/cr/staging_report'; |
| |
| /// Path to the prod crash servlet. |
| const String _crashEndpointPathProd = '/cr/report'; |
| |
| /// The field corresponding to the multipart/form-data file attachment where |
| /// crash backend expects to find the Dart stack trace. |
| const String _stackTraceFileField = 'DartError'; |
| |
| /// The name of the file attached as [_stackTraceFileField]. |
| /// |
| /// The precise value is not important. It is ignored by the crash back end, but |
| /// it must be supplied in the request. |
| const String _stackTraceFilename = 'stacktrace_file'; |
| |
| /// Sends crash reports to Google. |
| /// |
| /// Clients shouldn't extend, mixin or implement this class. |
| class CrashReportSender { |
| final Uri _baseUri; |
| |
| static const int _maxReportsToSend = 1000; |
| |
| final String crashProductId; |
| final EnablementCallback shouldSend; |
| final http.Client _httpClient; |
| final Stopwatch _processStopwatch = Stopwatch()..start(); |
| |
| final ThrottlingBucket _throttle = ThrottlingBucket(10, Duration(minutes: 1)); |
| int _reportsSent = 0; |
| int _skippedReports = 0; |
| |
| CrashReportSender._( |
| this.crashProductId, |
| this.shouldSend, { |
| http.Client? httpClient, |
| String endpointPath = _crashEndpointPathStaging, |
| }) : _httpClient = httpClient ?? http.Client(), |
| _baseUri = Uri( |
| scheme: 'https', host: _crashServerHost, path: endpointPath); |
| |
| /// Create a new [CrashReportSender] connected to the staging endpoint. |
| CrashReportSender.staging( |
| String crashProductId, |
| EnablementCallback shouldSend, { |
| http.Client? httpClient, |
| }) : this._(crashProductId, shouldSend, |
| httpClient: httpClient, endpointPath: _crashEndpointPathStaging); |
| |
| /// Create a new [CrashReportSender] connected to the prod endpoint. |
| CrashReportSender.prod( |
| String crashProductId, |
| EnablementCallback shouldSend, { |
| http.Client? httpClient, |
| }) : this._(crashProductId, shouldSend, |
| httpClient: httpClient, endpointPath: _crashEndpointPathProd); |
| |
| /// Sends one crash report. |
| /// |
| /// The report is populated from data in [error] and [stackTrace]. |
| /// |
| /// Additional context about the crash can optionally be passed in via |
| /// [comment]. Note that this field should not include PII. |
| Future sendReport( |
| dynamic error, |
| StackTrace stackTrace, { |
| List<CrashReportAttachment> attachments = const [], |
| String? comment, |
| }) async { |
| if (!shouldSend()) { |
| return; |
| } |
| |
| // Check if we've sent too many reports recently. |
| if (!_throttle.removeDrop()) { |
| _skippedReports++; |
| return; |
| } |
| |
| // Don't send too many total reports to crash reporting. |
| if (_reportsSent >= _maxReportsToSend) { |
| return; |
| } |
| |
| _reportsSent++; |
| |
| // Calculate the 'weight' of the this report; we increase the weight of a |
| // report if we had throttled previous reports. |
| int weight = math.min(_skippedReports + 1, 10000); |
| _skippedReports = 0; |
| |
| try { |
| final String dartVersion = Platform.version.split(' ').first; |
| |
| final Uri uri = _baseUri.replace( |
| queryParameters: <String, String>{ |
| 'product': crashProductId, |
| 'version': dartVersion, |
| }, |
| ); |
| |
| final http.MultipartRequest req = http.MultipartRequest('POST', uri); |
| |
| Map<String, String> fields = req.fields; |
| fields['product'] = crashProductId; |
| fields['version'] = dartVersion; |
| fields['osName'] = Platform.operatingSystem; |
| fields['osVersion'] = Platform.operatingSystemVersion; |
| fields['type'] = _dartTypeId; |
| fields['error_runtime_type'] = '${error.runtimeType}'; |
| fields['error_message'] = '$error'; |
| |
| // Optional comments. |
| if (comment != null) { |
| fields['comments'] = comment; |
| } |
| |
| // The uptime of the process before it crashed (in milliseconds). |
| fields['ptime'] = _processStopwatch.elapsedMilliseconds.toString(); |
| |
| // Send the amount to weight this report. |
| if (weight > 1) { |
| fields['weight'] = weight.toString(); |
| } |
| |
| final Chain chain = Chain.forTrace(stackTrace); |
| req.files.add( |
| http.MultipartFile.fromString( |
| _stackTraceFileField, |
| chain.terse.toString(), |
| filename: _stackTraceFilename, |
| ), |
| ); |
| |
| for (var attachment in attachments) { |
| req.files.add( |
| http.MultipartFile.fromString( |
| attachment._field, |
| attachment._value, |
| filename: attachment._field, |
| ), |
| ); |
| } |
| |
| final http.StreamedResponse resp = await _httpClient.send(req); |
| |
| if (resp.statusCode != 200) { |
| throw 'server responded with HTTP status code ${resp.statusCode}'; |
| } |
| } on SocketException catch (error) { |
| throw 'network error while sending crash report: $error'; |
| } catch (error, stackTrace) { |
| // If the sender itself crashes, just print. |
| throw 'exception while sending crash report: $error\n$stackTrace'; |
| } |
| } |
| |
| @visibleForTesting |
| int get reportsSent => _reportsSent; |
| |
| /// Closes the client and cleans up any resources associated with it. This |
| /// will close the associated [http.Client]. |
| void dispose() { |
| _httpClient.close(); |
| } |
| } |
| |
| /// The additional attachment to be added to a crash report. |
| class CrashReportAttachment { |
| final String _field; |
| final String _value; |
| |
| CrashReportAttachment.string({ |
| required String field, |
| required String value, |
| }) : _field = field, |
| _value = value; |
| } |
| |
| /// A typedef to allow crash reporting to query as to whether it should send a |
| /// crash report. |
| typedef EnablementCallback = bool Function(); |