diff --git a/AUTHORS b/AUTHORS index a682da1501..fa93e5ec4e 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 a0bf59a6f5..37b08a4823 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 9f2c5b9356..0c91138580 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 103714babb..1a61518ed1 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(