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
This commit is contained in:
Simon Lightfoot
2018-07-30 21:06:28 +01:00
committed by Yegor
parent 5715d15cd6
commit 1b9b7fb0de
4 changed files with 103 additions and 32 deletions

View File

@ -4,3 +4,4 @@
# Name/Organization <email address>
Google Inc.
Simon Lightfoot <simon@devangels.london>

View File

@ -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<String> 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<SentryResponse> 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<String, String> headers = <String, String>{
'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<String, dynamic> data = <String, dynamic>{
'project': projectId,
'event_id': _uuidGenerator(),
'timestamp': formatDateAsIso8601WithSecondPrecision(_clock.now()),
'timestamp': formatDateAsIso8601WithSecondPrecision(now),
'logger': defaultLoggerName,
};

View File

@ -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"

View File

@ -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<String, String> 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<String, String> expectedHeaders = <String, String>{
'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<String, String> 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(