From 8e45759a674615b792a8916c93ba1ce12960eeeb Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Thu, 29 Jun 2017 16:46:53 -0700 Subject: [PATCH] Add initial version of analytics and crash reporting package. BUG= R=brianwilkerson@google.com Review-Url: https://codereview.chromium.org/2954733002 . --- .packages | 1 + pkg/pkg.status | 1 + pkg/telemetry/LICENSE | 26 +++++ pkg/telemetry/README.md | 51 ++++++++++ pkg/telemetry/analysis_options.yaml | 10 ++ pkg/telemetry/lib/crash_reporting.dart | 92 ++++++++++++++++++ pkg/telemetry/lib/telemetry.dart | 99 ++++++++++++++++++++ pkg/telemetry/pubspec.yaml | 16 ++++ pkg/telemetry/test/crash_reporting_test.dart | 40 ++++++++ pkg/telemetry/test/telemetry_test.dart | 31 ++++++ 10 files changed, 367 insertions(+) create mode 100644 pkg/telemetry/LICENSE create mode 100644 pkg/telemetry/README.md create mode 100644 pkg/telemetry/analysis_options.yaml create mode 100644 pkg/telemetry/lib/crash_reporting.dart create mode 100644 pkg/telemetry/lib/telemetry.dart create mode 100644 pkg/telemetry/pubspec.yaml create mode 100644 pkg/telemetry/test/crash_reporting_test.dart create mode 100644 pkg/telemetry/test/telemetry_test.dart diff --git a/.packages b/.packages index 3c4fe9b4230..1f3ef641e99 100644 --- a/.packages +++ b/.packages @@ -91,6 +91,7 @@ source_span:third_party/pkg/source_span/lib stack_trace:third_party/pkg/stack_trace/lib stream_channel:third_party/pkg/stream_channel/lib string_scanner:third_party/pkg/string_scanner/lib +telemetry:pkg/telemetry/lib test:third_party/pkg/test/lib test_dart:tools/testing/dart testing:pkg/testing/lib diff --git a/pkg/pkg.status b/pkg/pkg.status index 860edb25b1d..457139f4026 100644 --- a/pkg/pkg.status +++ b/pkg/pkg.status @@ -123,6 +123,7 @@ collection/test/equality_test/none: Pass, Fail # Issue 14348 compiler/tool/*: SkipByDesign # Only meant to run on vm front_end/tool/*: SkipByDesign # Only meant to run on vm lookup_map/test/version_check_test: SkipByDesign # Only meant to run in vm. +telemetry/test/*: SkipByDesign # Only meant to run on vm typed_data/test/typed_buffers_test/01: Fail # Not supporting Int64List, Uint64List. front_end/test/incremental_kernel_generator_test: SkipByDesign # Uses dart:io front_end/test/incremental_resolved_ast_generator_test: SkipByDesign # Uses dart:io diff --git a/pkg/telemetry/LICENSE b/pkg/telemetry/LICENSE new file mode 100644 index 00000000000..389ce985634 --- /dev/null +++ b/pkg/telemetry/LICENSE @@ -0,0 +1,26 @@ +Copyright 2017, the Dart project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg/telemetry/README.md b/pkg/telemetry/README.md new file mode 100644 index 00000000000..e01b1b6fba0 --- /dev/null +++ b/pkg/telemetry/README.md @@ -0,0 +1,51 @@ +# telemetry + +A library to facilitate reporting analytics and crash reports. + +## Analytics + +This library is designed to allow all Dart SDK tools to easily send analytics +information and crash reports. The tools share a common setting to configure +sending analytics data. To use this library for a specific tool: + +``` +import 'package:telemtry/telemtry.dart'; +import 'package:usage/usage.dart'; + +main() async { + final String myAppTrackingID = ...; + final String myAppName = ...; + + Analytics analytics = createAnalyticsInstance(myAppTrackingID, myAppName); + ... + analytics.sendScreenView('home'); + ... + await analytics.waitForLastPing(); +} +``` + +The analytics object reads from the correct user configuration file +automatically without any additional configuration. Analytics will not be sent +if the user has opted-out. + +## Crash reporting + +To use the crash reporting functionality, import `crash_reporting.dart`, and +create a new `CrashReportSender` instance: + +```dart +import 'package:telemtry/crash_reporting.dart'; + +main() { + Analytics analytics = ...; + CrashReportSender sender = new CrashReportSender(analytics); + try { + ... + } catch (e, st) { + sender.sendReport(e, st); + } +} +``` + +Crash reports will only be sent if the cooresponding [Analytics] object is +configured to send analytics. diff --git a/pkg/telemetry/analysis_options.yaml b/pkg/telemetry/analysis_options.yaml new file mode 100644 index 00000000000..85f01f0a5d4 --- /dev/null +++ b/pkg/telemetry/analysis_options.yaml @@ -0,0 +1,10 @@ +analyzer: + strong-mode: true +linter: + rules: + - annotate_overrides + - empty_constructor_bodies + - empty_statements + - unawaited_futures + - unnecessary_brace_in_string_interps + - valid_regexps diff --git a/pkg/telemetry/lib/crash_reporting.dart b/pkg/telemetry/lib/crash_reporting.dart new file mode 100644 index 00000000000..37d6b755949 --- /dev/null +++ b/pkg/telemetry/lib/crash_reporting.dart @@ -0,0 +1,92 @@ +// 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 'package:http/http.dart' as http; +import 'package:stack_trace/stack_trace.dart'; +import 'package:usage/usage.dart'; + +/// Crash backend host. +const String _crashServerHost = 'clients2.google.com'; + +/// Path to the crash servlet. +const String _crashEndpointPath = '/cr/report'; // or, staging_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 { + static final Uri _baseUri = new Uri( + scheme: 'https', host: _crashServerHost, path: _crashEndpointPath); + + final Analytics analytics; + final http.Client _httpClient; + + /// Create a new [CrashReportSender], using the data from the given + /// [Analytics] instance. + CrashReportSender(this.analytics, {http.Client httpClient}) + : _httpClient = httpClient ?? new http.Client(); + + /// Sends one crash report. + /// + /// The report is populated from data in [error] and [stackTrace]. + Future sendReport(dynamic error, {StackTrace stackTrace}) async { + if (!analytics.enabled) { + return; + } + + try { + final Uri uri = _baseUri.replace( + queryParameters: { + 'product': analytics.trackingId, + 'version': analytics.applicationVersion, + }, + ); + + final http.MultipartRequest req = new http.MultipartRequest('POST', uri); + req.fields['uuid'] = analytics.clientId; + req.fields['product'] = analytics.trackingId; + req.fields['version'] = analytics.applicationVersion; + req.fields['osName'] = Platform.operatingSystem; + // TODO(devoncarew): Report the operating system version when we're able. + //req.fields['osVersion'] = Platform.operatingSystemVersion; + req.fields['type'] = 'DartError'; + req.fields['error_runtime_type'] = '${error.runtimeType}'; + + final Chain chain = new Chain.parse(stackTrace.toString()); + req.files.add(new http.MultipartFile.fromString( + _stackTraceFileField, chain.terse.toString(), + filename: _stackTraceFilename)); + + 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'; + } + } + + /// Closes the client and cleans up any resources associated with it. This + /// will close the associated [http.Client]. + void dispose() { + _httpClient.close(); + } +} diff --git a/pkg/telemetry/lib/telemetry.dart b/pkg/telemetry/lib/telemetry.dart new file mode 100644 index 00000000000..c0b912fa8b4 --- /dev/null +++ b/pkg/telemetry/lib/telemetry.dart @@ -0,0 +1,99 @@ +// 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:io'; + +import 'package:path/path.dart' as path; +import 'package:usage/src/usage_impl.dart'; +import 'package:usage/src/usage_impl_io.dart'; +import 'package:usage/src/usage_impl_io.dart' as usage_io show getDartVersion; +import 'package:usage/usage.dart'; +import 'package:usage/usage_io.dart'; + +final String _dartDirectoryName = '.dart'; +final String _settingsFileName = 'analytics.json'; + +/// Dart SDK tools with analytics should display this notice. +/// +/// In addition, they should support displaying the analytics' status, and have +/// a flag to toggle analytics. This may look something like: +/// +/// `Analytics are currently enabled (and can be disabled with --no-analytics).` +final String analyticsNotice = + "Dart SDK tools anonymously report feature usage statistics and basic " + "crash reports to help improve Dart tools over time. See Google's privacy " + "policy: https://www.google.com/intl/en/policies/privacy/."; + +/// Create an [Analytics] instance with the given trackingID and +/// applicationName. +/// +/// This analytics instance will share a common enablement state with the rest +/// of the Dart SDK tools. +Analytics createAnalyticsInstance(String trackingId, String applicationName, + {bool disableForSession: false}) { + Directory dir = getDartStorageDirectory(); + if (!dir.existsSync()) { + dir.createSync(); + } + + File file = new File(path.join(dir.path, _settingsFileName)); + return new _TelemetryAnalytics( + trackingId, applicationName, getDartVersion(), file, disableForSession); +} + +/// The directory used to store the analytics settings file. +/// +/// Typically, the directory is `~/.dart/` (and the settings file is +/// `analytics.json`). +Directory getDartStorageDirectory() => + new Directory(path.join(userHomeDir(), _dartDirectoryName)); + +/// Return the version of the Dart SDK. +String getDartVersion() => usage_io.getDartVersion(); + +class _TelemetryAnalytics extends AnalyticsImpl { + final bool disableForSession; + + _TelemetryAnalytics( + String trackingId, + String applicationName, + String applicationVersion, + File file, + this.disableForSession, + ) + : super( + trackingId, + new IOPersistentProperties.fromFile(file), + new IOPostHandler(), + applicationName: applicationName, + applicationVersion: applicationVersion, + ) { + final String locale = getPlatformLocale(); + if (locale != null) { + setSessionValue('ul', locale); + } + } + + @override + bool get enabled { + if (disableForSession || _isRunningOnBot()) { + return false; + } + return super.enabled; + } +} + +bool _isRunningOnBot() { + // - https://docs.travis-ci.com/user/environment-variables/ + // - https://www.appveyor.com/docs/environment-variables/ + // - CHROME_HEADLESS and BUILDBOT_BUILDERNAME are properties on Chrome infra + // bots. + return Platform.environment['TRAVIS'] == 'true' || + Platform.environment['BOT'] == 'true' || + Platform.environment['CONTINUOUS_INTEGRATION'] == 'true' || + Platform.environment['CHROME_HEADLESS'] == '1' || + Platform.environment.containsKey('BUILDBOT_BUILDERNAME') || + Platform.environment.containsKey('APPVEYOR') || + Platform.environment.containsKey('CI'); +} diff --git a/pkg/telemetry/pubspec.yaml b/pkg/telemetry/pubspec.yaml new file mode 100644 index 00000000000..4068d3ab633 --- /dev/null +++ b/pkg/telemetry/pubspec.yaml @@ -0,0 +1,16 @@ +name: telemetry +description: A library to facilitate reporting analytics and crash reports. +version: 0.0.1-dev +author: Dart Team + +environment: + sdk: '>=1.0.0 <2.0.0' + +dependencies: + http: ^0.11.3+12 + path: ^1.4.0 + stack_trace: ^1.7.0 + usage: ^3.2.0+1 + +dev_dependencies: + test: ^0.12.0 diff --git a/pkg/telemetry/test/crash_reporting_test.dart b/pkg/telemetry/test/crash_reporting_test.dart new file mode 100644 index 00000000000..a6241216ff0 --- /dev/null +++ b/pkg/telemetry/test/crash_reporting_test.dart @@ -0,0 +1,40 @@ +// 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:convert' show UTF8; + +import 'package:http/http.dart'; +import 'package:http/testing.dart'; +import 'package:telemetry/crash_reporting.dart'; +import 'package:test/test.dart'; +import 'package:usage/usage.dart'; + +void main() { + group('crash_reporting', () { + MockClient mockClient; + + Request request; + + setUp(() { + mockClient = new MockClient((Request r) async { + request = r; + return new Response('crash-report-001', 200); + }); + }); + + test('CrashReportSender', () async { + AnalyticsMock analytics = new AnalyticsMock(); + CrashReportSender sender = + new CrashReportSender(analytics, httpClient: mockClient); + + await sender.sendReport('test-error', stackTrace: StackTrace.current); + + String body = UTF8.decode(request.bodyBytes); + expect(body, contains('String')); // error.runtimeType + expect(body, contains(analytics.trackingId)); + expect(body, contains('1.0.0')); + expect(body, contains(analytics.clientId)); + }); + }); +} diff --git a/pkg/telemetry/test/telemetry_test.dart b/pkg/telemetry/test/telemetry_test.dart new file mode 100644 index 00000000000..7a9b70a4668 --- /dev/null +++ b/pkg/telemetry/test/telemetry_test.dart @@ -0,0 +1,31 @@ +// 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:io'; + +import 'package:telemetry/telemetry.dart'; +import 'package:test/test.dart'; +import 'package:usage/usage.dart'; + +void main() { + group('telemetry', () { + test('getDartStorageDirectory', () { + Directory dir = getDartStorageDirectory(); + expect(dir, isNotNull); + }); + + test('getDartVersion', () { + expect(getDartVersion(), isNotNull); + }); + + test('createAnalyticsInstance', () { + Analytics analytics = createAnalyticsInstance('UA-0', 'test-app'); + expect(analytics, isNotNull); + expect(analytics.trackingId, 'UA-0'); + expect(analytics.getSessionValue('an'), 'test-app'); + expect(analytics.getSessionValue('av'), isNotNull); + expect(analytics.clientId, isNotNull); + }); + }); +}