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'; ...@@ -6,8 +6,11 @@ import 'dart:async';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:stack_trace/stack_trace.dart';
import 'base/io.dart'; import 'base/io.dart';
import 'base/os.dart';
import 'base/platform.dart';
import 'globals.dart'; import 'globals.dart';
import 'usage.dart'; import 'usage.dart';
...@@ -21,8 +24,7 @@ const String _kDartTypeId = 'DartError'; ...@@ -21,8 +24,7 @@ const String _kDartTypeId = 'DartError';
const String _kCrashServerHost = 'clients2.google.com'; const String _kCrashServerHost = 'clients2.google.com';
/// Path to the crash servlet. /// Path to the crash servlet.
// TODO(yjbanov): switch to non-staging when production is ready. const String _kCrashEndpointPath = '/cr/report';
const String _kCrashEndpointPath = '/cr/staging_report';
/// The field corresponding to the multipart/form-data file attachment where /// The field corresponding to the multipart/form-data file attachment where
/// crash backend expects to find the Dart stack trace. /// crash backend expects to find the Dart stack trace.
...@@ -34,11 +36,6 @@ const String _kStackTraceFileField = 'DartError'; ...@@ -34,11 +36,6 @@ const String _kStackTraceFileField = 'DartError';
/// it must be supplied in the request. /// it must be supplied in the request.
const String _kStackTraceFilename = 'stacktrace_file'; 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. /// Sends crash reports to Google.
class CrashReportSender { class CrashReportSender {
static final Uri _baseUri = new Uri( static final Uri _baseUri = new Uri(
...@@ -72,12 +69,6 @@ class CrashReportSender { ...@@ -72,12 +69,6 @@ class CrashReportSender {
@required String getFlutterVersion(), @required String getFlutterVersion(),
}) async { }) async {
try { 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) if (_usage.suppressAnalytics)
return null; return null;
...@@ -92,14 +83,18 @@ class CrashReportSender { ...@@ -92,14 +83,18 @@ class CrashReportSender {
); );
final http.MultipartRequest req = new http.MultipartRequest('POST', uri); final http.MultipartRequest req = new http.MultipartRequest('POST', uri);
req.fields['uuid'] = _usage.clientId;
req.fields['product'] = _kProductId; req.fields['product'] = _kProductId;
req.fields['version'] = flutterVersion; req.fields['version'] = flutterVersion;
req.fields['osName'] = platform.operatingSystem;
req.fields['osVersion'] = os.name; // this actually includes version
req.fields['type'] = _kDartTypeId; req.fields['type'] = _kDartTypeId;
req.fields['error_runtime_type'] = '${error.runtimeType}'; req.fields['error_runtime_type'] = '${error.runtimeType}';
final String stackTraceWithRelativePaths = new Chain.parse(stackTrace.toString()).terse.toString();
req.files.add(new http.MultipartFile.fromString( req.files.add(new http.MultipartFile.fromString(
_kStackTraceFileField, _kStackTraceFileField,
stackTrace.toString(), stackTraceWithRelativePaths,
filename: _kStackTraceFilename, filename: _kStackTraceFilename,
)); ));
...@@ -122,15 +117,3 @@ class CrashReportSender { ...@@ -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 { ...@@ -66,6 +66,10 @@ class Usage {
_analytics.enabled = value; _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) { void sendCommand(String command) {
if (!suppressAnalytics) if (!suppressAnalytics)
_analytics.sendScreenView(command); _analytics.sendScreenView(command);
...@@ -114,12 +118,18 @@ class Usage { ...@@ -114,12 +118,18 @@ class Usage {
╔════════════════════════════════════════════════════════════════════════════╗ ╔════════════════════════════════════════════════════════════════════════════╗
║ Welcome to Flutter! - https://flutter.io ║ ║ Welcome to Flutter! - https://flutter.io ║
║ ║ ║ ║
║ The Flutter tool anonymously reports feature usage statistics and basic ║ ║ The Flutter tool anonymously reports feature usage statistics and crash ║
║ crash reports to Google in order to help Google contribute improvements to ║ ║ reports to Google in order to help Google contribute improvements to ║
║ Flutter over time. See Google's privacy policy: ║ 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/ ║ 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); ''', emphasis: true);
} }
......
...@@ -24,7 +24,7 @@ dependencies: ...@@ -24,7 +24,7 @@ dependencies:
platform: 1.1.1 platform: 1.1.1
process: 2.0.1 process: 2.0.1
stack_trace: ^1.4.0 stack_trace: ^1.4.0
usage: ^3.0.0+1 usage: ^3.0.1
vm_service_client: '0.2.2+4' vm_service_client: '0.2.2+4'
web_socket_channel: ^1.0.4 web_socket_channel: ^1.0.4
xml: ^2.4.1 xml: ^2.4.1
......
...@@ -3,10 +3,12 @@ ...@@ -3,10 +3,12 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/local.dart'; import 'package:file/local.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:http/testing.dart'; import 'package:http/testing.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -30,23 +32,42 @@ void main() { ...@@ -30,23 +32,42 @@ void main() {
tools.crashFileSystem = new MemoryFileSystem(); tools.crashFileSystem = new MemoryFileSystem();
tools.writelnStderr = ([_]) { }; tools.writelnStderr = ([_]) { };
setExitFunctionForTests((_) { }); setExitFunctionForTests((_) { });
enterTestingMode();
}); });
tearDown(() { tearDown(() {
tools.crashFileSystem = const LocalFileSystem(); tools.crashFileSystem = const LocalFileSystem();
tools.writelnStderr = stderr.writeln; tools.writelnStderr = stderr.writeln;
restoreExitFunction(); restoreExitFunction();
exitTestingMode();
}); });
testUsingContext('should send crash reports', () async { testUsingContext('should send crash reports', () async {
String method; String method;
Uri uri; Uri uri;
Map<String, String> fields;
CrashReportSender.initializeWith(new MockClient((Request request) async { CrashReportSender.initializeWith(new MockClient((Request request) async {
method = request.method; method = request.method;
uri = request.url; 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( return new Response(
'test-report-id', 'test-report-id',
200 200
...@@ -68,12 +89,20 @@ void main() { ...@@ -68,12 +89,20 @@ void main() {
scheme: 'https', scheme: 'https',
host: 'clients2.google.com', host: 'clients2.google.com',
port: 443, port: 443,
path: '/cr/staging_report', path: '/cr/report',
queryParameters: <String, String>{ queryParameters: <String, String>{
'product': 'Flutter_Tools', 'product': 'Flutter_Tools',
'version' : 'test-version', '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]; final BufferLogger logger = context[Logger];
expect(logger.statusText, 'Sending crash report to Google.\n' expect(logger.statusText, 'Sending crash report to Google.\n'
'Crash report sent (report ID: test-report-id)\n'); 'Crash report sent (report ID: test-report-id)\n');
......
...@@ -170,6 +170,9 @@ class MockSimControl extends Mock implements SimControl { ...@@ -170,6 +170,9 @@ class MockSimControl extends Mock implements SimControl {
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils { class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {
@override @override
List<File> whichAll(String execName) => <File>[]; List<File> whichAll(String execName) => <File>[];
@override
String get name => 'fake OS name and version';
} }
class MockIOSSimulatorUtils extends Mock implements IOSSimulatorUtils {} class MockIOSSimulatorUtils extends Mock implements IOSSimulatorUtils {}
...@@ -190,6 +193,9 @@ class MockUsage implements Usage { ...@@ -190,6 +193,9 @@ class MockUsage implements Usage {
@override @override
set enabled(bool value) { } set enabled(bool value) { }
@override
String get clientId => '00000000-0000-4000-0000-000000000000';
@override @override
void sendCommand(String command) { } 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