Commit 3ef99092 authored by Yegor's avatar Yegor Committed by GitHub

enable crash reporting in flutter_tools (#9039)

* enable crash reporting in flutter_tools

* fix analytics text; use relative paths

* fix test
parent ab6df3af
......@@ -6,8 +6,11 @@ import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:stack_trace/stack_trace.dart';
import 'base/io.dart';
import 'base/os.dart';
import 'base/platform.dart';
import 'globals.dart';
import 'usage.dart';
......@@ -21,8 +24,7 @@ const String _kDartTypeId = 'DartError';
const String _kCrashServerHost = 'clients2.google.com';
/// Path to the crash servlet.
// TODO(yjbanov): switch to non-staging when production is ready.
const String _kCrashEndpointPath = '/cr/staging_report';
const String _kCrashEndpointPath = '/cr/report';
/// The field corresponding to the multipart/form-data file attachment where
/// crash backend expects to find the Dart stack trace.
......@@ -34,11 +36,6 @@ const String _kStackTraceFileField = 'DartError';
/// it must be supplied in the request.
const String _kStackTraceFilename = 'stacktrace_file';
/// We only send crash reports in testing mode.
///
/// See [enterTestingMode] and [exitTestingMode].
bool _testing = false;
/// Sends crash reports to Google.
class CrashReportSender {
static final Uri _baseUri = new Uri(
......@@ -72,12 +69,6 @@ class CrashReportSender {
@required String getFlutterVersion(),
}) async {
try {
// TODO(yjbanov): we only actually send crash reports in tests. When we
// iron out the process, we will remove this guard and report crashes
// when !flutterUsage.suppressAnalytics.
if (!_testing)
return null;
if (_usage.suppressAnalytics)
return null;
......@@ -92,14 +83,18 @@ class CrashReportSender {
);
final http.MultipartRequest req = new http.MultipartRequest('POST', uri);
req.fields['uuid'] = _usage.clientId;
req.fields['product'] = _kProductId;
req.fields['version'] = flutterVersion;
req.fields['osName'] = platform.operatingSystem;
req.fields['osVersion'] = os.name; // this actually includes version
req.fields['type'] = _kDartTypeId;
req.fields['error_runtime_type'] = '${error.runtimeType}';
final String stackTraceWithRelativePaths = new Chain.parse(stackTrace.toString()).terse.toString();
req.files.add(new http.MultipartFile.fromString(
_kStackTraceFileField,
stackTrace.toString(),
stackTraceWithRelativePaths,
filename: _kStackTraceFilename,
));
......@@ -122,15 +117,3 @@ class CrashReportSender {
}
}
}
/// Enables testing mode.
@visibleForTesting
void enterTestingMode() {
_testing = true;
}
/// Disables testing mode.
@visibleForTesting
void exitTestingMode() {
_testing = false;
}
......@@ -66,6 +66,10 @@ class Usage {
_analytics.enabled = value;
}
/// A stable randomly generated UUID used to deduplicate multiple identical
/// reports coming from the same computer.
String get clientId => _analytics.clientId;
void sendCommand(String command) {
if (!suppressAnalytics)
_analytics.sendScreenView(command);
......@@ -114,12 +118,18 @@ class Usage {
╔════════════════════════════════════════════════════════════════════════════╗
║ Welcome to Flutter! - https://flutter.io ║
║ ║
║ The Flutter tool anonymously reports feature usage statistics and basic ║
║ crash reports to Google in order to help Google contribute improvements to ║
║ Flutter over time. See Google's privacy policy:
║ The Flutter tool anonymously reports feature usage statistics and crash ║
║ reports to Google in order to help Google contribute improvements to ║
║ Flutter over time. ║
║ ║
║ Read about data we send with crash reports: ║
║ https://github.com/flutter/flutter/wiki/Flutter-CLI-crash-reporting ║
║ ║
║ See Google's privacy policy:
https://www.google.com/intl/en/policies/privacy/ ║
Use "flutter config --no-analytics" to disable analytics reporting.
Use "flutter config --no-analytics" to disable analytics and crash
reporting.
╚════════════════════════════════════════════════════════════════════════════╝
''', emphasis: true);
}
......
......@@ -24,7 +24,7 @@ dependencies:
platform: 1.1.1
process: 2.0.1
stack_trace: ^1.4.0
usage: ^3.0.0+1
usage: ^3.0.1
vm_service_client: '0.2.2+4'
web_socket_channel: ^1.0.4
xml: ^2.4.1
......
......@@ -3,10 +3,12 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:http/http.dart';
import 'package:http/testing.dart';
import 'package:test/test.dart';
......@@ -30,23 +32,42 @@ void main() {
tools.crashFileSystem = new MemoryFileSystem();
tools.writelnStderr = ([_]) { };
setExitFunctionForTests((_) { });
enterTestingMode();
});
tearDown(() {
tools.crashFileSystem = const LocalFileSystem();
tools.writelnStderr = stderr.writeln;
restoreExitFunction();
exitTestingMode();
});
testUsingContext('should send crash reports', () async {
String method;
Uri uri;
Map<String, String> fields;
CrashReportSender.initializeWith(new MockClient((Request request) async {
method = request.method;
uri = request.url;
// A very ad-hoc multipart request parser. Good enough for this test.
String boundary = request.headers['Content-Type'];
boundary = boundary.substring(boundary.indexOf('boundary=') + 9);
fields = new Map<String, String>.fromIterable(
UTF8.decode(request.bodyBytes)
.split('--$boundary')
.map<List<String>>((String part) {
final Match nameMatch = new RegExp(r'name="(.*)"').firstMatch(part);
if (nameMatch == null)
return null;
final String name = nameMatch[1];
final String value = part.split('\n').skip(2).join('\n').trim();
return <String>[name, value];
})
.where((List<String> pair) => pair != null),
key: (List<String> pair) => pair[0],
value: (List<String> pair) => pair[1],
);
return new Response(
'test-report-id',
200
......@@ -68,12 +89,20 @@ void main() {
scheme: 'https',
host: 'clients2.google.com',
port: 443,
path: '/cr/staging_report',
path: '/cr/report',
queryParameters: <String, String>{
'product': 'Flutter_Tools',
'version' : 'test-version',
},
));
expect(fields['uuid'], '00000000-0000-4000-0000-000000000000');
expect(fields['product'], 'Flutter_Tools');
expect(fields['version'], 'test-version');
expect(fields['osName'], platform.operatingSystem);
expect(fields['osVersion'], 'fake OS name and version');
expect(fields['type'], 'DartError');
expect(fields['error_runtime_type'], 'StateError');
final BufferLogger logger = context[Logger];
expect(logger.statusText, 'Sending crash report to Google.\n'
'Crash report sent (report ID: test-report-id)\n');
......
......@@ -170,6 +170,9 @@ class MockSimControl extends Mock implements SimControl {
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {
@override
List<File> whichAll(String execName) => <File>[];
@override
String get name => 'fake OS name and version';
}
class MockIOSSimulatorUtils extends Mock implements IOSSimulatorUtils {}
......@@ -190,6 +193,9 @@ class MockUsage implements Usage {
@override
set enabled(bool value) { }
@override
String get clientId => '00000000-0000-4000-0000-000000000000';
@override
void sendCommand(String command) { }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment