From 05d390769a83a2d393fc13fc0ec26acb40a97d5e Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Mon, 19 Jun 2017 18:08:23 -0700 Subject: [PATCH 01/38] can parse a Sentry DSN --- .gitignore | 9 +++++++ .idea/modules.xml | 8 ++++++ .idea/sentry.iml | 16 ++++++++++++ .idea/vcs.xml | 6 +++++ LICENSE | 27 ++++++++++++++++++++ PATENTS | 17 +++++++++++++ README.md | 3 +++ lib/sentry.dart | 57 +++++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 8 ++++++ test/sentry_test.dart | 18 ++++++++++++++ 10 files changed, 169 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/sentry.iml create mode 100644 .idea/vcs.xml create mode 100644 LICENSE create mode 100644 PATENTS create mode 100644 README.md create mode 100644 lib/sentry.dart create mode 100644 pubspec.yaml create mode 100644 test/sentry_test.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..aeb3c1d58c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +.atom/ +.packages +.pub/ +build/ +packages +pubspec.lock +.idea/libraries +.idea/workspace.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000000..96a30c72817 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sentry.iml b/.idea/sentry.iml new file mode 100644 index 00000000000..369de463126 --- /dev/null +++ b/.idea/sentry.iml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000000..94a25f7f4cb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..6f2d1444dd9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2014 The Chromium 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. \ No newline at end of file diff --git a/PATENTS b/PATENTS new file mode 100644 index 00000000000..ac39faf6793 --- /dev/null +++ b/PATENTS @@ -0,0 +1,17 @@ +Google hereby grants to you a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this +section) patent license to make, have made, use, offer to sell, sell, +import, transfer, and otherwise run, modify and propagate the contents +of this implementation, where such license applies only to those +patent claims, both currently owned by Google and acquired in the +future, licensable by Google that are necessarily infringed by this +implementation. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute +or order or agree to the institution of patent litigation or any other +patent enforcement activity against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that this +implementation constitutes direct or contributory patent infringement, +or inducement of patent infringement, then any patent rights granted +to you under this License for this implementation shall terminate as +of the date such litigation is filed. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000000..d0a1ad75662 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Sentry.io client for Dart + +## WARNING: experimental code diff --git a/lib/sentry.dart b/lib/sentry.dart new file mode 100644 index 00000000000..f08f44556b4 --- /dev/null +++ b/lib/sentry.dart @@ -0,0 +1,57 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A pure Dart client for Sentry.io crash reporting. +library sentry; + +import 'package:meta/meta.dart'; + +/// Logs crash reports and events to the Sentry.io service. +class SentryClient { + + /// Instantiates a client from a [dns] issued to your project by Sentry.io. + factory SentryClient({@required String dsn}) { + final Uri uri = Uri.parse(dsn); + final List userInfo = uri.userInfo.split(':'); + assert(() { + if (userInfo.length != 2) + throw new ArgumentError('Colon-separated publicKey:secretKey pair not found in the user info field of the DSN URI: $dsn'); + + if (uri.pathSegments.isEmpty) + throw new ArgumentError('Project ID not found in the URI path of the DSN URI: $dsn'); + + return true; + }); + + final String host = uri.host; + final String publicKey = userInfo.first; + final String secretKey = userInfo.last; + final String projectId = uri.pathSegments.last; + + return new SentryClient._( + '${uri.scheme}://$host/api/$projectId/store', + publicKey, + secretKey, + projectId, + ); + } + + SentryClient._(this.postUri, this.publicKey, this.secretKey, this.projectId); + + /// The URI where this client sends events via HTTP POST. + @visibleForTesting + final String postUri; + + /// The Sentry.io public key for the project. + @visibleForTesting + final String publicKey; + + /// The Sentry.io secret key for the project. + @visibleForTesting + final String secretKey; + + /// The Sentry.io project identifier. + @visibleForTesting + final String projectId; +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000000..c9a238c24af --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,8 @@ +name: sentry +description: A pure Dart Sentry.io client. + +dependencies: + meta: ^1.0.5 + +dev_dependencies: + test: 0.12.21 diff --git a/test/sentry_test.dart b/test/sentry_test.dart new file mode 100644 index 00000000000..69c32bd72c4 --- /dev/null +++ b/test/sentry_test.dart @@ -0,0 +1,18 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + group('$SentryClient', () { + test('can parse DSN', () { + final SentryClient client = new SentryClient(dsn: 'https://public:secret@sentry.example.com/1'); + expect(client.postUri, 'https://sentry.example.com/api/1/store'); + expect(client.publicKey, 'public'); + expect(client.secretKey, 'secret'); + expect(client.projectId, '1'); + }); + }); +} From bbc9da80f518b2e779b9fa3a77995974300e77cd Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Jun 2017 14:53:06 -0700 Subject: [PATCH 02/38] able to send a crash report with basic data --- .idea/sentry.iml | 1 + bin/test.dart | 36 ++++++++++ lib/sentry.dart | 157 +++++++++++++++++++++++++++++++++++++++-- lib/src/version.dart | 7 ++ pubspec.yaml | 8 ++- test/sentry_test.dart | 68 +++++++++++++++++- test/version_test.dart | 18 +++++ 7 files changed, 284 insertions(+), 11 deletions(-) create mode 100644 bin/test.dart create mode 100644 lib/src/version.dart create mode 100644 test/version_test.dart diff --git a/.idea/sentry.iml b/.idea/sentry.iml index 369de463126..5dde51be9a4 100644 --- a/.idea/sentry.iml +++ b/.idea/sentry.iml @@ -4,6 +4,7 @@ + diff --git a/bin/test.dart b/bin/test.dart new file mode 100644 index 00000000000..978c67edaf3 --- /dev/null +++ b/bin/test.dart @@ -0,0 +1,36 @@ +// Copyright 2017 The Chromium Authors. 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:sentry/sentry.dart'; + +/// Sends a test exception report to Sentry.io using this Dart client. +Future main(List rawArgs) async { + if (rawArgs.length != 1) { + stderr.writeln('Expected exactly one argument, which is the DSN issued by Sentry.io to your project.'); + exit(1); + } + + final String dsn = rawArgs.single; + final SentryClient client = new SentryClient(dsn: dsn); + + try { + throw new StateError('This is a test error'); + } catch(error, stackTrace) { + final SentryResponse response = await client.captureException( + exception: error, + stackTrace: stackTrace, + ); + + if (response.isSuccessful) { + print('SUCCESS\nid: ${response.eventId}'); + } else { + print('FAILURE: ${response.error}'); + } + } finally { + await client.close(); + } +} diff --git a/lib/sentry.dart b/lib/sentry.dart index f08f44556b4..4bc47a2d440 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -5,15 +5,43 @@ /// A pure Dart client for Sentry.io crash reporting. library sentry; +import 'dart:async'; +import 'dart:convert'; + +import 'package:http/http.dart'; import 'package:meta/meta.dart'; +import 'package:quiver/time.dart'; +import 'package:usage/uuid/uuid.dart'; + +import 'src/version.dart'; +export 'src/version.dart' show sdkVersion; /// Logs crash reports and events to the Sentry.io service. class SentryClient { - /// Instantiates a client from a [dns] issued to your project by Sentry.io. - factory SentryClient({@required String dsn}) { + /// The name of the SDK used to submit events, i.e. _this_ SDK. + @visibleForTesting + static const String sdkName = 'dart'; + + /// Sentry.io client identifier for _this_ client. + @visibleForTesting + static const String sentryClient = '$sdkName/$sdkVersion'; + + /// Instantiates a client using [dns] issued to your project by Sentry.io as + /// the endpoint for submitting events. + /// + /// If [httpClient] is provided, it is used instead of the default client to + /// make HTTP calls to Sentry.io. + /// + /// If [clock] is provided, it is used instead of the system clock. + factory SentryClient({@required String dsn, Client httpClient, Clock clock, UuidGenerator uuidGenerator}) { + httpClient ??= new Client(); + clock ??= const Clock(); + uuidGenerator ??= _generateUuidV4WithoutDashes; + final Uri uri = Uri.parse(dsn); final List userInfo = uri.userInfo.split(':'); + assert(() { if (userInfo.length != 2) throw new ArgumentError('Colon-separated publicKey:secretKey pair not found in the user info field of the DSN URI: $dsn'); @@ -24,24 +52,30 @@ class SentryClient { return true; }); - final String host = uri.host; final String publicKey = userInfo.first; final String secretKey = userInfo.last; final String projectId = uri.pathSegments.last; return new SentryClient._( - '${uri.scheme}://$host/api/$projectId/store', + httpClient, + clock, + uuidGenerator, + uri, publicKey, secretKey, projectId, ); } - SentryClient._(this.postUri, this.publicKey, this.secretKey, this.projectId); + SentryClient._(this._httpClient, this._clock, this._uuidGenerator, this.dsnUri, this.publicKey, this.secretKey, this.projectId); + + final Clock _clock; + final Client _httpClient; + final UuidGenerator _uuidGenerator; - /// The URI where this client sends events via HTTP POST. + /// The DSN URI. @visibleForTesting - final String postUri; + final Uri dsnUri; /// The Sentry.io public key for the project. @visibleForTesting @@ -54,4 +88,113 @@ class SentryClient { /// The Sentry.io project identifier. @visibleForTesting final String projectId; + + @visibleForTesting + String get postUri => '${dsnUri.scheme}://${dsnUri.host}/api/$projectId/store/'; + + /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. + Future captureException({ + @required dynamic exception, + dynamic stackTrace, + }) async { + final DateTime now = _clock.now(); + final Map headers = { + 'User-Agent': '$sentryClient', + 'Content-Type': 'application/json', + 'X-Sentry-Auth': 'Sentry sentry_version=6, ' + 'sentry_client=$sentryClient, ' + 'sentry_timestamp=${now.millisecondsSinceEpoch}, ' + 'sentry_key=$publicKey, ' + 'sentry_secret=$secretKey', + }; + + final String body = JSON.encode({ + 'project': projectId, + 'event_id': _uuidGenerator(), + 'timestamp': now.toIso8601String(), + 'message': '$exception', + 'platform': 'dart', + 'exception': [{ + 'type': '${exception.runtimeType}', + 'value': '$exception', + }], + 'sdk': { + 'version': sdkVersion, + 'name': sdkName, + }, + }); + + final Response response = await _httpClient.post(postUri, headers: headers, body: body); + + if (response.statusCode != 200) { + return new SentryResponse.failure('Server responded with HTTP ${response.statusCode}'); + } + + final String eventId = JSON.decode(response.body)['id']; + return new SentryResponse.success(eventId: eventId); + } + + Future close() async { + _httpClient.close(); + } + + @override + String toString() => '$SentryClient("$postUri")'; +} + +class SentryResponse { + SentryResponse.success({@required eventId}) + : isSuccessful = true, + eventId = eventId, + error = null; + + SentryResponse.failure(error) + : isSuccessful = false, + eventId = null, + error = error; + + /// Whether event was submitted successfully. + final bool isSuccessful; + + /// The ID Sentry.io assigned to the submitted event for future reference. + final String eventId; + + /// Error message, if the response is not successful. + final String error; +} + +typedef UuidGenerator = String Function(); + +String _generateUuidV4WithoutDashes() { + return new Uuid().generateV4().replaceAll('-', ''); +} + +/// Severity of the logged [Event]. +enum SeverityLevel { + fatal, + error, + warning, + info, + debug, +} + +class Event { + static const String _defaultFingerprint = '{{ default }}'; + + Event({ + @required projectId, + @required String eventId, + @required DateTime timestamp, + @required String logger, + @required String platform, + SeverityLevel level, + String culprit, + String serverName, + String release, + Map tags, + String environment, + Map modules, + Map extra, + List fingerprint, + }); } diff --git a/lib/src/version.dart b/lib/src/version.dart new file mode 100644 index 00000000000..9e56f0d406f --- /dev/null +++ b/lib/src/version.dart @@ -0,0 +1,7 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The version of the Sentry for Dart SDK (it's just a library but Sentry.io +/// prefers calling these things SDK). +const String sdkVersion = '0.0.1'; diff --git a/pubspec.yaml b/pubspec.yaml index c9a238c24af..496469e1bf0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,14 @@ name: sentry +version: 0.0.1 description: A pure Dart Sentry.io client. dependencies: meta: ^1.0.5 + quiver: ^0.25.0 + usage: ^3.1.1 dev_dependencies: - test: 0.12.21 + args: ^0.13.7 + test: ^0.12.21 + yaml: ^2.1.12 + mockito: ^2.0.2 diff --git a/test/sentry_test.dart b/test/sentry_test.dart index 69c32bd72c4..a798a9034eb 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -2,17 +2,79 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:mockito/mockito.dart'; +import 'package:quiver/time.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +const String _testDsn = 'https://public:secret@sentry.example.com/1'; + void main() { group('$SentryClient', () { - test('can parse DSN', () { - final SentryClient client = new SentryClient(dsn: 'https://public:secret@sentry.example.com/1'); - expect(client.postUri, 'https://sentry.example.com/api/1/store'); + test('can parse DSN', () async { + final SentryClient client = new SentryClient(dsn: _testDsn); + expect(client.dsnUri, Uri.parse(_testDsn)); + expect(client.postUri, 'https://sentry.example.com/api/1/store/'); expect(client.publicKey, 'public'); expect(client.secretKey, 'secret'); expect(client.projectId, '1'); + await client.close(); + }); + + test('sends an exception report', () async { + final MockClient httpMock = new MockClient(); + final Clock fakeClock = new Clock.fixed(new DateTime(2017, 1, 2)); + + String postUri; + Map headers; + String body; + when(httpMock.post(any, headers: any, body: any)).thenAnswer((Invocation invocation) { + postUri = invocation.positionalArguments.single; + headers = invocation.namedArguments[#headers]; + body = invocation.namedArguments[#body]; + return new Response('{"id": "test-event-id"}', 200); + }); + + final SentryClient client = new SentryClient( + dsn: _testDsn, + httpClient: httpMock, + clock: fakeClock, + uuidGenerator: () => 'X' * 32, + ); + + try { + throw new ArgumentError('Test error'); + } catch(error, stackTrace) { + await client.captureException(exception: error, stackTrace: stackTrace); + } + + expect(postUri, client.postUri); + expect(headers, { + 'User-Agent': '${SentryClient.sdkName}/$sdkVersion', + 'Content-Type': 'application/json', + 'X-Sentry-Auth': 'Sentry sentry_version=6, ' + 'sentry_client=${SentryClient.sentryClient}, ' + 'sentry_timestamp=${fakeClock.now().millisecondsSinceEpoch}, ' + 'sentry_key=public, ' + 'sentry_secret=secret', + }); + + expect(JSON.decode(body), { + 'project': '1', + 'event_id': 'X' * 32, + 'timestamp': '2017-01-02T00:00:00.000', + 'message': 'Invalid argument(s): Test error', + 'platform': 'dart', + 'exception': [{'type': 'ArgumentError', 'value': 'Invalid argument(s): Test error'}], + 'sdk': {'version': '0.0.1', 'name': 'dart'} + }); + + await client.close(); }); }); } + +class MockClient extends Mock implements Client {} diff --git a/test/version_test.dart b/test/version_test.dart new file mode 100644 index 00000000000..f878e1972cb --- /dev/null +++ b/test/version_test.dart @@ -0,0 +1,18 @@ +// Copyright 2017 The Chromium Authors. 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:sentry/sentry.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart' as yaml; + +void main() { + group('sdkVersion', () { + test('matches that of pubspec.yaml', () { + final dynamic pubspec = yaml.loadYaml(new File('pubspec.yaml').readAsStringSync()); + expect(sdkVersion, pubspec['version']); + }); + }); +} From c0df06c28facef6e9dc2fe603a3accd698bdd39c Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Jun 2017 14:57:44 -0700 Subject: [PATCH 03/38] travis build --- .travis.yml | 5 +++++ lib/sentry.dart | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..583783d8342 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: dart +dart_task: +- test: --platform vm +- dartanalyzer: --strong --fatal-warnings ./ +- dartfmt: true diff --git a/lib/sentry.dart b/lib/sentry.dart index 4bc47a2d440..a8584807447 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -179,7 +179,7 @@ enum SeverityLevel { } class Event { - static const String _defaultFingerprint = '{{ default }}'; +// static const String _defaultFingerprint = '{{ default }}'; Event({ @required projectId, From cce0e9f369359ba2cac477f56e1c00231d5c9668 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Jun 2017 15:05:28 -0700 Subject: [PATCH 04/38] dartfmt; single-job travis --- .travis.yml | 11 +++++++---- bin/test.dart | 5 +++-- lib/sentry.dart | 37 ++++++++++++++++++++++++------------- test/sentry_test.dart | 9 ++++++--- test/version_test.dart | 3 ++- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/.travis.yml b/.travis.yml index 583783d8342..e6c47009ca4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ language: dart -dart_task: -- test: --platform vm -- dartanalyzer: --strong --fatal-warnings ./ -- dartfmt: true +dart: + - stable + - dev +script: + - dartanalyzer --strong --fatal-warnings ./ + - pub run test --platform vm + - dartfmt -n --set-exit-if-changed ./ diff --git a/bin/test.dart b/bin/test.dart index 978c67edaf3..0e90193e61f 100644 --- a/bin/test.dart +++ b/bin/test.dart @@ -10,7 +10,8 @@ import 'package:sentry/sentry.dart'; /// Sends a test exception report to Sentry.io using this Dart client. Future main(List rawArgs) async { if (rawArgs.length != 1) { - stderr.writeln('Expected exactly one argument, which is the DSN issued by Sentry.io to your project.'); + stderr.writeln( + 'Expected exactly one argument, which is the DSN issued by Sentry.io to your project.'); exit(1); } @@ -19,7 +20,7 @@ Future main(List rawArgs) async { try { throw new StateError('This is a test error'); - } catch(error, stackTrace) { + } catch (error, stackTrace) { final SentryResponse response = await client.captureException( exception: error, stackTrace: stackTrace, diff --git a/lib/sentry.dart b/lib/sentry.dart index a8584807447..a4c90e2ddb8 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -18,7 +18,6 @@ export 'src/version.dart' show sdkVersion; /// Logs crash reports and events to the Sentry.io service. class SentryClient { - /// The name of the SDK used to submit events, i.e. _this_ SDK. @visibleForTesting static const String sdkName = 'dart'; @@ -34,7 +33,11 @@ class SentryClient { /// make HTTP calls to Sentry.io. /// /// If [clock] is provided, it is used instead of the system clock. - factory SentryClient({@required String dsn, Client httpClient, Clock clock, UuidGenerator uuidGenerator}) { + factory SentryClient( + {@required String dsn, + Client httpClient, + Clock clock, + UuidGenerator uuidGenerator}) { httpClient ??= new Client(); clock ??= const Clock(); uuidGenerator ??= _generateUuidV4WithoutDashes; @@ -44,10 +47,12 @@ class SentryClient { assert(() { if (userInfo.length != 2) - throw new ArgumentError('Colon-separated publicKey:secretKey pair not found in the user info field of the DSN URI: $dsn'); + throw new ArgumentError( + 'Colon-separated publicKey:secretKey pair not found in the user info field of the DSN URI: $dsn'); if (uri.pathSegments.isEmpty) - throw new ArgumentError('Project ID not found in the URI path of the DSN URI: $dsn'); + throw new ArgumentError( + 'Project ID not found in the URI path of the DSN URI: $dsn'); return true; }); @@ -67,7 +72,8 @@ class SentryClient { ); } - SentryClient._(this._httpClient, this._clock, this._uuidGenerator, this.dsnUri, this.publicKey, this.secretKey, this.projectId); + SentryClient._(this._httpClient, this._clock, this._uuidGenerator, + this.dsnUri, this.publicKey, this.secretKey, this.projectId); final Clock _clock; final Client _httpClient; @@ -90,7 +96,8 @@ class SentryClient { final String projectId; @visibleForTesting - String get postUri => '${dsnUri.scheme}://${dsnUri.host}/api/$projectId/store/'; + String get postUri => + '${dsnUri.scheme}://${dsnUri.host}/api/$projectId/store/'; /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. Future captureException({ @@ -98,7 +105,7 @@ class SentryClient { dynamic stackTrace, }) async { final DateTime now = _clock.now(); - final Map headers = { + final Map headers = { 'User-Agent': '$sentryClient', 'Content-Type': 'application/json', 'X-Sentry-Auth': 'Sentry sentry_version=6, ' @@ -114,20 +121,24 @@ class SentryClient { 'timestamp': now.toIso8601String(), 'message': '$exception', 'platform': 'dart', - 'exception': [{ - 'type': '${exception.runtimeType}', - 'value': '$exception', - }], + 'exception': [ + { + 'type': '${exception.runtimeType}', + 'value': '$exception', + } + ], 'sdk': { 'version': sdkVersion, 'name': sdkName, }, }); - final Response response = await _httpClient.post(postUri, headers: headers, body: body); + final Response response = + await _httpClient.post(postUri, headers: headers, body: body); if (response.statusCode != 200) { - return new SentryResponse.failure('Server responded with HTTP ${response.statusCode}'); + return new SentryResponse.failure( + 'Server responded with HTTP ${response.statusCode}'); } final String eventId = JSON.decode(response.body)['id']; diff --git a/test/sentry_test.dart b/test/sentry_test.dart index a798a9034eb..f1229563bfe 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -31,7 +31,8 @@ void main() { String postUri; Map headers; String body; - when(httpMock.post(any, headers: any, body: any)).thenAnswer((Invocation invocation) { + when(httpMock.post(any, headers: any, body: any)) + .thenAnswer((Invocation invocation) { postUri = invocation.positionalArguments.single; headers = invocation.namedArguments[#headers]; body = invocation.namedArguments[#body]; @@ -47,7 +48,7 @@ void main() { try { throw new ArgumentError('Test error'); - } catch(error, stackTrace) { + } catch (error, stackTrace) { await client.captureException(exception: error, stackTrace: stackTrace); } @@ -68,7 +69,9 @@ void main() { 'timestamp': '2017-01-02T00:00:00.000', 'message': 'Invalid argument(s): Test error', 'platform': 'dart', - 'exception': [{'type': 'ArgumentError', 'value': 'Invalid argument(s): Test error'}], + 'exception': [ + {'type': 'ArgumentError', 'value': 'Invalid argument(s): Test error'} + ], 'sdk': {'version': '0.0.1', 'name': 'dart'} }); diff --git a/test/version_test.dart b/test/version_test.dart index f878e1972cb..4f32a66ea3c 100644 --- a/test/version_test.dart +++ b/test/version_test.dart @@ -11,7 +11,8 @@ import 'package:yaml/yaml.dart' as yaml; void main() { group('sdkVersion', () { test('matches that of pubspec.yaml', () { - final dynamic pubspec = yaml.loadYaml(new File('pubspec.yaml').readAsStringSync()); + final dynamic pubspec = + yaml.loadYaml(new File('pubspec.yaml').readAsStringSync()); expect(sdkVersion, pubspec['version']); }); }); From d0064e3d9abc98cf1ed9dbaa13a307c80ffd3a80 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Jun 2017 15:07:47 -0700 Subject: [PATCH 05/38] Travis badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d0a1ad75662..2e7c970c5ca 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # Sentry.io client for Dart +[![Build Status](https://travis-ci.org/yjbanov/sentry.svg?branch=master)](https://travis-ci.org/yjbanov/sentry) + ## WARNING: experimental code From 095750d6fa1dd661d61c80c7821c7287a9b46b4d Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Jun 2017 18:26:12 -0700 Subject: [PATCH 06/38] structured API for many basic event attributes --- .travis.yml | 5 +- lib/sentry.dart | 267 +++++++++++++++++++++++++++++++----------- lib/src/version.dart | 15 ++- test/sentry_test.dart | 56 ++++++++- tools/presubmit.sh | 8 ++ 5 files changed, 276 insertions(+), 75 deletions(-) create mode 100755 tools/presubmit.sh diff --git a/.travis.yml b/.travis.yml index e6c47009ca4..4bcdd1aeae4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,4 @@ language: dart dart: - stable - dev -script: - - dartanalyzer --strong --fatal-warnings ./ - - pub run test --platform vm - - dartfmt -n --set-exit-if-changed ./ +script: ./tool/presubmit.sh diff --git a/lib/sentry.dart b/lib/sentry.dart index a4c90e2ddb8..cf3621d2bee 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -14,33 +14,38 @@ import 'package:quiver/time.dart'; import 'package:usage/uuid/uuid.dart'; import 'src/version.dart'; -export 'src/version.dart' show sdkVersion; +export 'src/version.dart'; /// Logs crash reports and events to the Sentry.io service. class SentryClient { - /// The name of the SDK used to submit events, i.e. _this_ SDK. - @visibleForTesting - static const String sdkName = 'dart'; - /// Sentry.io client identifier for _this_ client. @visibleForTesting static const String sentryClient = '$sdkName/$sdkVersion'; + /// The default logger name used if no other value is supplied. + static const String defaultLoggerName = 'SentryClient'; + /// Instantiates a client using [dns] issued to your project by Sentry.io as /// the endpoint for submitting events. /// + /// If [loggerName] is provided, it is used instead of [defaultLoggerName]. + /// /// If [httpClient] is provided, it is used instead of the default client to /// make HTTP calls to Sentry.io. /// - /// If [clock] is provided, it is used instead of the system clock. - factory SentryClient( - {@required String dsn, - Client httpClient, - Clock clock, - UuidGenerator uuidGenerator}) { + /// If [clock] is provided, it is used to get time instead of the system + /// clock. + factory SentryClient({ + @required String dsn, + String loggerName, + Client httpClient, + Clock clock, + UuidGenerator uuidGenerator, + }) { httpClient ??= new Client(); clock ??= const Clock(); uuidGenerator ??= _generateUuidV4WithoutDashes; + loggerName ??= defaultLoggerName; final Uri uri = Uri.parse(dsn); final List userInfo = uri.userInfo.split(':'); @@ -62,23 +67,37 @@ class SentryClient { final String projectId = uri.pathSegments.last; return new SentryClient._( - httpClient, - clock, - uuidGenerator, - uri, - publicKey, - secretKey, - projectId, + httpClient: httpClient, + clock: clock, + uuidGenerator: uuidGenerator, + dsnUri: uri, + publicKey: publicKey, + secretKey: secretKey, + projectId: projectId, + loggerName: loggerName, ); } - SentryClient._(this._httpClient, this._clock, this._uuidGenerator, - this.dsnUri, this.publicKey, this.secretKey, this.projectId); + SentryClient._({ + Client httpClient, + Clock clock, + UuidGenerator uuidGenerator, + this.dsnUri, + this.publicKey, + this.secretKey, + this.projectId, + this.loggerName, + }) + : _httpClient = httpClient, + _clock = clock, + _uuidGenerator = uuidGenerator; - final Clock _clock; final Client _httpClient; + final Clock _clock; final UuidGenerator _uuidGenerator; + final String loggerName; + /// The DSN URI. @visibleForTesting final Uri dsnUri; @@ -99,11 +118,8 @@ class SentryClient { String get postUri => '${dsnUri.scheme}://${dsnUri.host}/api/$projectId/store/'; - /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. - Future captureException({ - @required dynamic exception, - dynamic stackTrace, - }) async { + /// Reports an [event] to Sentry.io. + Future capture({@required Event event}) async { final DateTime now = _clock.now(); final Map headers = { 'User-Agent': '$sentryClient', @@ -114,25 +130,7 @@ class SentryClient { 'sentry_key=$publicKey, ' 'sentry_secret=$secretKey', }; - - final String body = JSON.encode({ - 'project': projectId, - 'event_id': _uuidGenerator(), - 'timestamp': now.toIso8601String(), - 'message': '$exception', - 'platform': 'dart', - 'exception': [ - { - 'type': '${exception.runtimeType}', - 'value': '$exception', - } - ], - 'sdk': { - 'version': sdkVersion, - 'name': sdkName, - }, - }); - + final String body = JSON.encode(event.toJson()); final Response response = await _httpClient.post(postUri, headers: headers, body: body); @@ -145,6 +143,21 @@ class SentryClient { return new SentryResponse.success(eventId: eventId); } + /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. + Future captureException({ + @required dynamic exception, + dynamic stackTrace, + }) { + final Event event = new Event( + projectId: projectId, + eventId: _uuidGenerator(), + timestamp: _clock.now(), + exception: exception, + loggerName: loggerName, + ); + return capture(event: event); + } + Future close() async { _httpClient.close(); } @@ -181,31 +194,153 @@ String _generateUuidV4WithoutDashes() { } /// Severity of the logged [Event]. -enum SeverityLevel { - fatal, - error, - warning, - info, - debug, +class SeverityLevel { + static const fatal = const SeverityLevel._('fatal'); + static const error = const SeverityLevel._('error'); + static const warning = const SeverityLevel._('warning'); + static const info = const SeverityLevel._('info'); + static const debug = const SeverityLevel._('debug'); + + const SeverityLevel._(this.name); + + /// API name of the level as it is encoded in the JSON protocol. + final String name; } +/// An event to be reported to Sentry.io. class Event { -// static const String _defaultFingerprint = '{{ default }}'; + /// Refers to the default fingerprinting algorithm. + /// + /// You do not need to specify this value unless you supplement the default + /// fingerprint with custom fingerprints. + static const String defaultFingerprint = '{{ default }}'; + /// Creates an event. Event({ - @required projectId, - @required String eventId, - @required DateTime timestamp, - @required String logger, - @required String platform, - SeverityLevel level, - String culprit, - String serverName, - String release, - Map tags, - String environment, - Map modules, - Map extra, - List fingerprint, + @required this.projectId, + @required this.eventId, + @required this.timestamp, + this.loggerName, + this.message, + this.exception, + this.level, + this.culprit, + this.serverName, + this.release, + this.tags, + this.environment, + this.extra, + this.fingerprint, }); + + /// The ID issued by Sentry.io to your project. + final String projectId; + + /// A 32-character long UUID v4 value without dashes. + final String eventId; + + /// The time the event happened. + final DateTime timestamp; + + /// The logger that logged the event. + /// + /// If not specified [SentryClient.defaultLoggerName] is used. + final String loggerName; + + /// Event message. + /// + /// Generally an event either contains a [message] or an [exception]. + final String message; + + /// An object that was thrown. + /// + /// It's `runtimeType` and `toString()` are logged. If this behavior is + /// undesirable, consider using a custom formatted [message] instead. + final dynamic exception; + + /// How important this event is. + final SeverityLevel level; + + /// What caused this event to be logged. + final String culprit; + + /// Identifies the server that logged this event. + final String serverName; + + /// The version of the application that logged the event. + final String release; + + /// Name/value pairs that events can be searched by. + final Map tags; + + /// The environment that logged the event, e.g. "production", "staging". + final String environment; + + /// Arbitrary name/value pairs attached to the event. + /// + /// Sentry.io docs do not talk about restrictions on the values, other than + /// they must be JSON-serializable. + final Map extra; + + /// Used to deduplicate events by grouping ones with the same fingerprint + /// together. + /// + /// If not specified a default deduplication fingerprint is used. The default + /// fingerprint may be supplemented by additional fingerprints by specifying + /// multiple values. The default fingerprint can be specified by adding + /// [defaultFingerprint] to the list in addition to your custom values. + /// + /// Examples: + /// + /// // A completely custom fingerprint: + /// var custom = ['foo', 'bar', 'baz']; + /// // A fingerprint that supplements the default one with value 'foo': + /// var supplemented = [Event.defaultFingerprint, 'foo']; + final List fingerprint; + + /// Serializes this event to JSON. + Map toJson() { + final Map json = { + 'project': projectId, + 'event_id': eventId, + 'timestamp': timestamp.toIso8601String(), + 'platform': sdkPlatform, + 'sdk': { + 'version': sdkVersion, + 'name': sdkName, + }, + }; + + json['logger'] = loggerName ?? SentryClient.defaultLoggerName; + + if (message != null) json['message'] = message; + + if (exception != null) { + json['exception'] = [ + { + 'type': '${exception.runtimeType}', + 'value': '$exception', + } + ]; + } + + if (level != null) json['level'] = level.name; + + if (culprit != null) json['culprit'] = culprit; + + if (serverName != null) json['server_name'] = serverName; + + if (release != null) json['release'] = release; + + if (tags != null && tags.isNotEmpty) json['tags'] = tags; + + if (environment != null) json['environment'] = environment; + + if (extra != null && extra.isNotEmpty) json['extra'] = extra; + + if (fingerprint != null && fingerprint.isNotEmpty) + json['fingerprint'] = fingerprint; + + return json; + } } diff --git a/lib/src/version.dart b/lib/src/version.dart index 9e56f0d406f..f81cfa7ce3e 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -2,6 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// The version of the Sentry for Dart SDK (it's just a library but Sentry.io -/// prefers calling these things SDK). +/// Sentry.io has a concept of "SDK", which refers to the client library or +/// tool used to submit events to Sentry.io. +/// +/// This library contains Sentry.io SDK constants used by this package. +library version; + +/// The SDK version reported to Sentry.io in the submitted events. const String sdkVersion = '0.0.1'; + +/// The SDK name reported to Sentry.io in the submitted events. +const String sdkName = 'dart'; + +/// The name of the SDK platform reported to Sentry.io in the submitted events. +const String sdkPlatform = 'dart'; diff --git a/test/sentry_test.dart b/test/sentry_test.dart index f1229563bfe..375c62110e2 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -54,7 +54,7 @@ void main() { expect(postUri, client.postUri); expect(headers, { - 'User-Agent': '${SentryClient.sdkName}/$sdkVersion', + 'User-Agent': '$sdkName/$sdkVersion', 'Content-Type': 'application/json', 'X-Sentry-Auth': 'Sentry sentry_version=6, ' 'sentry_client=${SentryClient.sentryClient}, ' @@ -67,17 +67,67 @@ void main() { 'project': '1', 'event_id': 'X' * 32, 'timestamp': '2017-01-02T00:00:00.000', - 'message': 'Invalid argument(s): Test error', 'platform': 'dart', 'exception': [ {'type': 'ArgumentError', 'value': 'Invalid argument(s): Test error'} ], - 'sdk': {'version': '0.0.1', 'name': 'dart'} + 'sdk': {'version': '0.0.1', 'name': 'dart'}, + 'logger': SentryClient.defaultLoggerName, }); await client.close(); }); }); + + group('$Event', () { + test('serializes to JSON', () { + final DateTime now = new DateTime(2017); + expect( + new Event( + projectId: '123', + eventId: 'X' * 32, + timestamp: now, + loggerName: 'test-logger', + message: 'test-message', + exception: new StateError('test-error'), + level: SeverityLevel.debug, + culprit: 'Professor Moriarty', + serverName: 'test.server.com', + release: '1.2.3', + tags: { + 'a': 'b', + 'c': 'd', + }, + environment: 'staging', + extra: { + 'e': 'f', + 'g': 2, + }, + fingerprint: [Event.defaultFingerprint, 'foo'], + ).toJson(), + { + 'project': '123', + 'event_id': 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + 'timestamp': '2017-01-01T00:00:00.000', + 'platform': 'dart', + 'sdk': {'version': '0.0.1', 'name': 'dart'}, + 'logger': 'test-logger', + 'message': 'test-message', + 'exception': [ + {'type': 'StateError', 'value': 'Bad state: test-error'} + ], + 'level': 'debug', + 'culprit': 'Professor Moriarty', + 'server_name': 'test.server.com', + 'release': '1.2.3', + 'tags': {'a': 'b', 'c': 'd'}, + 'environment': 'staging', + 'extra': {'e': 'f', 'g': 2}, + 'fingerprint': ['{{ default }}', 'foo'], + }, + ); + }); + }); } class MockClient extends Mock implements Client {} diff --git a/tools/presubmit.sh b/tools/presubmit.sh new file mode 100755 index 00000000000..898a89e8a67 --- /dev/null +++ b/tools/presubmit.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e +set -x + +dartanalyzer --strong --fatal-warnings ./ +pub run test --platform vm +dartfmt -n --set-exit-if-changed ./ From cc502dd1bdcea9700fe9b82766a1eba39d19617f Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Jun 2017 18:33:34 -0700 Subject: [PATCH 07/38] rename tools => tool; make it pub publisheable --- .idea/sentry.iml | 1 + pubspec.yaml | 17 ++++++++++------- {tools => tool}/presubmit.sh | 0 3 files changed, 11 insertions(+), 7 deletions(-) rename {tools => tool}/presubmit.sh (100%) diff --git a/.idea/sentry.iml b/.idea/sentry.iml index 5dde51be9a4..7457fc5904d 100644 --- a/.idea/sentry.iml +++ b/.idea/sentry.iml @@ -8,6 +8,7 @@ + diff --git a/pubspec.yaml b/pubspec.yaml index 496469e1bf0..e46680350da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,14 +1,17 @@ name: sentry version: 0.0.1 description: A pure Dart Sentry.io client. +author: Yegor Jbanov +homepage: https://github.com/yjbanov/sentry dependencies: - meta: ^1.0.5 - quiver: ^0.25.0 - usage: ^3.1.1 + http: ">=0.11.3+13 <2.0.0" + meta: ">=1.0.5 <2.0.0" + quiver: ">=0.25.0 <2.0.0" + usage: ">=3.1.1 <4.0.0" dev_dependencies: - args: ^0.13.7 - test: ^0.12.21 - yaml: ^2.1.12 - mockito: ^2.0.2 + args: ">=0.13.7 <2.0.0" + test: ">=0.12.21 <2.0.0" + yaml: ">=2.1.12 <3.0.0" + mockito: ">=2.0.2 <3.0.0" diff --git a/tools/presubmit.sh b/tool/presubmit.sh similarity index 100% rename from tools/presubmit.sh rename to tool/presubmit.sh From 537701069ff284c72a0aeea411706799a6a9d52c Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Jun 2017 20:51:50 -0700 Subject: [PATCH 08/38] compress payloads by default --- lib/sentry.dart | 35 ++++++++++++++++++++++++++--------- lib/src/version.dart | 2 +- pubspec.yaml | 2 +- test/sentry_test.dart | 31 ++++++++++++++++++++++++++----- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/lib/sentry.dart b/lib/sentry.dart index cf3621d2bee..8ceb2b3f0c7 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -8,6 +8,7 @@ library sentry; import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart'; import 'package:meta/meta.dart'; import 'package:quiver/time.dart'; @@ -41,11 +42,13 @@ class SentryClient { Client httpClient, Clock clock, UuidGenerator uuidGenerator, + bool compressPayload, }) { httpClient ??= new Client(); clock ??= const Clock(); uuidGenerator ??= _generateUuidV4WithoutDashes; loggerName ??= defaultLoggerName; + compressPayload ??= true; final Uri uri = Uri.parse(dsn); final List userInfo = uri.userInfo.split(':'); @@ -75,18 +78,20 @@ class SentryClient { secretKey: secretKey, projectId: projectId, loggerName: loggerName, + compressPayload: compressPayload, ); } SentryClient._({ - Client httpClient, - Clock clock, - UuidGenerator uuidGenerator, - this.dsnUri, - this.publicKey, - this.secretKey, - this.projectId, - this.loggerName, + @required Client httpClient, + @required Clock clock, + @required UuidGenerator uuidGenerator, + @required this.dsnUri, + @required this.publicKey, + @required this.secretKey, + @required this.projectId, + @required this.loggerName, + @required this.compressPayload, }) : _httpClient = httpClient, _clock = clock, @@ -96,8 +101,14 @@ class SentryClient { final Clock _clock; final UuidGenerator _uuidGenerator; + /// The logger that logged the event. + /// + /// If not specified [SentryClient.defaultLoggerName] is used. final String loggerName; + /// Whether to compress payloads sent to Sentry.io. + final bool compressPayload; + /// The DSN URI. @visibleForTesting final Uri dsnUri; @@ -130,7 +141,13 @@ class SentryClient { 'sentry_key=$publicKey, ' 'sentry_secret=$secretKey', }; - final String body = JSON.encode(event.toJson()); + + List body = UTF8.encode(JSON.encode(event.toJson())); + if (compressPayload) { + headers['Content-Encoding'] = 'gzip'; + body = GZIP.encode(body); + } + final Response response = await _httpClient.post(postUri, headers: headers, body: body); diff --git a/lib/src/version.dart b/lib/src/version.dart index f81cfa7ce3e..107d9c0001a 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '0.0.1'; +const String sdkVersion = '0.0.2'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index e46680350da..ec91f812b55 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 0.0.1 +version: 0.0.2 description: A pure Dart Sentry.io client. author: Yegor Jbanov homepage: https://github.com/yjbanov/sentry diff --git a/test/sentry_test.dart b/test/sentry_test.dart index 375c62110e2..eebbb7853a1 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -4,6 +4,7 @@ import 'dart:convert'; +import 'dart:io'; import 'package:http/http.dart'; import 'package:mockito/mockito.dart'; import 'package:quiver/time.dart'; @@ -24,13 +25,13 @@ void main() { await client.close(); }); - test('sends an exception report', () async { + testCaptureException(bool compressPayload) async { final MockClient httpMock = new MockClient(); final Clock fakeClock = new Clock.fixed(new DateTime(2017, 1, 2)); String postUri; Map headers; - String body; + List body; when(httpMock.post(any, headers: any, body: any)) .thenAnswer((Invocation invocation) { postUri = invocation.positionalArguments.single; @@ -44,6 +45,7 @@ void main() { httpClient: httpMock, clock: fakeClock, uuidGenerator: () => 'X' * 32, + compressPayload: compressPayload, ); try { @@ -53,7 +55,8 @@ void main() { } expect(postUri, client.postUri); - expect(headers, { + + Map expectedHeaders = { 'User-Agent': '$sdkName/$sdkVersion', 'Content-Type': 'application/json', 'X-Sentry-Auth': 'Sentry sentry_version=6, ' @@ -61,9 +64,19 @@ void main() { 'sentry_timestamp=${fakeClock.now().millisecondsSinceEpoch}, ' 'sentry_key=public, ' 'sentry_secret=secret', - }); + }; + + if (compressPayload) expectedHeaders['Content-Encoding'] = 'gzip'; + + expect(headers, expectedHeaders); - expect(JSON.decode(body), { + String json; + if (compressPayload) { + json = UTF8.decode(GZIP.decode(body)); + } else { + json = UTF8.decode(body); + } + expect(JSON.decode(json), { 'project': '1', 'event_id': 'X' * 32, 'timestamp': '2017-01-02T00:00:00.000', @@ -76,6 +89,14 @@ void main() { }); await client.close(); + } + + test('sends an exception report (compressed)', () async { + await testCaptureException(true); + }); + + test('sends an exception report (uncompressed)', () async { + await testCaptureException(false); }); }); From 069f434fb1426134e71c07bac23218a4811e7242 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Jun 2017 20:54:47 -0700 Subject: [PATCH 09/38] unhardcode version number in test --- test/sentry_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/sentry_test.dart b/test/sentry_test.dart index eebbb7853a1..28859d687a8 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -84,7 +84,7 @@ void main() { 'exception': [ {'type': 'ArgumentError', 'value': 'Invalid argument(s): Test error'} ], - 'sdk': {'version': '0.0.1', 'name': 'dart'}, + 'sdk': {'version': sdkVersion, 'name': 'dart'}, 'logger': SentryClient.defaultLoggerName, }); @@ -131,7 +131,7 @@ void main() { 'event_id': 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'timestamp': '2017-01-01T00:00:00.000', 'platform': 'dart', - 'sdk': {'version': '0.0.1', 'name': 'dart'}, + 'sdk': {'version': sdkVersion, 'name': 'dart'}, 'logger': 'test-logger', 'message': 'test-message', 'exception': [ From 1c7a9845ef7c45699a3a1563282055ce7ddddf90 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Jun 2017 21:04:37 -0700 Subject: [PATCH 10/38] move environment-specific attributes from Event to SentryClient --- lib/sentry.dart | 81 +++++++++++++++++++++---------------------- test/sentry_test.dart | 16 ++++----- 2 files changed, 45 insertions(+), 52 deletions(-) diff --git a/lib/sentry.dart b/lib/sentry.dart index 8ceb2b3f0c7..57e9f71b427 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -43,6 +43,9 @@ class SentryClient { Clock clock, UuidGenerator uuidGenerator, bool compressPayload, + String serverName, + String release, + String environment, }) { httpClient ??= new Client(); clock ??= const Clock(); @@ -79,6 +82,9 @@ class SentryClient { projectId: projectId, loggerName: loggerName, compressPayload: compressPayload, + serverName: serverName, + release: release, + environment: environment, ); } @@ -89,9 +95,12 @@ class SentryClient { @required this.dsnUri, @required this.publicKey, @required this.secretKey, + @required this.compressPayload, @required this.projectId, @required this.loggerName, - @required this.compressPayload, + @required this.serverName, + @required this.release, + @required this.environment, }) : _httpClient = httpClient, _clock = clock, @@ -101,11 +110,6 @@ class SentryClient { final Clock _clock; final UuidGenerator _uuidGenerator; - /// The logger that logged the event. - /// - /// If not specified [SentryClient.defaultLoggerName] is used. - final String loggerName; - /// Whether to compress payloads sent to Sentry.io. final bool compressPayload; @@ -121,10 +125,27 @@ class SentryClient { @visibleForTesting final String secretKey; - /// The Sentry.io project identifier. - @visibleForTesting + /// The ID issued by Sentry.io to your project. + /// + /// Attached to the event payload. final String projectId; + /// The logger that logged the event. + /// + /// Attached to the event payload. + /// + /// If not specified [SentryClient.defaultLoggerName] is used. + final String loggerName; + + /// Identifies the server that logged this event. + final String serverName; + + /// The version of the application that logged the event. + final String release; + + /// The environment that logged the event, e.g. "production", "staging". + final String environment; + @visibleForTesting String get postUri => '${dsnUri.scheme}://${dsnUri.host}/api/$projectId/store/'; @@ -142,7 +163,16 @@ class SentryClient { 'sentry_secret=$secretKey', }; - List body = UTF8.encode(JSON.encode(event.toJson())); + Map json = { + 'project': projectId, + }; + json['logger'] = loggerName ?? SentryClient.defaultLoggerName; + if (serverName != null) json['server_name'] = serverName; + if (release != null) json['release'] = release; + if (environment != null) json['environment'] = environment; + json.addAll(event.toJson()); + + List body = UTF8.encode(JSON.encode(json)); if (compressPayload) { headers['Content-Encoding'] = 'gzip'; body = GZIP.encode(body); @@ -166,11 +196,9 @@ class SentryClient { dynamic stackTrace, }) { final Event event = new Event( - projectId: projectId, eventId: _uuidGenerator(), timestamp: _clock.now(), exception: exception, - loggerName: loggerName, ); return capture(event: event); } @@ -234,36 +262,23 @@ class Event { /// Creates an event. Event({ - @required this.projectId, @required this.eventId, @required this.timestamp, - this.loggerName, this.message, this.exception, this.level, this.culprit, - this.serverName, - this.release, this.tags, - this.environment, this.extra, this.fingerprint, }); - /// The ID issued by Sentry.io to your project. - final String projectId; - /// A 32-character long UUID v4 value without dashes. final String eventId; /// The time the event happened. final DateTime timestamp; - /// The logger that logged the event. - /// - /// If not specified [SentryClient.defaultLoggerName] is used. - final String loggerName; - /// Event message. /// /// Generally an event either contains a [message] or an [exception]. @@ -281,18 +296,9 @@ class Event { /// What caused this event to be logged. final String culprit; - /// Identifies the server that logged this event. - final String serverName; - - /// The version of the application that logged the event. - final String release; - /// Name/value pairs that events can be searched by. final Map tags; - /// The environment that logged the event, e.g. "production", "staging". - final String environment; - /// Arbitrary name/value pairs attached to the event. /// /// Sentry.io docs do not talk about restrictions on the values, other than @@ -318,7 +324,6 @@ class Event { /// Serializes this event to JSON. Map toJson() { final Map json = { - 'project': projectId, 'event_id': eventId, 'timestamp': timestamp.toIso8601String(), 'platform': sdkPlatform, @@ -328,8 +333,6 @@ class Event { }, }; - json['logger'] = loggerName ?? SentryClient.defaultLoggerName; - if (message != null) json['message'] = message; if (exception != null) { @@ -345,14 +348,8 @@ class Event { if (culprit != null) json['culprit'] = culprit; - if (serverName != null) json['server_name'] = serverName; - - if (release != null) json['release'] = release; - if (tags != null && tags.isNotEmpty) json['tags'] = tags; - if (environment != null) json['environment'] = environment; - if (extra != null && extra.isNotEmpty) json['extra'] = extra; if (fingerprint != null && fingerprint.isNotEmpty) diff --git a/test/sentry_test.dart b/test/sentry_test.dart index 28859d687a8..ce8a41dfbc7 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -46,6 +46,9 @@ void main() { clock: fakeClock, uuidGenerator: () => 'X' * 32, compressPayload: compressPayload, + serverName: 'test.server.com', + release: '1.2.3', + environment: 'staging', ); try { @@ -86,6 +89,9 @@ void main() { ], 'sdk': {'version': sdkVersion, 'name': 'dart'}, 'logger': SentryClient.defaultLoggerName, + 'server_name': 'test.server.com', + 'release': '1.2.3', + 'environment': 'staging', }); await client.close(); @@ -105,21 +111,16 @@ void main() { final DateTime now = new DateTime(2017); expect( new Event( - projectId: '123', eventId: 'X' * 32, timestamp: now, - loggerName: 'test-logger', message: 'test-message', exception: new StateError('test-error'), level: SeverityLevel.debug, culprit: 'Professor Moriarty', - serverName: 'test.server.com', - release: '1.2.3', tags: { 'a': 'b', 'c': 'd', }, - environment: 'staging', extra: { 'e': 'f', 'g': 2, @@ -127,22 +128,17 @@ void main() { fingerprint: [Event.defaultFingerprint, 'foo'], ).toJson(), { - 'project': '123', 'event_id': 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'timestamp': '2017-01-01T00:00:00.000', 'platform': 'dart', 'sdk': {'version': sdkVersion, 'name': 'dart'}, - 'logger': 'test-logger', 'message': 'test-message', 'exception': [ {'type': 'StateError', 'value': 'Bad state: test-error'} ], 'level': 'debug', 'culprit': 'Professor Moriarty', - 'server_name': 'test.server.com', - 'release': '1.2.3', 'tags': {'a': 'b', 'c': 'd'}, - 'environment': 'staging', 'extra': {'e': 'f', 'g': 2}, 'fingerprint': ['{{ default }}', 'foo'], }, From f039e9d9b0889395a8f9f7d583a754d845542281 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 21 Jun 2017 10:39:55 -0700 Subject: [PATCH 11/38] implement x-sentry-error header; enforce immutability --- lib/sentry.dart | 15 ++++++++++++-- test/sentry_test.dart | 48 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/lib/sentry.dart b/lib/sentry.dart index 57e9f71b427..ba1706b1b7d 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -182,8 +182,11 @@ class SentryClient { await _httpClient.post(postUri, headers: headers, body: body); if (response.statusCode != 200) { - return new SentryResponse.failure( - 'Server responded with HTTP ${response.statusCode}'); + String errorMessage = + 'Sentry.io responded with HTTP ${response.statusCode}'; + if (response.headers['x-sentry-error'] != null) + errorMessage += ': ${response.headers['x-sentry-error']}'; + return new SentryResponse.failure(errorMessage); } final String eventId = JSON.decode(response.body)['id']; @@ -211,6 +214,12 @@ class SentryClient { String toString() => '$SentryClient("$postUri")'; } +/// A response from Sentry.io. +/// +/// If [isSuccessful] the [eventId] field will contain the ID assigned to the +/// captured event by the Sentry.io backend. Otherwise, the [error] field will +/// contain the description of the error. +@immutable class SentryResponse { SentryResponse.success({@required eventId}) : isSuccessful = true, @@ -239,6 +248,7 @@ String _generateUuidV4WithoutDashes() { } /// Severity of the logged [Event]. +@immutable class SeverityLevel { static const fatal = const SeverityLevel._('fatal'); static const error = const SeverityLevel._('error'); @@ -253,6 +263,7 @@ class SeverityLevel { } /// An event to be reported to Sentry.io. +@immutable class Event { /// Refers to the default fingerprinting algorithm. /// diff --git a/test/sentry_test.dart b/test/sentry_test.dart index ce8a41dfbc7..eef511f3b15 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -54,7 +54,11 @@ void main() { try { throw new ArgumentError('Test error'); } catch (error, stackTrace) { - await client.captureException(exception: error, stackTrace: stackTrace); + final SentryResponse response = await client.captureException( + exception: error, stackTrace: stackTrace); + expect(response.isSuccessful, true); + expect(response.eventId, 'test-event-id'); + expect(response.error, null); } expect(postUri, client.postUri); @@ -104,6 +108,48 @@ void main() { test('sends an exception report (uncompressed)', () async { await testCaptureException(false); }); + + test('reads error message from the x-sentry-error header', () async { + final MockClient httpMock = new MockClient(); + final Clock fakeClock = new Clock.fixed(new DateTime(2017, 1, 2)); + + String postUri; + Map headers; + List body; + when(httpMock.post(any, headers: any, body: any)) + .thenAnswer((Invocation invocation) { + postUri = invocation.positionalArguments.single; + headers = invocation.namedArguments[#headers]; + body = invocation.namedArguments[#body]; + return new Response('', 401, headers: { + 'x-sentry-error': 'Invalid api key', + }); + }); + + final SentryClient client = new SentryClient( + dsn: _testDsn, + httpClient: httpMock, + clock: fakeClock, + uuidGenerator: () => 'X' * 32, + compressPayload: false, + serverName: 'test.server.com', + release: '1.2.3', + environment: 'staging', + ); + + try { + throw new ArgumentError('Test error'); + } catch (error, stackTrace) { + final SentryResponse response = await client.captureException( + exception: error, stackTrace: stackTrace); + expect(response.isSuccessful, false); + expect(response.eventId, null); + expect(response.error, + 'Sentry.io responded with HTTP 401: Invalid api key'); + } + + await client.close(); + }); }); group('$Event', () { From f935a8cc28bd03f267413d047e9e215e1943bb03 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 21 Jun 2017 15:05:26 -0700 Subject: [PATCH 12/38] parse and report stack traces --- CHANGELOG.md | 11 +++++++++++ lib/sentry.dart | 21 +++++++++++++++++++++ lib/src/stack_trace.dart | 34 ++++++++++++++++++++++++++++++++++ pubspec.yaml | 1 + test/sentry_test.dart | 27 +++++++++++++++++---------- test/stack_trace_test.dart | 28 ++++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 lib/src/stack_trace.dart create mode 100644 test/stack_trace_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000000..71357d7b083 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# package:sentry changelog + +## 0.0.2 + +- parse and report stack traces +- use x-sentry-error HTTP response header +- gzip outgoing payloads by default + +## 0.0.1 + +- basic ability to send exception reports to Sentry.io diff --git a/lib/sentry.dart b/lib/sentry.dart index ba1706b1b7d..15bb9b15608 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -12,9 +12,12 @@ import 'dart:io'; import 'package:http/http.dart'; import 'package:meta/meta.dart'; import 'package:quiver/time.dart'; +import 'package:stack_trace/stack_trace.dart'; import 'package:usage/uuid/uuid.dart'; +import 'src/stack_trace.dart'; import 'src/version.dart'; + export 'src/version.dart'; /// Logs crash reports and events to the Sentry.io service. @@ -202,6 +205,7 @@ class SentryClient { eventId: _uuidGenerator(), timestamp: _clock.now(), exception: exception, + stackTrace: stackTrace, ); return capture(event: event); } @@ -277,6 +281,7 @@ class Event { @required this.timestamp, this.message, this.exception, + this.stackTrace, this.level, this.culprit, this.tags, @@ -301,6 +306,11 @@ class Event { /// undesirable, consider using a custom formatted [message] instead. final dynamic exception; + /// The stack trace corresponding to the thrown [exception]. + /// + /// Can be `null`, a [String], or a [StackTrace]. + final dynamic stackTrace; + /// How important this event is. final SeverityLevel level; @@ -355,6 +365,17 @@ class Event { ]; } + if (stackTrace != null) { + assert(stackTrace is String || stackTrace is StackTrace); + final Trace trace = stackTrace is StackTrace + ? new Trace.from(stackTrace) + : new Trace.parse(stackTrace); + + json['stacktrace'] = { + 'frames': trace.frames.map(stackTraceFrameToJsonFrame).toList(), + }; + } + if (level != null) json['level'] = level.name; if (culprit != null) json['culprit'] = culprit; diff --git a/lib/src/stack_trace.dart b/lib/src/stack_trace.dart new file mode 100644 index 00000000000..2f7d73d028f --- /dev/null +++ b/lib/src/stack_trace.dart @@ -0,0 +1,34 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:stack_trace/stack_trace.dart'; + +Map stackTraceFrameToJsonFrame(Frame frame) { + final Map json = { + 'abs_path': _absolutePathForCrashReport(frame), + 'function': frame.member, + 'lineno': frame.line, + 'in_app': !frame.isCore, + }; + + if (frame.uri.pathSegments.isNotEmpty) + json['filename'] = frame.uri.pathSegments.last; + + return json; +} + +/// A stack frame's code path may be one of "file:", "dart:" and "package:". +/// +/// Absolute file paths may contain personally identifiable information, and +/// therefore are stripped to only send the base file name. For example, +/// "/foo/bar/baz.dart" is reported as "baz.dart". +/// +/// "dart:" and "package:" imports are always relative and are OK to send in +/// full. +String _absolutePathForCrashReport(Frame frame) { + if (frame.uri.scheme != 'dart' && frame.uri.scheme != 'package') + return frame.uri.pathSegments.last; + + return '${frame.uri}'; +} diff --git a/pubspec.yaml b/pubspec.yaml index ec91f812b55..44ed2c2685b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ dependencies: http: ">=0.11.3+13 <2.0.0" meta: ">=1.0.5 <2.0.0" quiver: ">=0.25.0 <2.0.0" + stack_trace: ">=1.7.3 <2.0.0" usage: ">=3.1.1 <4.0.0" dev_dependencies: diff --git a/test/sentry_test.dart b/test/sentry_test.dart index eef511f3b15..0ec324a5dc2 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -77,13 +77,26 @@ void main() { expect(headers, expectedHeaders); - String json; + Map json; if (compressPayload) { - json = UTF8.decode(GZIP.decode(body)); + json = JSON.decode(UTF8.decode(GZIP.decode(body))); } else { - json = UTF8.decode(body); + json = JSON.decode(UTF8.decode(body)); } - expect(JSON.decode(json), { + final Map stacktrace = json.remove('stacktrace'); + expect(stacktrace['frames'], new isInstanceOf()); + expect(stacktrace['frames'], isNotEmpty); + + final Map topFrame = stacktrace['frames'].first; + expect(topFrame.keys, + ['abs_path', 'function', 'lineno', 'in_app', 'filename']); + expect(topFrame['abs_path'], 'sentry_test.dart'); + expect(topFrame['function'], 'main..testCaptureException'); + expect(topFrame['lineno'], greaterThan(0)); + expect(topFrame['in_app'], true); + expect(topFrame['filename'], 'sentry_test.dart'); + + expect(json, { 'project': '1', 'event_id': 'X' * 32, 'timestamp': '2017-01-02T00:00:00.000', @@ -113,14 +126,8 @@ void main() { final MockClient httpMock = new MockClient(); final Clock fakeClock = new Clock.fixed(new DateTime(2017, 1, 2)); - String postUri; - Map headers; - List body; when(httpMock.post(any, headers: any, body: any)) .thenAnswer((Invocation invocation) { - postUri = invocation.positionalArguments.single; - headers = invocation.namedArguments[#headers]; - body = invocation.namedArguments[#body]; return new Response('', 401, headers: { 'x-sentry-error': 'Invalid api key', }); diff --git a/test/stack_trace_test.dart b/test/stack_trace_test.dart new file mode 100644 index 00000000000..248b115ea2d --- /dev/null +++ b/test/stack_trace_test.dart @@ -0,0 +1,28 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:sentry/src/stack_trace.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:test/test.dart'; + +void main() { + group('stackTraceFrameToJsonFrame', () { + test('marks dart: frames as not app frames', () { + final Frame frame = new Frame(Uri.parse('dart:core'), 1, 2, 'buzz'); + expect(stackTraceFrameToJsonFrame(frame), { + 'abs_path': 'dart:core', + 'function': 'buzz', + 'lineno': 1, + 'in_app': false, + 'filename': 'core' + }); + }); + + test('cleanses absolute paths', () { + final Frame frame = + new Frame(Uri.parse('file://foo/bar/baz.dart'), 1, 2, 'buzz'); + expect(stackTraceFrameToJsonFrame(frame)['abs_path'], 'baz.dart'); + }); + }); +} From 0540fc0117d7ce08a5128b50c8036e8cc2bd5c09 Mon Sep 17 00:00:00 2001 From: Yegor Date: Wed, 21 Jun 2017 15:27:03 -0700 Subject: [PATCH 13/38] populate README with useful stuff (#1) --- README.md | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e7c970c5ca..8f1d41e88a9 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,51 @@ [![Build Status](https://travis-ci.org/yjbanov/sentry.svg?branch=master)](https://travis-ci.org/yjbanov/sentry) -## WARNING: experimental code +**WARNING: experimental code** + +Use this library in your Dart programs (Flutter, command-line and (TBD) AngularDart) to report errors thrown by your +program to https://sentry.io error tracking service. + +## Usage + +Sign up for a Sentry.io account and get a DSN at http://sentry.io. + +Add `sentry` dependency to your `pubspec.yaml`: + +```yaml +dependencies: + sentry: any +``` + +In your Dart code, import `package:sentry/sentry.dart` and create a `SentryClient` using the DSN issued by Sentry.io: + +```dart +import 'package:sentry/sentry.dart'; + +final SentryClient sentry = new SentryClient(dsn: YOUR_DSN); +``` + +In an exception handler, call `captureException()`: + +```dart +main() async { + try { + doSomethingThatMightThrowAnError(); + } catch(error, stackTrace) { + await sentry.captureException( + exception: error, + stackTrace: stackTrace, + ); + } +} +``` + +## Tips for catching errors + +- use a `try/catch` block +- create a `Zone` with an error handler, e.g. using [runZoned][run_zoned] +- in Flutter, use [FlutterError.onError][flutter_error] +- use `Isolate.current.addErrorListener` to capture uncaught errors in the root zone + +[run_zoned]: https://api.dartlang.org/stable/1.24.1/dart-async/runZoned.html +[flutter_error]: https://docs.flutter.io/flutter/foundation/FlutterError/onError.html From aaa90bf23e4262d434ec6ae4ba3b97b40b0cb7f2 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 21 Jun 2017 17:40:21 -0700 Subject: [PATCH 14/38] auto-generate event_id and timestamp for events --- lib/sentry.dart | 14 ++------------ test/sentry_test.dart | 5 ----- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/lib/sentry.dart b/lib/sentry.dart index 15bb9b15608..844e67485fd 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -168,6 +168,8 @@ class SentryClient { Map json = { 'project': projectId, + 'event_id': _uuidGenerator(), + 'timestamp': _clock.now().toIso8601String(), }; json['logger'] = loggerName ?? SentryClient.defaultLoggerName; if (serverName != null) json['server_name'] = serverName; @@ -202,8 +204,6 @@ class SentryClient { dynamic stackTrace, }) { final Event event = new Event( - eventId: _uuidGenerator(), - timestamp: _clock.now(), exception: exception, stackTrace: stackTrace, ); @@ -277,8 +277,6 @@ class Event { /// Creates an event. Event({ - @required this.eventId, - @required this.timestamp, this.message, this.exception, this.stackTrace, @@ -289,12 +287,6 @@ class Event { this.fingerprint, }); - /// A 32-character long UUID v4 value without dashes. - final String eventId; - - /// The time the event happened. - final DateTime timestamp; - /// Event message. /// /// Generally an event either contains a [message] or an [exception]. @@ -345,8 +337,6 @@ class Event { /// Serializes this event to JSON. Map toJson() { final Map json = { - 'event_id': eventId, - 'timestamp': timestamp.toIso8601String(), 'platform': sdkPlatform, 'sdk': { 'version': sdkVersion, diff --git a/test/sentry_test.dart b/test/sentry_test.dart index 0ec324a5dc2..f9b085dd7d0 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -161,11 +161,8 @@ void main() { group('$Event', () { test('serializes to JSON', () { - final DateTime now = new DateTime(2017); expect( new Event( - eventId: 'X' * 32, - timestamp: now, message: 'test-message', exception: new StateError('test-error'), level: SeverityLevel.debug, @@ -181,8 +178,6 @@ void main() { fingerprint: [Event.defaultFingerprint, 'foo'], ).toJson(), { - 'event_id': 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - 'timestamp': '2017-01-01T00:00:00.000', 'platform': 'dart', 'sdk': {'version': sdkVersion, 'name': 'dart'}, 'message': 'test-message', From 6230c79281df215b6b819f4b60fae0930df5b446 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Thu, 22 Jun 2017 10:14:16 -0700 Subject: [PATCH 15/38] environment attriutes; v0.0.3 --- CHANGELOG.md | 5 +++ lib/sentry.dart | 99 ++++++++++++++++++++++++++----------------- lib/src/utils.dart | 22 ++++++++++ lib/src/version.dart | 2 +- pubspec.yaml | 2 +- test/sentry_test.dart | 16 ++++--- test/utils_test.dart | 39 +++++++++++++++++ 7 files changed, 138 insertions(+), 47 deletions(-) create mode 100644 lib/src/utils.dart create mode 100644 test/utils_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 71357d7b083..dd24949541d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # package:sentry changelog +## 0.0.3 + +- environment attributes +- auto-generate event_id and timestamp for events + ## 0.0.2 - parse and report stack traces diff --git a/lib/sentry.dart b/lib/sentry.dart index 844e67485fd..9e3ab165011 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -7,8 +7,8 @@ library sentry; import 'dart:async'; import 'dart:convert'; - import 'dart:io'; + import 'package:http/http.dart'; import 'package:meta/meta.dart'; import 'package:quiver/time.dart'; @@ -16,6 +16,7 @@ import 'package:stack_trace/stack_trace.dart'; import 'package:usage/uuid/uuid.dart'; import 'src/stack_trace.dart'; +import 'src/utils.dart'; import 'src/version.dart'; export 'src/version.dart'; @@ -32,28 +33,36 @@ class SentryClient { /// Instantiates a client using [dns] issued to your project by Sentry.io as /// the endpoint for submitting events. /// - /// If [loggerName] is provided, it is used instead of [defaultLoggerName]. + /// [environmentAttributes] contain event attributes that do not change over + /// the course of a program's lifecycle. These attributes will be added to + /// all events captured via this client. The following attributes often fall + /// under this category: [Event.loggerName], [Event.serverName], + /// [Event.release], [Event.environment]. + /// + /// If [compressPayload] is `true` the outgoing HTTP payloads are compressed + /// using gzip. Otherwise, the payloads are sent in plain UTF8-encoded JSON + /// text. If not specified, the compression is enabled by default. /// /// If [httpClient] is provided, it is used instead of the default client to - /// make HTTP calls to Sentry.io. + /// make HTTP calls to Sentry.io. This is useful in tests. /// /// If [clock] is provided, it is used to get time instead of the system - /// clock. + /// clock. This is useful in tests. + /// + /// If [uuidGenerator] is provided, it is used to generate the "event_id" + /// field instead of the built-in random UUID v4 generator. This is useful in + /// tests. factory SentryClient({ @required String dsn, - String loggerName, + Event environmentAttributes, + bool compressPayload, Client httpClient, Clock clock, UuidGenerator uuidGenerator, - bool compressPayload, - String serverName, - String release, - String environment, }) { httpClient ??= new Client(); clock ??= const Clock(); uuidGenerator ??= _generateUuidV4WithoutDashes; - loggerName ??= defaultLoggerName; compressPayload ??= true; final Uri uri = Uri.parse(dsn); @@ -79,15 +88,12 @@ class SentryClient { httpClient: httpClient, clock: clock, uuidGenerator: uuidGenerator, + environmentAttributes: environmentAttributes, dsnUri: uri, publicKey: publicKey, secretKey: secretKey, projectId: projectId, - loggerName: loggerName, compressPayload: compressPayload, - serverName: serverName, - release: release, - environment: environment, ); } @@ -95,15 +101,12 @@ class SentryClient { @required Client httpClient, @required Clock clock, @required UuidGenerator uuidGenerator, + @required this.environmentAttributes, @required this.dsnUri, @required this.publicKey, @required this.secretKey, @required this.compressPayload, @required this.projectId, - @required this.loggerName, - @required this.serverName, - @required this.release, - @required this.environment, }) : _httpClient = httpClient, _clock = clock, @@ -113,6 +116,15 @@ class SentryClient { final Clock _clock; final UuidGenerator _uuidGenerator; + /// Contains [Event] attributes that are automatically mixed into all events + /// captured through this client. + /// + /// This event is designed to contain static values that do not change from + /// event to event, such as local operating system version, the version of + /// Dart/Flutter SDK, etc. These attributes have lower precedence than those + /// supplied in the even passed to [capture]. + final Event environmentAttributes; + /// Whether to compress payloads sent to Sentry.io. final bool compressPayload; @@ -133,22 +145,6 @@ class SentryClient { /// Attached to the event payload. final String projectId; - /// The logger that logged the event. - /// - /// Attached to the event payload. - /// - /// If not specified [SentryClient.defaultLoggerName] is used. - final String loggerName; - - /// Identifies the server that logged this event. - final String serverName; - - /// The version of the application that logged the event. - final String release; - - /// The environment that logged the event, e.g. "production", "staging". - final String environment; - @visibleForTesting String get postUri => '${dsnUri.scheme}://${dsnUri.host}/api/$projectId/store/'; @@ -170,12 +166,13 @@ class SentryClient { 'project': projectId, 'event_id': _uuidGenerator(), 'timestamp': _clock.now().toIso8601String(), + 'logger': defaultLoggerName, }; - json['logger'] = loggerName ?? SentryClient.defaultLoggerName; - if (serverName != null) json['server_name'] = serverName; - if (release != null) json['release'] = release; - if (environment != null) json['environment'] = environment; - json.addAll(event.toJson()); + + if (environmentAttributes != null) + mergeAttributes(environmentAttributes.toJson(), into: json); + + mergeAttributes(event.toJson(), into: json); List body = UTF8.encode(JSON.encode(json)); if (compressPayload) { @@ -277,6 +274,10 @@ class Event { /// Creates an event. Event({ + this.loggerName, + this.serverName, + this.release, + this.environment, this.message, this.exception, this.stackTrace, @@ -287,6 +288,18 @@ class Event { this.fingerprint, }); + /// The logger that logged the event. + final String loggerName; + + /// Identifies the server that logged this event. + final String serverName; + + /// The version of the application that logged the event. + final String release; + + /// The environment that logged the event, e.g. "production", "staging". + final String environment; + /// Event message. /// /// Generally an event either contains a [message] or an [exception]. @@ -344,6 +357,14 @@ class Event { }, }; + if (loggerName != null) json['logger'] = loggerName; + + if (serverName != null) json['server_name'] = serverName; + + if (release != null) json['release'] = release; + + if (environment != null) json['environment'] = environment; + if (message != null) json['message'] = message; if (exception != null) { diff --git a/lib/src/utils.dart b/lib/src/utils.dart new file mode 100644 index 00000000000..550b35ec28b --- /dev/null +++ b/lib/src/utils.dart @@ -0,0 +1,22 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +/// Recursively merges [attributes] [into] another map of attributes. +/// +/// [attributes] take precedence over the target map. Recursion takes place +/// along [Map] values only. All other types are overwritten entirely. +void mergeAttributes(Map attributes, + {@required Map into}) { + assert(attributes != null && into != null); + attributes.forEach((String name, dynamic value) { + dynamic targetValue = into[name]; + if (value is Map && targetValue is Map) { + mergeAttributes(value, into: targetValue); + } else { + into[name] = value; + } + }); +} diff --git a/lib/src/version.dart b/lib/src/version.dart index 107d9c0001a..faf41d4d2aa 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '0.0.2'; +const String sdkVersion = '0.0.3'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 44ed2c2685b..19ea6e25f48 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 0.0.2 +version: 0.0.3 description: A pure Dart Sentry.io client. author: Yegor Jbanov homepage: https://github.com/yjbanov/sentry diff --git a/test/sentry_test.dart b/test/sentry_test.dart index f9b085dd7d0..f79d0828daf 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -46,9 +46,11 @@ void main() { clock: fakeClock, uuidGenerator: () => 'X' * 32, compressPayload: compressPayload, - serverName: 'test.server.com', - release: '1.2.3', - environment: 'staging', + environmentAttributes: new Event( + serverName: 'test.server.com', + release: '1.2.3', + environment: 'staging', + ), ); try { @@ -139,9 +141,11 @@ void main() { clock: fakeClock, uuidGenerator: () => 'X' * 32, compressPayload: false, - serverName: 'test.server.com', - release: '1.2.3', - environment: 'staging', + environmentAttributes: new Event( + serverName: 'test.server.com', + release: '1.2.3', + environment: 'staging', + ), ); try { diff --git a/test/utils_test.dart b/test/utils_test.dart new file mode 100644 index 00000000000..3fa1e3eb1c5 --- /dev/null +++ b/test/utils_test.dart @@ -0,0 +1,39 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:test/test.dart'; + +import 'package:sentry/src/utils.dart'; + +void main() { + group('mergeAttributes', () { + test('merges attributes', () { + final Map target = { + 'overwritten': 1, + 'unchanged': 2, + 'recursed': { + 'overwritten_child': [1, 2, 3], + 'unchanged_child': 'qwerty', + }, + }; + + final Map attributes = { + 'overwritten': 2, + 'recursed': { + 'overwritten_child': [4, 5, 6], + }, + }; + + mergeAttributes(attributes, into: target); + expect(target, { + 'overwritten': 2, + 'unchanged': 2, + 'recursed': { + 'overwritten_child': [4, 5, 6], + 'unchanged_child': 'qwerty', + }, + }); + }); + }); +} From b2b5afc6df2864212249fb58b5c2d2223ce0baf7 Mon Sep 17 00:00:00 2001 From: Yegor Date: Mon, 7 Aug 2017 14:43:06 -0700 Subject: [PATCH 16/38] parse and report async gaps (#1) * parse and report async gaps * address comments * dartfmt --- bin/test.dart | 16 ++++++++++- lib/sentry.dart | 8 +----- lib/src/stack_trace.dart | 25 ++++++++++++++++- lib/src/version.dart | 2 +- pubspec.yaml | 2 +- test/stack_trace_test.dart | 56 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 95 insertions(+), 14 deletions(-) diff --git a/bin/test.dart b/bin/test.dart index 0e90193e61f..bbeadbe1fcd 100644 --- a/bin/test.dart +++ b/bin/test.dart @@ -19,8 +19,10 @@ Future main(List rawArgs) async { final SentryClient client = new SentryClient(dsn: dsn); try { - throw new StateError('This is a test error'); + await foo(); } catch (error, stackTrace) { + print('Reporting the following stack trace: '); + print(stackTrace); final SentryResponse response = await client.captureException( exception: error, stackTrace: stackTrace, @@ -35,3 +37,15 @@ Future main(List rawArgs) async { await client.close(); } } + +Future foo() async { + await bar(); +} + +Future bar() async { + await baz(); +} + +Future baz() async { + throw new StateError('This is a test error'); +} diff --git a/lib/sentry.dart b/lib/sentry.dart index 9e3ab165011..a2d4343bbdd 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -12,7 +12,6 @@ import 'dart:io'; import 'package:http/http.dart'; import 'package:meta/meta.dart'; import 'package:quiver/time.dart'; -import 'package:stack_trace/stack_trace.dart'; import 'package:usage/uuid/uuid.dart'; import 'src/stack_trace.dart'; @@ -377,13 +376,8 @@ class Event { } if (stackTrace != null) { - assert(stackTrace is String || stackTrace is StackTrace); - final Trace trace = stackTrace is StackTrace - ? new Trace.from(stackTrace) - : new Trace.parse(stackTrace); - json['stacktrace'] = { - 'frames': trace.frames.map(stackTraceFrameToJsonFrame).toList(), + 'frames': encodeStackTrace(stackTrace), }; } diff --git a/lib/src/stack_trace.dart b/lib/src/stack_trace.dart index 2f7d73d028f..13b1b122ef7 100644 --- a/lib/src/stack_trace.dart +++ b/lib/src/stack_trace.dart @@ -4,7 +4,30 @@ import 'package:stack_trace/stack_trace.dart'; -Map stackTraceFrameToJsonFrame(Frame frame) { +/// Sentry.io JSON encoding of a stack frame for the asynchronous suspension, +/// which is the gap between asynchronous calls. +const Map asynchronousGapFrameJson = const { + 'abs_path': '', +}; + +/// Encodes [strackTrace] as JSON in the Sentry.io format. +/// +/// [stackTrace] must be [String] or [StackTrace]. +List> encodeStackTrace(dynamic stackTrace) { + assert(stackTrace is String || stackTrace is StackTrace); + final Chain chain = stackTrace is StackTrace + ? new Chain.forTrace(stackTrace) + : new Chain.parse(stackTrace); + + final List> frames = >[]; + for (int t = 0; t < chain.traces.length; t += 1) { + frames.addAll(chain.traces[t].frames.map(encodeStackTraceFrame)); + if (t < chain.traces.length - 1) frames.add(asynchronousGapFrameJson); + } + return frames; +} + +Map encodeStackTraceFrame(Frame frame) { final Map json = { 'abs_path': _absolutePathForCrashReport(frame), 'function': frame.member, diff --git a/lib/src/version.dart b/lib/src/version.dart index faf41d4d2aa..1462cfc1ed8 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '0.0.3'; +const String sdkVersion = '0.0.4'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 19ea6e25f48..8ec03bbf180 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 0.0.3 +version: 0.0.4 description: A pure Dart Sentry.io client. author: Yegor Jbanov homepage: https://github.com/yjbanov/sentry diff --git a/test/stack_trace_test.dart b/test/stack_trace_test.dart index 248b115ea2d..eb5ab06f854 100644 --- a/test/stack_trace_test.dart +++ b/test/stack_trace_test.dart @@ -7,10 +7,10 @@ import 'package:stack_trace/stack_trace.dart'; import 'package:test/test.dart'; void main() { - group('stackTraceFrameToJsonFrame', () { + group('encodeStackTraceFrame', () { test('marks dart: frames as not app frames', () { final Frame frame = new Frame(Uri.parse('dart:core'), 1, 2, 'buzz'); - expect(stackTraceFrameToJsonFrame(frame), { + expect(encodeStackTraceFrame(frame), { 'abs_path': 'dart:core', 'function': 'buzz', 'lineno': 1, @@ -22,7 +22,57 @@ void main() { test('cleanses absolute paths', () { final Frame frame = new Frame(Uri.parse('file://foo/bar/baz.dart'), 1, 2, 'buzz'); - expect(stackTraceFrameToJsonFrame(frame)['abs_path'], 'baz.dart'); + expect(encodeStackTraceFrame(frame)['abs_path'], 'baz.dart'); + }); + }); + + group('encodeStackTrace', () { + test('encodes a simple stack trace', () { + expect(encodeStackTrace(''' +#0 baz (file:///pathto/test.dart:50:3) +#1 bar (file:///pathto/test.dart:46:9) + '''), [ + { + 'abs_path': 'test.dart', + 'function': 'baz', + 'lineno': 50, + 'in_app': true, + 'filename': 'test.dart' + }, + { + 'abs_path': 'test.dart', + 'function': 'bar', + 'lineno': 46, + 'in_app': true, + 'filename': 'test.dart' + } + ]); + }); + + test('encodes an asynchronous stack trace', () { + expect(encodeStackTrace(''' +#0 baz (file:///pathto/test.dart:50:3) + +#1 bar (file:///pathto/test.dart:46:9) + '''), [ + { + 'abs_path': 'test.dart', + 'function': 'baz', + 'lineno': 50, + 'in_app': true, + 'filename': 'test.dart' + }, + { + 'abs_path': '', + }, + { + 'abs_path': 'test.dart', + 'function': 'bar', + 'lineno': 46, + 'in_app': true, + 'filename': 'test.dart' + } + ]); }); }); } From 56d544a620791c02ed0ce6844be842407bf11093 Mon Sep 17 00:00:00 2001 From: Yegor Date: Mon, 14 Aug 2017 17:00:12 -0700 Subject: [PATCH 17/38] remove sub-seconds from the timestamp format (#2) --- CHANGELOG.md | 8 ++++++++ lib/sentry.dart | 2 +- lib/src/utils.dart | 8 ++++++++ lib/src/version.dart | 2 +- pubspec.yaml | 2 +- test/sentry_test.dart | 6 +++--- test/utils_test.dart | 10 ++++++++++ 7 files changed, 32 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd24949541d..c7d6950f97a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # package:sentry changelog +## 0.0.5 + +- remove sub-seconds from the timestamp + +## 0.0.4 + +- parse and report async gaps in stack traces + ## 0.0.3 - environment attributes diff --git a/lib/sentry.dart b/lib/sentry.dart index a2d4343bbdd..e65a4b92759 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -164,7 +164,7 @@ class SentryClient { Map json = { 'project': projectId, 'event_id': _uuidGenerator(), - 'timestamp': _clock.now().toIso8601String(), + 'timestamp': formatDateAsIso8601WithSecondPrecision(_clock.now()), 'logger': defaultLoggerName, }; diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 550b35ec28b..d87b26719bd 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -20,3 +20,11 @@ void mergeAttributes(Map attributes, } }); } + +String formatDateAsIso8601WithSecondPrecision(DateTime date) { + String iso = date.toIso8601String(); + final millisecondSeparatorIndex = iso.lastIndexOf('.'); + if (millisecondSeparatorIndex != -1) + iso = iso.substring(0, millisecondSeparatorIndex); + return iso; +} diff --git a/lib/src/version.dart b/lib/src/version.dart index 1462cfc1ed8..7722e1175c0 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '0.0.4'; +const String sdkVersion = '0.0.5'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 8ec03bbf180..f4c68866784 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 0.0.4 +version: 0.0.5 description: A pure Dart Sentry.io client. author: Yegor Jbanov homepage: https://github.com/yjbanov/sentry diff --git a/test/sentry_test.dart b/test/sentry_test.dart index f79d0828daf..a034186349a 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -3,8 +3,8 @@ // found in the LICENSE file. import 'dart:convert'; - import 'dart:io'; + import 'package:http/http.dart'; import 'package:mockito/mockito.dart'; import 'package:quiver/time.dart'; @@ -27,7 +27,7 @@ void main() { testCaptureException(bool compressPayload) async { final MockClient httpMock = new MockClient(); - final Clock fakeClock = new Clock.fixed(new DateTime(2017, 1, 2)); + final Clock fakeClock = new Clock.fixed(new DateTime.utc(2017, 1, 2)); String postUri; Map headers; @@ -101,7 +101,7 @@ void main() { expect(json, { 'project': '1', 'event_id': 'X' * 32, - 'timestamp': '2017-01-02T00:00:00.000', + 'timestamp': '2017-01-02T00:00:00', 'platform': 'dart', 'exception': [ {'type': 'ArgumentError', 'value': 'Invalid argument(s): Test error'} diff --git a/test/utils_test.dart b/test/utils_test.dart index 3fa1e3eb1c5..3ee15130dc4 100644 --- a/test/utils_test.dart +++ b/test/utils_test.dart @@ -36,4 +36,14 @@ void main() { }); }); }); + + group('formatDateAsIso8601WithSecondPrecision', () { + test('strips sub-millisecond parts', () { + final DateTime testDate = + new DateTime.fromMillisecondsSinceEpoch(1502467721598, isUtc: true); + expect(testDate.toIso8601String(), '2017-08-11T16:08:41.598Z'); + expect(formatDateAsIso8601WithSecondPrecision(testDate), + '2017-08-11T16:08:41'); + }); + }); } From e3945aa128227a4ea9da70741fe2201ffc1be8f3 Mon Sep 17 00:00:00 2001 From: Yegor Date: Mon, 14 Aug 2017 17:29:23 -0700 Subject: [PATCH 18/38] update authors and readme (#3) --- AUTHORS | 6 ++++++ README.md | 2 +- pubspec.yaml | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 AUTHORS diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000000..a682da1501c --- /dev/null +++ b/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to package:sentry. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/README.md b/README.md index 8f1d41e88a9..db90a8e097e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sentry.io client for Dart -[![Build Status](https://travis-ci.org/yjbanov/sentry.svg?branch=master)](https://travis-ci.org/yjbanov/sentry) +[![Build Status](https://travis-ci.org/flutter/sentry.svg?branch=master)](https://travis-ci.org/flutter/sentry) **WARNING: experimental code** diff --git a/pubspec.yaml b/pubspec.yaml index f4c68866784..b9ddea4c216 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ name: sentry version: 0.0.5 description: A pure Dart Sentry.io client. -author: Yegor Jbanov -homepage: https://github.com/yjbanov/sentry +author: Flutter Authors +homepage: https://github.com/flutter/sentry dependencies: http: ">=0.11.3+13 <2.0.0" From 0f2131f0f0a80fb2cb437e07a32c0837dce6dc9a Mon Sep 17 00:00:00 2001 From: Yegor Date: Mon, 18 Sep 2017 12:52:19 -0700 Subject: [PATCH 19/38] submit date-time in UTC format (#4) * submit date-time in UTC format * version bump and changelog * change dep version to unblock pub get --- CHANGELOG.md | 4 ++++ README.md | 4 ++++ lib/sentry.dart | 6 +++++- lib/src/version.dart | 2 +- pubspec.yaml | 18 +++++++++--------- tool/presubmit.sh | 1 + 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7d6950f97a..3ce6a392552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # package:sentry changelog +## 0.0.6 + +- use UTC in the `timestamp` field + ## 0.0.5 - remove sub-seconds from the timestamp diff --git a/README.md b/README.md index db90a8e097e..55830291e75 100644 --- a/README.md +++ b/README.md @@ -50,3 +50,7 @@ main() async { [run_zoned]: https://api.dartlang.org/stable/1.24.1/dart-async/runZoned.html [flutter_error]: https://docs.flutter.io/flutter/foundation/FlutterError/onError.html + +## Found a bug? + +Please file it at https://github.com/flutter/flutter/issues/new diff --git a/lib/sentry.dart b/lib/sentry.dart index e65a4b92759..73d97cae55b 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -60,7 +60,7 @@ class SentryClient { UuidGenerator uuidGenerator, }) { httpClient ??= new Client(); - clock ??= const Clock(); + clock ??= const Clock(_getUtcDateTime); uuidGenerator ??= _generateUuidV4WithoutDashes; compressPayload ??= true; @@ -262,6 +262,10 @@ class SeverityLevel { final String name; } +/// Sentry does not take a timezone and instead expects the date-time to be +/// submitted in UTC timezone. +DateTime _getUtcDateTime() => new DateTime.now().toUtc(); + /// An event to be reported to Sentry.io. @immutable class Event { diff --git a/lib/src/version.dart b/lib/src/version.dart index 7722e1175c0..3b44835c506 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '0.0.5'; +const String sdkVersion = '0.0.6'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index b9ddea4c216..89d51108320 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,18 +1,18 @@ name: sentry -version: 0.0.5 +version: 0.0.6 description: A pure Dart Sentry.io client. author: Flutter Authors homepage: https://github.com/flutter/sentry dependencies: - http: ">=0.11.3+13 <2.0.0" - meta: ">=1.0.5 <2.0.0" + http: ">=0.11.0 <2.0.0" + meta: ">=1.0.0 <2.0.0" quiver: ">=0.25.0 <2.0.0" - stack_trace: ">=1.7.3 <2.0.0" - usage: ">=3.1.1 <4.0.0" + stack_trace: ">=1.0.0 <2.0.0" + usage: ">=3.0.0 <4.0.0" dev_dependencies: - args: ">=0.13.7 <2.0.0" - test: ">=0.12.21 <2.0.0" - yaml: ">=2.1.12 <3.0.0" - mockito: ">=2.0.2 <3.0.0" + args: ">=0.13.0 <2.0.0" + test: ">=0.12.0 <2.0.0" + yaml: ">=2.1.0 <3.0.0" + mockito: ">=2.0.0 <3.0.0" diff --git a/tool/presubmit.sh b/tool/presubmit.sh index 898a89e8a67..a50a5f167dd 100755 --- a/tool/presubmit.sh +++ b/tool/presubmit.sh @@ -3,6 +3,7 @@ set -e set -x +pub get dartanalyzer --strong --fatal-warnings ./ pub run test --platform vm dartfmt -n --set-exit-if-changed ./ From 4ed2ee85c8b6d9307ca003e4cee91efff72116a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnter=20Z=C3=B6chbauer?= Date: Fri, 16 Feb 2018 11:54:29 +0100 Subject: [PATCH 20/38] #fixes 14747 assert for Dart 2 (#6) --- lib/sentry.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sentry.dart b/lib/sentry.dart index 73d97cae55b..7dc2e8c96b6 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -77,7 +77,7 @@ class SentryClient { 'Project ID not found in the URI path of the DSN URI: $dsn'); return true; - }); + }()); final String publicKey = userInfo.first; final String secretKey = userInfo.last; From b5dcb6bdbd2405a3eb3e190bd40403bb7c9e3796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnter=20Z=C3=B6chbauer?= Date: Wed, 28 Feb 2018 00:28:40 +0100 Subject: [PATCH 21/38] forgot to update the changelog (#8) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ce6a392552..5c16a0c0efe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # package:sentry changelog +## 0.0.7 + +- fix code for Dart 2 + ## 0.0.6 - use UTC in the `timestamp` field From bcdefdc85525152a29daaf6cbb381b69ccfa3769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnter=20Z=C3=B6chbauer?= Date: Fri, 2 Mar 2018 18:29:02 +0100 Subject: [PATCH 22/38] minor cleanup (#7) --- lib/sentry.dart | 19 ++++++++----------- lib/src/stack_trace.dart | 2 +- lib/src/utils.dart | 2 +- test/sentry_test.dart | 8 ++++---- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/sentry.dart b/lib/sentry.dart index 7dc2e8c96b6..19e873e531a 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -29,7 +29,7 @@ class SentryClient { /// The default logger name used if no other value is supplied. static const String defaultLoggerName = 'SentryClient'; - /// Instantiates a client using [dns] issued to your project by Sentry.io as + /// Instantiates a client using [dsn] issued to your project by Sentry.io as /// the endpoint for submitting events. /// /// [environmentAttributes] contain event attributes that do not change over @@ -161,7 +161,7 @@ class SentryClient { 'sentry_secret=$secretKey', }; - Map json = { + final Map json = { 'project': projectId, 'event_id': _uuidGenerator(), 'timestamp': formatDateAsIso8601WithSecondPrecision(_clock.now()), @@ -221,15 +221,13 @@ class SentryClient { /// contain the description of the error. @immutable class SentryResponse { - SentryResponse.success({@required eventId}) + const SentryResponse.success({@required this.eventId}) : isSuccessful = true, - eventId = eventId, error = null; - SentryResponse.failure(error) + const SentryResponse.failure(this.error) : isSuccessful = false, - eventId = null, - error = error; + eventId = null; /// Whether event was submitted successfully. final bool isSuccessful; @@ -243,9 +241,8 @@ class SentryResponse { typedef UuidGenerator = String Function(); -String _generateUuidV4WithoutDashes() { - return new Uuid().generateV4().replaceAll('-', ''); -} +String _generateUuidV4WithoutDashes() => + new Uuid().generateV4().replaceAll('-', ''); /// Severity of the logged [Event]. @immutable @@ -276,7 +273,7 @@ class Event { static const String defaultFingerprint = '{{ default }}'; /// Creates an event. - Event({ + const Event({ this.loggerName, this.serverName, this.release, diff --git a/lib/src/stack_trace.dart b/lib/src/stack_trace.dart index 13b1b122ef7..ef8ce6218b7 100644 --- a/lib/src/stack_trace.dart +++ b/lib/src/stack_trace.dart @@ -10,7 +10,7 @@ const Map asynchronousGapFrameJson = const { 'abs_path': '', }; -/// Encodes [strackTrace] as JSON in the Sentry.io format. +/// Encodes [stackTrace] as JSON in the Sentry.io format. /// /// [stackTrace] must be [String] or [StackTrace]. List> encodeStackTrace(dynamic stackTrace) { diff --git a/lib/src/utils.dart b/lib/src/utils.dart index d87b26719bd..bcbccf167c0 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -12,7 +12,7 @@ void mergeAttributes(Map attributes, {@required Map into}) { assert(attributes != null && into != null); attributes.forEach((String name, dynamic value) { - dynamic targetValue = into[name]; + final dynamic targetValue = into[name]; if (value is Map && targetValue is Map) { mergeAttributes(value, into: targetValue); } else { diff --git a/test/sentry_test.dart b/test/sentry_test.dart index a034186349a..5313a989097 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -46,7 +46,7 @@ void main() { clock: fakeClock, uuidGenerator: () => 'X' * 32, compressPayload: compressPayload, - environmentAttributes: new Event( + environmentAttributes: const Event( serverName: 'test.server.com', release: '1.2.3', environment: 'staging', @@ -65,7 +65,7 @@ void main() { expect(postUri, client.postUri); - Map expectedHeaders = { + final Map expectedHeaders = { 'User-Agent': '$sdkName/$sdkVersion', 'Content-Type': 'application/json', 'X-Sentry-Auth': 'Sentry sentry_version=6, ' @@ -86,7 +86,7 @@ void main() { json = JSON.decode(UTF8.decode(body)); } final Map stacktrace = json.remove('stacktrace'); - expect(stacktrace['frames'], new isInstanceOf()); + expect(stacktrace['frames'], const isInstanceOf()); expect(stacktrace['frames'], isNotEmpty); final Map topFrame = stacktrace['frames'].first; @@ -141,7 +141,7 @@ void main() { clock: fakeClock, uuidGenerator: () => 'X' * 32, compressPayload: false, - environmentAttributes: new Event( + environmentAttributes: const Event( serverName: 'test.server.com', release: '1.2.3', environment: 'staging', From ee4c346aad678595a5b616a8c7293afb790e5a5a Mon Sep 17 00:00:00 2001 From: Yegor Date: Tue, 20 Mar 2018 14:39:52 -0700 Subject: [PATCH 23/38] release 1.0.0 - the last Dart 1 compatible major version (#11) --- README.md | 8 ++++++-- lib/src/version.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 55830291e75..773c8c5e404 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,15 @@ [![Build Status](https://travis-ci.org/flutter/sentry.svg?branch=master)](https://travis-ci.org/flutter/sentry) -**WARNING: experimental code** - Use this library in your Dart programs (Flutter, command-line and (TBD) AngularDart) to report errors thrown by your program to https://sentry.io error tracking service. +## Versions + +>=0.0.0 and <2.0.0 is the range of versions compatible with Dart 1. + +>=2.0.0 and <3.0.0 is the range of versions compatible with Dart 2. + ## Usage Sign up for a Sentry.io account and get a DSN at http://sentry.io. diff --git a/lib/src/version.dart b/lib/src/version.dart index 3b44835c506..f470521fc3f 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '0.0.6'; +const String sdkVersion = '1.0.0'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 89d51108320..5ed45105942 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 0.0.6 +version: 1.0.0 description: A pure Dart Sentry.io client. author: Flutter Authors homepage: https://github.com/flutter/sentry From a922746f6ee554e64ddb66342a921c8b2a5c10d9 Mon Sep 17 00:00:00 2001 From: Yegor Date: Tue, 20 Mar 2018 14:46:14 -0700 Subject: [PATCH 24/38] 1.0.0 changelog (#12) --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c16a0c0efe..78f257958ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # package:sentry changelog -## 0.0.7 +## 1.0.0 +- first and last Dart 1-compatible release (we may fix bugs on a separate branch if there's demand) - fix code for Dart 2 ## 0.0.6 From a0c252437df2e009c4de4107b79bb88181870e16 Mon Sep 17 00:00:00 2001 From: Anatoly Pulyaevskiy Date: Tue, 20 Mar 2018 20:16:23 -0700 Subject: [PATCH 25/38] Fixes tests and deprecation warnings for Dart2 (#9) * Fixed tests for Dart2 * Run tests with --preview-dart-2 flag * Addressed PR comments * Updated changelog and bumped version to 2.0.0 --- .gitignore | 2 +- CHANGELOG.md | 5 ++++ lib/sentry.dart | 13 +++++----- lib/src/version.dart | 2 +- pubspec.yaml | 2 +- test/sentry_test.dart | 57 +++++++++++++++++++++++++++++-------------- tool/dart2_test.sh | 7 ++++++ tool/presubmit.sh | 1 + 8 files changed, 61 insertions(+), 28 deletions(-) create mode 100755 tool/dart2_test.sh diff --git a/.gitignore b/.gitignore index aeb3c1d58c8..826669578dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .DS_Store .atom/ .packages -.pub/ +.dart_tool/ build/ packages pubspec.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 78f257958ce..45f48e624c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # package:sentry changelog +## 2.0.0 + +- Fixed deprecation warnings for Dart 2 +- Refactored tests to work with Dart 2 + ## 1.0.0 - first and last Dart 1-compatible release (we may fix bugs on a separate branch if there's demand) diff --git a/lib/sentry.dart b/lib/sentry.dart index 19e873e531a..3dbbb93518f 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -106,8 +106,7 @@ class SentryClient { @required this.secretKey, @required this.compressPayload, @required this.projectId, - }) - : _httpClient = httpClient, + }) : _httpClient = httpClient, _clock = clock, _uuidGenerator = uuidGenerator; @@ -161,7 +160,7 @@ class SentryClient { 'sentry_secret=$secretKey', }; - final Map json = { + final Map data = { 'project': projectId, 'event_id': _uuidGenerator(), 'timestamp': formatDateAsIso8601WithSecondPrecision(_clock.now()), @@ -169,11 +168,11 @@ class SentryClient { }; if (environmentAttributes != null) - mergeAttributes(environmentAttributes.toJson(), into: json); + mergeAttributes(environmentAttributes.toJson(), into: data); - mergeAttributes(event.toJson(), into: json); + mergeAttributes(event.toJson(), into: data); - List body = UTF8.encode(JSON.encode(json)); + List body = utf8.encode(json.encode(data)); if (compressPayload) { headers['Content-Encoding'] = 'gzip'; body = GZIP.encode(body); @@ -190,7 +189,7 @@ class SentryClient { return new SentryResponse.failure(errorMessage); } - final String eventId = JSON.decode(response.body)['id']; + final String eventId = json.decode(response.body)['id']; return new SentryResponse.success(eventId: eventId); } diff --git a/lib/src/version.dart b/lib/src/version.dart index f470521fc3f..1cd67219db7 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '1.0.0'; +const String sdkVersion = '2.0.0'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 5ed45105942..ddca8951831 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 1.0.0 +version: 2.0.0 description: A pure Dart Sentry.io client. author: Flutter Authors homepage: https://github.com/flutter/sentry diff --git a/test/sentry_test.dart b/test/sentry_test.dart index 5313a989097..4710b4f555a 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -6,7 +6,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart'; -import 'package:mockito/mockito.dart'; import 'package:quiver/time.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; @@ -32,12 +31,17 @@ void main() { String postUri; Map headers; List body; - when(httpMock.post(any, headers: any, body: any)) - .thenAnswer((Invocation invocation) { - postUri = invocation.positionalArguments.single; - headers = invocation.namedArguments[#headers]; - body = invocation.namedArguments[#body]; - return new Response('{"id": "test-event-id"}', 200); + httpMock.answerWith((Invocation invocation) { + if (invocation.memberName == #close) { + return null; + } + if (invocation.memberName == #post) { + postUri = invocation.positionalArguments.single; + headers = invocation.namedArguments[#headers]; + body = invocation.namedArguments[#body]; + return new Response('{"id": "test-event-id"}', 200); + } + fail('Unexpected invocation of ${invocation.memberName} in HttpMock'); }); final SentryClient client = new SentryClient( @@ -79,13 +83,13 @@ void main() { expect(headers, expectedHeaders); - Map json; + Map data; if (compressPayload) { - json = JSON.decode(UTF8.decode(GZIP.decode(body))); + data = json.decode(utf8.decode(GZIP.decode(body))); } else { - json = JSON.decode(UTF8.decode(body)); + data = json.decode(utf8.decode(body)); } - final Map stacktrace = json.remove('stacktrace'); + final Map stacktrace = data.remove('stacktrace'); expect(stacktrace['frames'], const isInstanceOf()); expect(stacktrace['frames'], isNotEmpty); @@ -98,7 +102,7 @@ void main() { expect(topFrame['in_app'], true); expect(topFrame['filename'], 'sentry_test.dart'); - expect(json, { + expect(data, { 'project': '1', 'event_id': 'X' * 32, 'timestamp': '2017-01-02T00:00:00', @@ -128,11 +132,16 @@ void main() { final MockClient httpMock = new MockClient(); final Clock fakeClock = new Clock.fixed(new DateTime(2017, 1, 2)); - when(httpMock.post(any, headers: any, body: any)) - .thenAnswer((Invocation invocation) { - return new Response('', 401, headers: { - 'x-sentry-error': 'Invalid api key', - }); + httpMock.answerWith((Invocation invocation) { + if (invocation.memberName == #close) { + return null; + } + if (invocation.memberName == #post) { + return new Response('', 401, headers: { + 'x-sentry-error': 'Invalid api key', + }); + } + fail('Unexpected invocation of ${invocation.memberName} in HttpMock'); }); final SentryClient client = new SentryClient( @@ -199,4 +208,16 @@ void main() { }); } -class MockClient extends Mock implements Client {} +typedef Answer = dynamic Function(Invocation invocation); + +class MockClient implements Client { + Answer _answer; + + void answerWith(Answer answer) { + _answer = answer; + } + + noSuchMethod(Invocation invocation) { + return _answer(invocation); + } +} diff --git a/tool/dart2_test.sh b/tool/dart2_test.sh new file mode 100755 index 00000000000..d9f38d362c2 --- /dev/null +++ b/tool/dart2_test.sh @@ -0,0 +1,7 @@ +#!/bin/sh +# Temporary workaround until Pub supports --preview-dart-2 flag +set -e +set -x +for filename in test/*_test.dart; do + dart --preview-dart-2 --enable_asserts "$filename" +done diff --git a/tool/presubmit.sh b/tool/presubmit.sh index a50a5f167dd..a464495fd04 100755 --- a/tool/presubmit.sh +++ b/tool/presubmit.sh @@ -6,4 +6,5 @@ set -x pub get dartanalyzer --strong --fatal-warnings ./ pub run test --platform vm +./tool/dart2_test.sh dartfmt -n --set-exit-if-changed ./ From 7348e101e8e81921eb784a14354152f3ac285936 Mon Sep 17 00:00:00 2001 From: Yegor Date: Wed, 21 Mar 2018 12:10:01 -0700 Subject: [PATCH 26/38] small clean-ups; sdk version constraint (#13) --- .travis.yml | 2 +- README.md | 4 ++-- pubspec.yaml | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4bcdd1aeae4..b3a6410de40 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: dart dart: - - stable + # - stable # there's no Dart 2 on the stable channel yet - dev script: ./tool/presubmit.sh diff --git a/README.md b/README.md index 773c8c5e404..b6849509b02 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ program to https://sentry.io error tracking service. ## Versions ->=0.0.0 and <2.0.0 is the range of versions compatible with Dart 1. +`>=0.0.0 <2.0.0` is the range of versions compatible with Dart 1. ->=2.0.0 and <3.0.0 is the range of versions compatible with Dart 2. +`>=2.0.0 <3.0.0` is the range of versions compatible with Dart 2. ## Usage diff --git a/pubspec.yaml b/pubspec.yaml index ddca8951831..fb88edc5a72 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,6 +4,9 @@ description: A pure Dart Sentry.io client. author: Flutter Authors homepage: https://github.com/flutter/sentry +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + dependencies: http: ">=0.11.0 <2.0.0" meta: ">=1.0.0 <2.0.0" From 3d21b1ebbe2e6978b7e3a5bc4b7aab35572aba4a Mon Sep 17 00:00:00 2001 From: Yegor Date: Thu, 26 Apr 2018 09:31:27 -0700 Subject: [PATCH 27/38] fix Dart 2 type issue in a test (#15) --- test/sentry_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/sentry_test.dart b/test/sentry_test.dart index 4710b4f555a..2e1529c4621 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -31,7 +31,7 @@ void main() { String postUri; Map headers; List body; - httpMock.answerWith((Invocation invocation) { + httpMock.answerWith((Invocation invocation) async { if (invocation.memberName == #close) { return null; } @@ -132,7 +132,7 @@ void main() { final MockClient httpMock = new MockClient(); final Clock fakeClock = new Clock.fixed(new DateTime(2017, 1, 2)); - httpMock.answerWith((Invocation invocation) { + httpMock.answerWith((Invocation invocation) async { if (invocation.memberName == #close) { return null; } From f611b8061c08daa9113211a9da507a8616cea1ac Mon Sep 17 00:00:00 2001 From: Simon Lightfoot Date: Thu, 26 Apr 2018 18:16:00 +0100 Subject: [PATCH 28/38] Fixed "Culprit" recording in Sentry (#14) * Fix stack-trace frame ordering. In order for `Culprit` to be logged correctly it should have the latest frame at th top. * Updated tests. * Fix stack-frame tests. * dartfmt over test/sentry_test.dart --- lib/src/stack_trace.dart | 2 +- test/sentry_test.dart | 3 ++- test/stack_trace_test.dart | 20 ++++++++++---------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/src/stack_trace.dart b/lib/src/stack_trace.dart index ef8ce6218b7..d1175d69213 100644 --- a/lib/src/stack_trace.dart +++ b/lib/src/stack_trace.dart @@ -24,7 +24,7 @@ List> encodeStackTrace(dynamic stackTrace) { frames.addAll(chain.traces[t].frames.map(encodeStackTraceFrame)); if (t < chain.traces.length - 1) frames.add(asynchronousGapFrameJson); } - return frames; + return frames.reversed.toList(); } Map encodeStackTraceFrame(Frame frame) { diff --git a/test/sentry_test.dart b/test/sentry_test.dart index 2e1529c4621..afc4426a717 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -93,7 +93,8 @@ void main() { expect(stacktrace['frames'], const isInstanceOf()); expect(stacktrace['frames'], isNotEmpty); - final Map topFrame = stacktrace['frames'].first; + final Map topFrame = + (stacktrace['frames'] as Iterable).last; expect(topFrame.keys, ['abs_path', 'function', 'lineno', 'in_app', 'filename']); expect(topFrame['abs_path'], 'sentry_test.dart'); diff --git a/test/stack_trace_test.dart b/test/stack_trace_test.dart index eb5ab06f854..3523f5c91c1 100644 --- a/test/stack_trace_test.dart +++ b/test/stack_trace_test.dart @@ -34,18 +34,18 @@ void main() { '''), [ { 'abs_path': 'test.dart', - 'function': 'baz', - 'lineno': 50, + 'function': 'bar', + 'lineno': 46, 'in_app': true, 'filename': 'test.dart' }, { 'abs_path': 'test.dart', - 'function': 'bar', - 'lineno': 46, + 'function': 'baz', + 'lineno': 50, 'in_app': true, 'filename': 'test.dart' - } + }, ]); }); @@ -57,8 +57,8 @@ void main() { '''), [ { 'abs_path': 'test.dart', - 'function': 'baz', - 'lineno': 50, + 'function': 'bar', + 'lineno': 46, 'in_app': true, 'filename': 'test.dart' }, @@ -67,11 +67,11 @@ void main() { }, { 'abs_path': 'test.dart', - 'function': 'bar', - 'lineno': 46, + 'function': 'baz', + 'lineno': 50, 'in_app': true, 'filename': 'test.dart' - } + }, ]); }); }); From 86502830bfb195d608cf7113cb3844f76d03f3e8 Mon Sep 17 00:00:00 2001 From: Yegor Date: Thu, 26 Apr 2018 10:22:15 -0700 Subject: [PATCH 29/38] bump version to 2.0.1 (#16) --- CHANGELOG.md | 4 ++++ lib/src/version.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f48e624c4..fdac5169ee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # package:sentry changelog +## 2.0.1 + +- Invert stack frames to be compatible with Sentry's default culprit detection. + ## 2.0.0 - Fixed deprecation warnings for Dart 2 diff --git a/lib/src/version.dart b/lib/src/version.dart index 1cd67219db7..bd162799a00 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '2.0.0'; +const String sdkVersion = '2.0.1'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index fb88edc5a72..f756b5fe80c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 2.0.0 +version: 2.0.1 description: A pure Dart Sentry.io client. author: Flutter Authors homepage: https://github.com/flutter/sentry From 78ef3f7b330f8af0fa354ec9bd345bd20f533a71 Mon Sep 17 00:00:00 2001 From: Dustin Graham Date: Tue, 15 May 2018 10:21:04 -0700 Subject: [PATCH 30/38] Support user context (#17) Add support for user context information in events submitted to Sentry. --- lib/sentry.dart | 75 ++++++++++++++++++++++++++++++++++++++ test/sentry_test.dart | 84 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 1 deletion(-) diff --git a/lib/sentry.dart b/lib/sentry.dart index 3dbbb93518f..dff6a7fd29a 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -143,6 +143,14 @@ class SentryClient { /// Attached to the event payload. final String projectId; + /// The user data that will get sent with every logged event + /// + /// Note that a [Event.userContext] that is set on a logged [Event] + /// will override the [User] context set here. + /// + /// see: https://docs.sentry.io/learn/context/#capturing-the-user + User userContext; + @visibleForTesting String get postUri => '${dsnUri.scheme}://${dsnUri.host}/api/$projectId/store/'; @@ -170,6 +178,10 @@ class SentryClient { if (environmentAttributes != null) mergeAttributes(environmentAttributes.toJson(), into: data); + // merge the user context + if (userContext != null) { + mergeAttributes({'user': userContext.toJson()}, into: data); + } mergeAttributes(event.toJson(), into: data); List body = utf8.encode(json.encode(data)); @@ -285,6 +297,7 @@ class Event { this.tags, this.extra, this.fingerprint, + this.userContext, }); /// The logger that logged the event. @@ -330,6 +343,12 @@ class Event { /// they must be JSON-serializable. final Map extra; + /// User information that is sent with the logged [Event] + /// + /// The value in this field overrides the user context + /// set in [SentryClient.userContext] for this logged event. + final User userContext; + /// Used to deduplicate events by grouping ones with the same fingerprint /// together. /// @@ -389,9 +408,65 @@ class Event { if (extra != null && extra.isNotEmpty) json['extra'] = extra; + Map userContextMap; + if (userContext != null && + (userContextMap = userContext.toJson()).isNotEmpty) + json['user'] = userContextMap; + if (fingerprint != null && fingerprint.isNotEmpty) json['fingerprint'] = fingerprint; return json; } } + +/// An interface which describes the authenticated User for a request. +/// You should provide at least either an id (a unique identifier for an +/// authenticated user) or ip_address (their IP address). +/// +/// Conforms to the User Interface contract for Sentry +/// https://docs.sentry.io/clientdev/interfaces/user/ +/// +/// The outgoing json representation is: +/// ``` +/// "user": { +/// "id": "unique_id", +/// "username": "my_user", +/// "email": "foo@example.com", +/// "ip_address": "127.0.0.1", +/// "subscription": "basic" +/// } +/// ``` +class User { + /// The unique ID of the user. + final String id; + + /// The username of the user + final String username; + + /// The email address of the user. + final String email; + + /// The IP of the user. + final String ipAddress; + + /// Any other user context information that may be helpful + /// All other keys are stored as extra information but not + /// specifically processed by sentry. + final Map extras; + + /// At a minimum you must set an [id] or an [ipAddress] + const User({this.id, this.username, this.email, this.ipAddress, this.extras}) + : assert(id != null || ipAddress != null); + + /// produces a [Map] that can be serialized to JSON + Map toJson() { + return { + "id": id, + "username": username, + "email": email, + "ip_address": ipAddress, + "extras": extras, + }; + } +} diff --git a/test/sentry_test.dart b/test/sentry_test.dart index afc4426a717..103714babb5 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -74,7 +74,9 @@ void main() { 'Content-Type': 'application/json', 'X-Sentry-Auth': 'Sentry sentry_version=6, ' 'sentry_client=${SentryClient.sentryClient}, ' - 'sentry_timestamp=${fakeClock.now().millisecondsSinceEpoch}, ' + 'sentry_timestamp=${fakeClock + .now() + .millisecondsSinceEpoch}, ' 'sentry_key=public, ' 'sentry_secret=secret', }; @@ -171,10 +173,82 @@ void main() { await client.close(); }); + + test('$Event userContext overrides client', () async { + final MockClient httpMock = new MockClient(); + final Clock fakeClock = new Clock.fixed(new DateTime(2017, 1, 2)); + + String loggedUserId; // used to find out what user context was sent + httpMock.answerWith((Invocation invocation) async { + if (invocation.memberName == #close) { + return null; + } + if (invocation.memberName == #post) { + // parse the body and detect which user context was sent + var bodyData = invocation.namedArguments[new Symbol("body")]; + var decoded = new Utf8Codec().decode(bodyData); + var decodedJson = new JsonDecoder().convert(decoded); + loggedUserId = decodedJson['user']['id']; + return new Response('', 401, headers: { + 'x-sentry-error': 'Invalid api key', + }); + } + fail('Unexpected invocation of ${invocation.memberName} in HttpMock'); + }); + + final clientUserContext = new User( + id: "client_user", + username: "username", + email: "email@email.com", + ipAddress: "127.0.0.1"); + final eventUserContext = new User( + id: "event_user", + username: "username", + email: "email@email.com", + ipAddress: "127.0.0.1", + extras: {"foo": "bar"}); + + final SentryClient client = new SentryClient( + dsn: _testDsn, + httpClient: httpMock, + clock: fakeClock, + uuidGenerator: () => 'X' * 32, + compressPayload: false, + environmentAttributes: const Event( + serverName: 'test.server.com', + release: '1.2.3', + environment: 'staging', + ), + ); + client.userContext = clientUserContext; + + try { + throw new ArgumentError('Test error'); + } catch (error, stackTrace) { + final eventWithoutContext = + new Event(exception: error, stackTrace: stackTrace); + final eventWithContext = new Event( + exception: error, + stackTrace: stackTrace, + userContext: eventUserContext); + await client.capture(event: eventWithoutContext); + expect(loggedUserId, clientUserContext.id); + await client.capture(event: eventWithContext); + expect(loggedUserId, eventUserContext.id); + } + + await client.close(); + }); }); group('$Event', () { test('serializes to JSON', () { + final user = new User( + id: "user_id", + username: "username", + email: "email@email.com", + ipAddress: "127.0.0.1", + extras: {"foo": "bar"}); expect( new Event( message: 'test-message', @@ -190,6 +264,7 @@ void main() { 'g': 2, }, fingerprint: [Event.defaultFingerprint, 'foo'], + userContext: user, ).toJson(), { 'platform': 'dart', @@ -203,6 +278,13 @@ void main() { 'tags': {'a': 'b', 'c': 'd'}, 'extra': {'e': 'f', 'g': 2}, 'fingerprint': ['{{ default }}', 'foo'], + 'user': { + 'id': 'user_id', + 'username': 'username', + 'email': 'email@email.com', + 'ip_address': '127.0.0.1', + 'extras': {'foo': 'bar'} + }, }, ); }); From 5715d15cd6606c70a09e67b29937667ed569bad1 Mon Sep 17 00:00:00 2001 From: Yegor Date: Tue, 15 May 2018 10:42:00 -0700 Subject: [PATCH 31/38] minor dartdoc clean-up; 2.0.2 version bump (#18) --- CHANGELOG.md | 4 ++++ lib/sentry.dart | 47 +++++++++++++++++++++++++++----------------- lib/src/version.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdac5169ee5..13db533a405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # package:sentry changelog +## 2.0.2 + +- Add support for user context in Sentry events. + ## 2.0.1 - Invert stack frames to be compatible with Sentry's default culprit detection. diff --git a/lib/sentry.dart b/lib/sentry.dart index dff6a7fd29a..a0bf59a6f54 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -143,12 +143,16 @@ class SentryClient { /// Attached to the event payload. final String projectId; - /// The user data that will get sent with every logged event + /// Information about the current user. /// - /// Note that a [Event.userContext] that is set on a logged [Event] - /// will override the [User] context set here. + /// This information is sent with every logged event. If the value + /// of this field is updated, all subsequent events will carry the + /// new information. /// - /// see: https://docs.sentry.io/learn/context/#capturing-the-user + /// [Event.userContext] overrides the [User] context set here. + /// + /// See also: + /// * https://docs.sentry.io/learn/context/#capturing-the-user User userContext; @visibleForTesting @@ -178,7 +182,7 @@ class SentryClient { if (environmentAttributes != null) mergeAttributes(environmentAttributes.toJson(), into: data); - // merge the user context + // Merge the user context. if (userContext != null) { mergeAttributes({'user': userContext.toJson()}, into: data); } @@ -343,7 +347,7 @@ class Event { /// they must be JSON-serializable. final Map extra; - /// User information that is sent with the logged [Event] + /// Information about the current user. /// /// The value in this field overrides the user context /// set in [SentryClient.userContext] for this logged event. @@ -420,14 +424,20 @@ class Event { } } -/// An interface which describes the authenticated User for a request. -/// You should provide at least either an id (a unique identifier for an -/// authenticated user) or ip_address (their IP address). +/// Describes the current user associated with the application, such as the +/// currently signed in user. +/// +/// The user can be specified globally in the [SentryClient.userContext] field, +/// or per event in the [Event.userContext] field. +/// +/// You should provide at least either an [id] (a unique identifier for an +/// authenticated user) or [ipAddress] (their IP address). /// /// Conforms to the User Interface contract for Sentry -/// https://docs.sentry.io/clientdev/interfaces/user/ +/// https://docs.sentry.io/clientdev/interfaces/user/. +/// +/// The outgoing JSON representation is: /// -/// The outgoing json representation is: /// ``` /// "user": { /// "id": "unique_id", @@ -438,10 +448,10 @@ class Event { /// } /// ``` class User { - /// The unique ID of the user. + /// A unique identifier of the user. final String id; - /// The username of the user + /// The username of the user. final String username; /// The email address of the user. @@ -450,16 +460,17 @@ class User { /// The IP of the user. final String ipAddress; - /// Any other user context information that may be helpful - /// All other keys are stored as extra information but not - /// specifically processed by sentry. + /// Any other user context information that may be helpful. + /// + /// These keys are stored as extra information but not specifically processed + /// by Sentry. final Map extras; - /// At a minimum you must set an [id] or an [ipAddress] + /// At a minimum you must set an [id] or an [ipAddress]. const User({this.id, this.username, this.email, this.ipAddress, this.extras}) : assert(id != null || ipAddress != null); - /// produces a [Map] that can be serialized to JSON + /// Produces a [Map] that can be serialized to JSON. Map toJson() { return { "id": id, diff --git a/lib/src/version.dart b/lib/src/version.dart index bd162799a00..50ed4af0344 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '2.0.1'; +const String sdkVersion = '2.0.2'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index f756b5fe80c..9f2c5b9356d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 2.0.1 +version: 2.0.2 description: A pure Dart Sentry.io client. author: Flutter Authors homepage: https://github.com/flutter/sentry From 1b9b7fb0def74672d8835bdce6e506ebe4931eaa Mon Sep 17 00:00:00 2001 From: Simon Lightfoot Date: Mon, 30 Jul 2018 21:06:28 +0100 Subject: [PATCH 32/38] Support for new DSN format (without secretKey) and remove the quiver dependency. (#20) * Make the secretKey optional for the new DSN format. * Remove the requirement for quiver. * Fixed test issue. Fixed dartfmt issues. * Code review modifications. * dartfmt --- AUTHORS | 1 + lib/sentry.dart | 47 ++++++++++++----------- pubspec.yaml | 1 - test/sentry_test.dart | 86 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 103 insertions(+), 32 deletions(-) diff --git a/AUTHORS b/AUTHORS index a682da1501c..fa93e5ec4e7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,3 +4,4 @@ # Name/Organization Google Inc. +Simon Lightfoot diff --git a/lib/sentry.dart b/lib/sentry.dart index a0bf59a6f54..37b08a4823b 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -11,7 +11,6 @@ import 'dart:io'; import 'package:http/http.dart'; import 'package:meta/meta.dart'; -import 'package:quiver/time.dart'; import 'package:usage/uuid/uuid.dart'; import 'src/stack_trace.dart'; @@ -20,6 +19,9 @@ import 'src/version.dart'; export 'src/version.dart'; +/// Used to provide timestamp for logging. +typedef ClockProvider = DateTime Function(); + /// Logs crash reports and events to the Sentry.io service. class SentryClient { /// Sentry.io client identifier for _this_ client. @@ -46,7 +48,9 @@ class SentryClient { /// make HTTP calls to Sentry.io. This is useful in tests. /// /// If [clock] is provided, it is used to get time instead of the system - /// clock. This is useful in tests. + /// clock. This is useful in tests. Should be an implementation of ClockProvider. + /// This parameter is dynamic to maintain backwards compatibility with + /// previous use of Clock from the Quiver library. /// /// If [uuidGenerator] is provided, it is used to generate the "event_id" /// field instead of the built-in random UUID v4 generator. This is useful in @@ -56,22 +60,21 @@ class SentryClient { Event environmentAttributes, bool compressPayload, Client httpClient, - Clock clock, + dynamic clock, UuidGenerator uuidGenerator, }) { httpClient ??= new Client(); - clock ??= const Clock(_getUtcDateTime); + clock ??= _getUtcDateTime; uuidGenerator ??= _generateUuidV4WithoutDashes; compressPayload ??= true; + final ClockProvider clockProvider = + clock is ClockProvider ? clock : clock.get; + final Uri uri = Uri.parse(dsn); final List userInfo = uri.userInfo.split(':'); assert(() { - if (userInfo.length != 2) - throw new ArgumentError( - 'Colon-separated publicKey:secretKey pair not found in the user info field of the DSN URI: $dsn'); - if (uri.pathSegments.isEmpty) throw new ArgumentError( 'Project ID not found in the URI path of the DSN URI: $dsn'); @@ -79,13 +82,13 @@ class SentryClient { return true; }()); - final String publicKey = userInfo.first; - final String secretKey = userInfo.last; + final String publicKey = userInfo[0]; + final String secretKey = userInfo.length >= 2 ? userInfo[1] : null; final String projectId = uri.pathSegments.last; return new SentryClient._( httpClient: httpClient, - clock: clock, + clock: clockProvider, uuidGenerator: uuidGenerator, environmentAttributes: environmentAttributes, dsnUri: uri, @@ -98,12 +101,12 @@ class SentryClient { SentryClient._({ @required Client httpClient, - @required Clock clock, + @required ClockProvider clock, @required UuidGenerator uuidGenerator, @required this.environmentAttributes, @required this.dsnUri, @required this.publicKey, - @required this.secretKey, + this.secretKey, @required this.compressPayload, @required this.projectId, }) : _httpClient = httpClient, @@ -111,7 +114,7 @@ class SentryClient { _uuidGenerator = uuidGenerator; final Client _httpClient; - final Clock _clock; + final ClockProvider _clock; final UuidGenerator _uuidGenerator; /// Contains [Event] attributes that are automatically mixed into all events @@ -161,21 +164,23 @@ class SentryClient { /// Reports an [event] to Sentry.io. Future capture({@required Event event}) async { - final DateTime now = _clock.now(); + final DateTime now = _clock(); + String authHeader = 'Sentry sentry_version=6, sentry_client=$sentryClient, ' + 'sentry_timestamp=${now.millisecondsSinceEpoch}, sentry_key=$publicKey'; + if (secretKey != null) { + authHeader += ', sentry_secret=$secretKey'; + } + final Map headers = { 'User-Agent': '$sentryClient', 'Content-Type': 'application/json', - 'X-Sentry-Auth': 'Sentry sentry_version=6, ' - 'sentry_client=$sentryClient, ' - 'sentry_timestamp=${now.millisecondsSinceEpoch}, ' - 'sentry_key=$publicKey, ' - 'sentry_secret=$secretKey', + 'X-Sentry-Auth': authHeader, }; final Map data = { 'project': projectId, 'event_id': _uuidGenerator(), - 'timestamp': formatDateAsIso8601WithSecondPrecision(_clock.now()), + 'timestamp': formatDateAsIso8601WithSecondPrecision(now), 'logger': defaultLoggerName, }; diff --git a/pubspec.yaml b/pubspec.yaml index 9f2c5b9356d..0c91138580d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,6 @@ environment: dependencies: http: ">=0.11.0 <2.0.0" meta: ">=1.0.0 <2.0.0" - quiver: ">=0.25.0 <2.0.0" stack_trace: ">=1.0.0 <2.0.0" usage: ">=3.0.0 <4.0.0" diff --git a/test/sentry_test.dart b/test/sentry_test.dart index 103714babb5..1a61518ed1f 100644 --- a/test/sentry_test.dart +++ b/test/sentry_test.dart @@ -6,11 +6,11 @@ import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart'; -import 'package:quiver/time.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; const String _testDsn = 'https://public:secret@sentry.example.com/1'; +const String _testDsnWithoutSecret = 'https://public@sentry.example.com/1'; void main() { group('$SentryClient', () { @@ -24,9 +24,75 @@ void main() { await client.close(); }); + test('can parse DSN without secret', () async { + final SentryClient client = new SentryClient(dsn: _testDsnWithoutSecret); + expect(client.dsnUri, Uri.parse(_testDsnWithoutSecret)); + expect(client.postUri, 'https://sentry.example.com/api/1/store/'); + expect(client.publicKey, 'public'); + expect(client.secretKey, null); + expect(client.projectId, '1'); + await client.close(); + }); + + test('sends client auth header without secret', () async { + final MockClient httpMock = new MockClient(); + final ClockProvider fakeClockProvider = + () => new DateTime.utc(2017, 1, 2); + + Map headers; + + httpMock.answerWith((Invocation invocation) async { + if (invocation.memberName == #close) { + return null; + } + if (invocation.memberName == #post) { + headers = invocation.namedArguments[#headers]; + return new Response('{"id": "test-event-id"}', 200); + } + fail('Unexpected invocation of ${invocation.memberName} in HttpMock'); + }); + + final SentryClient client = new SentryClient( + dsn: _testDsnWithoutSecret, + httpClient: httpMock, + clock: fakeClockProvider, + compressPayload: false, + uuidGenerator: () => 'X' * 32, + environmentAttributes: const Event( + serverName: 'test.server.com', + release: '1.2.3', + environment: 'staging', + ), + ); + + try { + throw new ArgumentError('Test error'); + } catch (error, stackTrace) { + final SentryResponse response = await client.captureException( + exception: error, stackTrace: stackTrace); + expect(response.isSuccessful, true); + expect(response.eventId, 'test-event-id'); + expect(response.error, null); + } + + final Map expectedHeaders = { + 'User-Agent': '$sdkName/$sdkVersion', + 'Content-Type': 'application/json', + 'X-Sentry-Auth': 'Sentry sentry_version=6, ' + 'sentry_client=${SentryClient.sentryClient}, ' + 'sentry_timestamp=${fakeClockProvider().millisecondsSinceEpoch}, ' + 'sentry_key=public', + }; + + expect(headers, expectedHeaders); + + await client.close(); + }); + testCaptureException(bool compressPayload) async { final MockClient httpMock = new MockClient(); - final Clock fakeClock = new Clock.fixed(new DateTime.utc(2017, 1, 2)); + final ClockProvider fakeClockProvider = + () => new DateTime.utc(2017, 1, 2); String postUri; Map headers; @@ -47,7 +113,7 @@ void main() { final SentryClient client = new SentryClient( dsn: _testDsn, httpClient: httpMock, - clock: fakeClock, + clock: fakeClockProvider, uuidGenerator: () => 'X' * 32, compressPayload: compressPayload, environmentAttributes: const Event( @@ -74,9 +140,7 @@ void main() { 'Content-Type': 'application/json', 'X-Sentry-Auth': 'Sentry sentry_version=6, ' 'sentry_client=${SentryClient.sentryClient}, ' - 'sentry_timestamp=${fakeClock - .now() - .millisecondsSinceEpoch}, ' + 'sentry_timestamp=${fakeClockProvider().millisecondsSinceEpoch}, ' 'sentry_key=public, ' 'sentry_secret=secret', }; @@ -133,7 +197,8 @@ void main() { test('reads error message from the x-sentry-error header', () async { final MockClient httpMock = new MockClient(); - final Clock fakeClock = new Clock.fixed(new DateTime(2017, 1, 2)); + final ClockProvider fakeClockProvider = + () => new DateTime.utc(2017, 1, 2); httpMock.answerWith((Invocation invocation) async { if (invocation.memberName == #close) { @@ -150,7 +215,7 @@ void main() { final SentryClient client = new SentryClient( dsn: _testDsn, httpClient: httpMock, - clock: fakeClock, + clock: fakeClockProvider, uuidGenerator: () => 'X' * 32, compressPayload: false, environmentAttributes: const Event( @@ -176,7 +241,8 @@ void main() { test('$Event userContext overrides client', () async { final MockClient httpMock = new MockClient(); - final Clock fakeClock = new Clock.fixed(new DateTime(2017, 1, 2)); + final ClockProvider fakeClockProvider = + () => new DateTime.utc(2017, 1, 2); String loggedUserId; // used to find out what user context was sent httpMock.answerWith((Invocation invocation) async { @@ -211,7 +277,7 @@ void main() { final SentryClient client = new SentryClient( dsn: _testDsn, httpClient: httpMock, - clock: fakeClock, + clock: fakeClockProvider, uuidGenerator: () => 'X' * 32, compressPayload: false, environmentAttributes: const Event( From 5167a5c1fe65ecbfebd1b6e7cbc8122b8645ff40 Mon Sep 17 00:00:00 2001 From: Yegor Date: Mon, 30 Jul 2018 13:20:50 -0700 Subject: [PATCH 33/38] version 2.1.0; minor dartdoc clean-up (#21) * version 2.1.0; minor dartdoc clean-up * update changelog --- CHANGELOG.md | 7 +++++++ lib/sentry.dart | 5 +++-- lib/src/version.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13db533a405..7a3079c1aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # package:sentry changelog +## 2.1.0 + +- Support DNS format without secret key. +- Remove dependency on `package:quiver`. +- The `clock` argument to `SentryClient` constructor _should_ now be + `ClockProvider` (but still accepts `Clock` for backwards compatibility). + ## 2.0.2 - Add support for user context in Sentry events. diff --git a/lib/sentry.dart b/lib/sentry.dart index 37b08a4823b..ccf20f884b8 100644 --- a/lib/sentry.dart +++ b/lib/sentry.dart @@ -48,9 +48,10 @@ class SentryClient { /// make HTTP calls to Sentry.io. This is useful in tests. /// /// If [clock] is provided, it is used to get time instead of the system - /// clock. This is useful in tests. Should be an implementation of ClockProvider. + /// clock. This is useful in tests. Should be an implementation of [ClockProvider]. /// This parameter is dynamic to maintain backwards compatibility with - /// previous use of Clock from the Quiver library. + /// previous use of [Clock](https://pub.dartlang.org/documentation/quiver/latest/quiver.time/Clock-class.html) + /// from [`package:quiver`](https://pub.dartlang.org/packages/quiver). /// /// If [uuidGenerator] is provided, it is used to generate the "event_id" /// field instead of the built-in random UUID v4 generator. This is useful in diff --git a/lib/src/version.dart b/lib/src/version.dart index 50ed4af0344..16aa807850c 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '2.0.2'; +const String sdkVersion = '2.1.0'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 0c91138580d..98f1cd53485 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 2.0.2 +version: 2.1.0 description: A pure Dart Sentry.io client. author: Flutter Authors homepage: https://github.com/flutter/sentry From 856e6f2a7ae053b6cc76f440f28006360984d518 Mon Sep 17 00:00:00 2001 From: Artem Sheremet Date: Tue, 9 Oct 2018 17:55:54 +0200 Subject: [PATCH 34/38] Bump upper bound of 'mockito' dependency (#24) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 98f1cd53485..3c51d7311b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,4 +17,4 @@ dev_dependencies: args: ">=0.13.0 <2.0.0" test: ">=0.12.0 <2.0.0" yaml: ">=2.1.0 <3.0.0" - mockito: ">=2.0.0 <3.0.0" + mockito: ">=2.0.0 <4.0.0" From 044e4c1f43c2d199ed206e5529e2a630c90e4434 Mon Sep 17 00:00:00 2001 From: Artem Sheremet Date: Tue, 9 Oct 2018 17:59:02 +0200 Subject: [PATCH 35/38] mergeAttributes: deepcopy RHS Map if LHS isn't Map (#23) mergeAttributes: deepcopy RHS Map if LHS isn't Map --- lib/src/utils.dart | 9 +++++++-- test/utils_test.dart | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/src/utils.dart b/lib/src/utils.dart index bcbccf167c0..0e5e93e1811 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -12,8 +12,13 @@ void mergeAttributes(Map attributes, {@required Map into}) { assert(attributes != null && into != null); attributes.forEach((String name, dynamic value) { - final dynamic targetValue = into[name]; - if (value is Map && targetValue is Map) { + dynamic targetValue = into[name]; + if (value is Map) { + if (targetValue is! Map) { + // Let mergeAttributes make a deep copy, because assigning a reference + // of 'value' will expose 'value' to be mutated by further merges. + into[name] = targetValue = {}; + } mergeAttributes(value, into: targetValue); } else { into[name] = value; diff --git a/test/utils_test.dart b/test/utils_test.dart index 3ee15130dc4..e86a57a2aae 100644 --- a/test/utils_test.dart +++ b/test/utils_test.dart @@ -35,6 +35,25 @@ void main() { }, }); }); + + test('does not allow overriding original maps', () { + final environment = { + 'extra': { + 'device': 'Pixel 2', + }, + }; + + final event = { + 'extra': { + 'widget': 'Scaffold', + }, + }; + + final target = {}; + mergeAttributes(environment, into: target); + mergeAttributes(event, into: target); + expect(environment['extra'], {'device': 'Pixel 2'}); + }); }); group('formatDateAsIso8601WithSecondPrecision', () { From 4fc83812dd99121e4f61dc972d0ed4d792ef82f2 Mon Sep 17 00:00:00 2001 From: Yegor Date: Tue, 9 Oct 2018 09:10:12 -0700 Subject: [PATCH 36/38] version 2.1.1; changelog update (#25) --- .gitignore | 2 ++ CHANGELOG.md | 5 +++++ lib/src/version.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 826669578dc..d1e81e6df34 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ packages pubspec.lock .idea/libraries .idea/workspace.xml +android/ +ios/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a3079c1aed..527ec150ece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # package:sentry changelog +## 2.1.1 + +- Defensively copy internal maps event attributes to + avoid shared mutable state (https://github.com/flutter/sentry/commit/044e4c1f43c2d199ed206e5529e2a630c90e4434) + ## 2.1.0 - Support DNS format without secret key. diff --git a/lib/src/version.dart b/lib/src/version.dart index 16aa807850c..b7b759e2bc4 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '2.1.0'; +const String sdkVersion = '2.1.1'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 3c51d7311b0..6e133bb0ce8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 2.1.0 +version: 2.1.1 description: A pure Dart Sentry.io client. author: Flutter Authors homepage: https://github.com/flutter/sentry From 5926e247cdae71a2a000a4709553a075b9eb5c16 Mon Sep 17 00:00:00 2001 From: Filip Hracek Date: Thu, 18 Oct 2018 16:49:37 -0700 Subject: [PATCH 37/38] Fix link to runZoned docs (#26) The previous link lead to Dart 1.24 docs. The new one should automatically redirect to current stable version of Dart (currently 2.0.0). --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6849509b02..1bd7d9579ec 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ main() async { - in Flutter, use [FlutterError.onError][flutter_error] - use `Isolate.current.addErrorListener` to capture uncaught errors in the root zone -[run_zoned]: https://api.dartlang.org/stable/1.24.1/dart-async/runZoned.html +[run_zoned]: https://api.dartlang.org/stable/dart-async/runZoned.html [flutter_error]: https://docs.flutter.io/flutter/foundation/FlutterError/onError.html ## Found a bug? From 86f77ddaa78a4a348b94c276e9a861aff146a289 Mon Sep 17 00:00:00 2001 From: Ray Rischpater Date: Fri, 26 Oct 2018 13:38:33 -0700 Subject: [PATCH 38/38] relocate package to packages/sentry prior to merging with flutter/packages repository --- {.idea => packages/sentry/.idea}/modules.xml | 0 {.idea => packages/sentry/.idea}/sentry.iml | 0 {.idea => packages/sentry/.idea}/vcs.xml | 0 .travis.yml => packages/sentry/.travis.yml | 0 AUTHORS => packages/sentry/AUTHORS | 0 CHANGELOG.md => packages/sentry/CHANGELOG.md | 0 LICENSE => packages/sentry/LICENSE | 0 PATENTS => packages/sentry/PATENTS | 0 README.md => packages/sentry/README.md | 0 {bin => packages/sentry/bin}/test.dart | 0 {lib => packages/sentry/lib}/sentry.dart | 0 {lib => packages/sentry/lib}/src/stack_trace.dart | 0 {lib => packages/sentry/lib}/src/utils.dart | 0 {lib => packages/sentry/lib}/src/version.dart | 0 pubspec.yaml => packages/sentry/pubspec.yaml | 0 {test => packages/sentry/test}/sentry_test.dart | 0 {test => packages/sentry/test}/stack_trace_test.dart | 0 {test => packages/sentry/test}/utils_test.dart | 0 {test => packages/sentry/test}/version_test.dart | 0 {tool => packages/sentry/tool}/dart2_test.sh | 0 {tool => packages/sentry/tool}/presubmit.sh | 0 21 files changed, 0 insertions(+), 0 deletions(-) rename {.idea => packages/sentry/.idea}/modules.xml (100%) rename {.idea => packages/sentry/.idea}/sentry.iml (100%) rename {.idea => packages/sentry/.idea}/vcs.xml (100%) rename .travis.yml => packages/sentry/.travis.yml (100%) rename AUTHORS => packages/sentry/AUTHORS (100%) rename CHANGELOG.md => packages/sentry/CHANGELOG.md (100%) rename LICENSE => packages/sentry/LICENSE (100%) rename PATENTS => packages/sentry/PATENTS (100%) rename README.md => packages/sentry/README.md (100%) rename {bin => packages/sentry/bin}/test.dart (100%) rename {lib => packages/sentry/lib}/sentry.dart (100%) rename {lib => packages/sentry/lib}/src/stack_trace.dart (100%) rename {lib => packages/sentry/lib}/src/utils.dart (100%) rename {lib => packages/sentry/lib}/src/version.dart (100%) rename pubspec.yaml => packages/sentry/pubspec.yaml (100%) rename {test => packages/sentry/test}/sentry_test.dart (100%) rename {test => packages/sentry/test}/stack_trace_test.dart (100%) rename {test => packages/sentry/test}/utils_test.dart (100%) rename {test => packages/sentry/test}/version_test.dart (100%) rename {tool => packages/sentry/tool}/dart2_test.sh (100%) rename {tool => packages/sentry/tool}/presubmit.sh (100%) diff --git a/.idea/modules.xml b/packages/sentry/.idea/modules.xml similarity index 100% rename from .idea/modules.xml rename to packages/sentry/.idea/modules.xml diff --git a/.idea/sentry.iml b/packages/sentry/.idea/sentry.iml similarity index 100% rename from .idea/sentry.iml rename to packages/sentry/.idea/sentry.iml diff --git a/.idea/vcs.xml b/packages/sentry/.idea/vcs.xml similarity index 100% rename from .idea/vcs.xml rename to packages/sentry/.idea/vcs.xml diff --git a/.travis.yml b/packages/sentry/.travis.yml similarity index 100% rename from .travis.yml rename to packages/sentry/.travis.yml diff --git a/AUTHORS b/packages/sentry/AUTHORS similarity index 100% rename from AUTHORS rename to packages/sentry/AUTHORS diff --git a/CHANGELOG.md b/packages/sentry/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/sentry/CHANGELOG.md diff --git a/LICENSE b/packages/sentry/LICENSE similarity index 100% rename from LICENSE rename to packages/sentry/LICENSE diff --git a/PATENTS b/packages/sentry/PATENTS similarity index 100% rename from PATENTS rename to packages/sentry/PATENTS diff --git a/README.md b/packages/sentry/README.md similarity index 100% rename from README.md rename to packages/sentry/README.md diff --git a/bin/test.dart b/packages/sentry/bin/test.dart similarity index 100% rename from bin/test.dart rename to packages/sentry/bin/test.dart diff --git a/lib/sentry.dart b/packages/sentry/lib/sentry.dart similarity index 100% rename from lib/sentry.dart rename to packages/sentry/lib/sentry.dart diff --git a/lib/src/stack_trace.dart b/packages/sentry/lib/src/stack_trace.dart similarity index 100% rename from lib/src/stack_trace.dart rename to packages/sentry/lib/src/stack_trace.dart diff --git a/lib/src/utils.dart b/packages/sentry/lib/src/utils.dart similarity index 100% rename from lib/src/utils.dart rename to packages/sentry/lib/src/utils.dart diff --git a/lib/src/version.dart b/packages/sentry/lib/src/version.dart similarity index 100% rename from lib/src/version.dart rename to packages/sentry/lib/src/version.dart diff --git a/pubspec.yaml b/packages/sentry/pubspec.yaml similarity index 100% rename from pubspec.yaml rename to packages/sentry/pubspec.yaml diff --git a/test/sentry_test.dart b/packages/sentry/test/sentry_test.dart similarity index 100% rename from test/sentry_test.dart rename to packages/sentry/test/sentry_test.dart diff --git a/test/stack_trace_test.dart b/packages/sentry/test/stack_trace_test.dart similarity index 100% rename from test/stack_trace_test.dart rename to packages/sentry/test/stack_trace_test.dart diff --git a/test/utils_test.dart b/packages/sentry/test/utils_test.dart similarity index 100% rename from test/utils_test.dart rename to packages/sentry/test/utils_test.dart diff --git a/test/version_test.dart b/packages/sentry/test/version_test.dart similarity index 100% rename from test/version_test.dart rename to packages/sentry/test/version_test.dart diff --git a/tool/dart2_test.sh b/packages/sentry/tool/dart2_test.sh similarity index 100% rename from tool/dart2_test.sh rename to packages/sentry/tool/dart2_test.sh diff --git a/tool/presubmit.sh b/packages/sentry/tool/presubmit.sh similarity index 100% rename from tool/presubmit.sh rename to packages/sentry/tool/presubmit.sh