mirror of
https://github.com/dart-lang/sdk
synced 2024-09-16 03:27:43 +00:00
b8f4b252ef
Change-Id: I75130cc8d5964ef0f95a672858da8bbce8ffd78c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/200520 Commit-Queue: Devon Carew <devoncarew@google.com> Reviewed-by: Phil Quitslund <pquitslund@google.com>
204 lines
6.2 KiB
Dart
204 lines
6.2 KiB
Dart
// 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();
|