blob: 5ec776f49a99e062160109436ca7b892794948f9 [file] [log] [blame]
// 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();