// 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 = new 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 ?? new http.Client(), _baseUri = new 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 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: { 'product': crashProductId, 'version': dartVersion, }, ); final http.MultipartRequest req = new http.MultipartRequest('POST', uri); Map 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 = new Chain.forTrace(stackTrace); req.files.add( new http.MultipartFile.fromString( _stackTraceFileField, chain.terse.toString(), filename: _stackTraceFilename, ), ); for (var attachment in attachments) { req.files.add( new 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 bool EnablementCallback();