diff --git a/dart/example_web/pubspec.yaml b/dart/example_web/pubspec.yaml index 2b2d21817f..e409ff6cdc 100644 --- a/dart/example_web/pubspec.yaml +++ b/dart/example_web/pubspec.yaml @@ -1,7 +1,5 @@ name: sentry_dart_web_example description: An absolute bare-bones web app. -# version: 1.0.0 -#homepage: https://www.example.com environment: sdk: ^2.0.0 diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 63eaf3faaa..c37e8e3ebf 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -249,7 +249,7 @@ class Hub { /// Clones the Hub Hub clone() { if (!_isEnabled) { - _options..logger(SentryLevel.warning, 'Disabled Hub cloned.'); + _options.logger(SentryLevel.warning, 'Disabled Hub cloned.'); } final clone = Hub(_options); for (final item in _stack) { diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index ebf8867306..343e7b5d58 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -75,8 +75,8 @@ class Scope { } // run before breadcrumb callback if set - if (_options.beforeBreadcrumbCallback != null) { - breadcrumb = _options.beforeBreadcrumbCallback(breadcrumb, hint); + if (_options.beforeBreadcrumb != null) { + breadcrumb = _options.beforeBreadcrumb(breadcrumb, hint); if (breadcrumb == null) { _options.logger( diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index a889367f8f..74be045723 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -7,8 +7,8 @@ import 'sentry_client_stub.dart' if (dart.library.html) 'sentry_browser_client.dart' if (dart.library.io) 'sentry_io_client.dart'; import 'sentry_options.dart'; +import 'transport/http_transport.dart'; import 'transport/noop_transport.dart'; -import 'transport/transport.dart'; import 'version.dart'; /// Logs crash reports and events to the Sentry.io service. @@ -22,7 +22,7 @@ abstract class SentryClient { SentryClient.base(this._options, {String origin}) { _random = _options.sampleRate == null ? null : Random(); if (_options.transport is NoOpTransport) { - _options.transport = Transport(options: _options, origin: origin); + _options.transport = HttpTransport(options: _options, origin: origin); } } @@ -30,19 +30,19 @@ abstract class SentryClient { Random _random; + static final _sentryId = Future.value(SentryId.empty()); + /// Reports an [event] to Sentry.io. Future captureEvent( SentryEvent event, { Scope scope, dynamic hint, }) async { - final emptyFuture = Future.value(SentryId.empty()); - event = _processEvent(event, eventProcessors: _options.eventProcessors); // dropped by sampling or event processors if (event == null) { - return emptyFuture; + return _sentryId; } if (scope != null) { @@ -53,14 +53,14 @@ abstract class SentryClient { // dropped by scope event processors if (event == null) { - return emptyFuture; + return _sentryId; } event = _prepareEvent(event); - if (_options.beforeSendCallback != null) { + if (_options.beforeSend != null) { try { - event = _options.beforeSendCallback(event, hint); + event = _options.beforeSend(event, hint); } catch (err) { _options.logger( SentryLevel.error, @@ -69,7 +69,7 @@ abstract class SentryClient { } if (event == null) { _options.logger(SentryLevel.debug, 'Event was dropped by a processor'); - return emptyFuture; + return _sentryId; } } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 384bfffa32..2e5a594cfa 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -92,11 +92,11 @@ class SentryOptions { /// This function is called with an SDK specific event object and can return a modified event /// object or nothing to skip reporting the event - BeforeSendCallback beforeSendCallback; + BeforeSendCallback beforeSend; /// This function is called with an SDK specific breadcrumb object before the breadcrumb is added /// to the scope. When nothing is returned from the function, the breadcrumb is dropped - BeforeBreadcrumbCallback beforeBreadcrumbCallback; + BeforeBreadcrumbCallback beforeBreadcrumb; /// Sets the release. SDK will try to automatically configure a release out of the box String release; @@ -131,8 +131,6 @@ class SentryOptions { set transport(Transport transport) => _transport = transport ?? NoOpTransport(); - // TODO: transportGate, connectionTimeoutMillis, readTimeoutMillis, hostnameVerifier, sslSocketFactory, proxy - /// Sets the distribution. Think about it together with release and environment String dist; diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart new file mode 100644 index 0000000000..2bf0c8a52e --- /dev/null +++ b/dart/lib/src/transport/http_transport.dart @@ -0,0 +1,138 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:meta/meta.dart'; + +import '../protocol.dart'; +import '../sentry_options.dart'; +import '../utils.dart'; +import 'noop_encode.dart' if (dart.library.io) 'encode.dart'; +import 'transport.dart'; + +/// A transport is in charge of sending the event to the Sentry server. +class HttpTransport implements Transport { + final SentryOptions _options; + + @visibleForTesting + final Dsn dsn; + + /// Use for browser stacktrace + final String _origin; + + CredentialBuilder _credentialBuilder; + + final Map _headers; + + HttpTransport({@required SentryOptions options, String origin}) + : _options = options, + _origin = origin, + dsn = Dsn.parse(options.dsn), + _headers = _buildHeaders(sdkIdentifier: options.sdk.identifier) { + _credentialBuilder = CredentialBuilder( + dsn: Dsn.parse(options.dsn), + clientId: options.sdk.identifier, + clock: options.clock, + ); + } + + @override + Future send(SentryEvent event) async { + final data = event.toJson(origin: _origin); + + final body = _bodyEncoder( + data, + _headers, + compressPayload: _options.compressPayload, + ); + + final response = await _options.httpClient.post( + dsn.postUri, + headers: _credentialBuilder.configure(_headers), + body: body, + ); + + if (response.statusCode != 200) { + // body guard to not log the error as it has performance impact to allocate + // the body String. + if (_options.debug) { + _options.logger( + SentryLevel.error, + 'API returned an error, statusCode = ${response.statusCode}, ' + 'body = ${response.body}', + ); + } + return SentryId.empty(); + } else { + _options.logger( + SentryLevel.debug, + 'Event ${event.eventId} was sent suceffully.', + ); + } + + final eventId = json.decode(response.body)['id']; + return eventId != null ? SentryId.fromId(eventId) : SentryId.empty(); + } + + List _bodyEncoder( + Map data, + Map headers, { + bool compressPayload, + }) { + // [SentryIOClient] implement gzip compression + // gzip compression is not available on browser + var body = utf8.encode(json.encode(data)); + if (compressPayload) { + body = compressBody(body, headers); + } + return body; + } +} + +class CredentialBuilder { + final String _authHeader; + + final ClockProvider clock; + + int get timestamp => clock().millisecondsSinceEpoch; + + CredentialBuilder({@required Dsn dsn, String clientId, @required this.clock}) + : _authHeader = buildAuthHeader( + publicKey: dsn.publicKey, + secretKey: dsn.secretKey, + clientId: clientId, + ); + + static String buildAuthHeader({ + String publicKey, + String secretKey, + String clientId, + }) { + var header = 'Sentry sentry_version=6, sentry_client=$clientId, ' + 'sentry_key=${publicKey}'; + + if (secretKey != null) { + header += ', sentry_secret=${secretKey}'; + } + + return header; + } + + Map configure(Map headers) { + return headers + ..addAll( + { + 'X-Sentry-Auth': '$_authHeader, sentry_timestamp=${timestamp}' + }, + ); + } +} + +Map _buildHeaders({String sdkIdentifier}) { + final headers = {'Content-Type': 'application/json'}; + // NOTE(lejard_h) overriding user agent on VM and Flutter not sure why + // for web it use browser user agent + if (!isWeb) { + headers['User-Agent'] = sdkIdentifier; + } + return headers; +} diff --git a/dart/lib/src/transport/noop_transport.dart b/dart/lib/src/transport/noop_transport.dart index 5c14599bf5..9c657ba0a7 100644 --- a/dart/lib/src/transport/noop_transport.dart +++ b/dart/lib/src/transport/noop_transport.dart @@ -4,9 +4,6 @@ import '../protocol.dart'; import 'transport.dart'; class NoOpTransport implements Transport { - @override - Dsn get dsn => null; - @override Future send(SentryEvent event) => Future.value(SentryId.empty()); } diff --git a/dart/lib/src/transport/transport.dart b/dart/lib/src/transport/transport.dart index da7c637b8c..295181cbc2 100644 --- a/dart/lib/src/transport/transport.dart +++ b/dart/lib/src/transport/transport.dart @@ -1,122 +1,9 @@ import 'dart:async'; -import 'dart:convert'; - -import 'package:meta/meta.dart'; import '../protocol.dart'; -import '../sentry_options.dart'; -import '../utils.dart'; -import 'noop_encode.dart' if (dart.library.io) 'encode.dart'; - -/// A transport is in charge of sending the event to the Sentry server. -class Transport { - final SentryOptions _options; - - @visibleForTesting - final Dsn dsn; - - /// Use for browser stacktrace - final String _origin; - - CredentialBuilder _credentialBuilder; - - final Map _headers; - - Transport({@required SentryOptions options, String origin}) - : _options = options, - _origin = origin, - dsn = Dsn.parse(options.dsn), - _headers = _buildHeaders(sdkIdentifier: options.sdk.identifier) { - _credentialBuilder = CredentialBuilder( - dsn: Dsn.parse(options.dsn), - clientId: options.sdk.identifier, - clock: options.clock, - ); - } - - Future send(SentryEvent event) async { - final data = event.toJson(origin: _origin); - - final body = _bodyEncoder( - data, - _headers, - compressPayload: _options.compressPayload, - ); - - final response = await _options.httpClient.post( - dsn.postUri, - headers: _credentialBuilder.configure(_headers), - body: body, - ); - - if (response.statusCode != 200) { - return SentryId.empty(); - } - - final eventId = json.decode(response.body)['id']; - return eventId != null ? SentryId.fromId(eventId) : SentryId.empty(); - } - - List _bodyEncoder( - Map data, - Map headers, { - bool compressPayload, - }) { - // [SentryIOClient] implement gzip compression - // gzip compression is not available on browser - var body = utf8.encode(json.encode(data)); - if (compressPayload) { - body = compressBody(body, headers); - } - return body; - } -} - -class CredentialBuilder { - final String _authHeader; - - final ClockProvider clock; - - int get timestamp => clock().millisecondsSinceEpoch; - - CredentialBuilder({@required Dsn dsn, String clientId, @required this.clock}) - : _authHeader = buildAuthHeader( - publicKey: dsn.publicKey, - secretKey: dsn.secretKey, - clientId: clientId, - ); - - static String buildAuthHeader({ - String publicKey, - String secretKey, - String clientId, - }) { - var header = 'Sentry sentry_version=6, sentry_client=$clientId, ' - 'sentry_key=${publicKey}'; - - if (secretKey != null) { - header += ', sentry_secret=${secretKey}'; - } - - return header; - } - - Map configure(Map headers) { - return headers - ..addAll( - { - 'X-Sentry-Auth': '$_authHeader, sentry_timestamp=${timestamp}' - }, - ); - } -} -Map _buildHeaders({String sdkIdentifier}) { - final headers = {'Content-Type': 'application/json'}; - // NOTE(lejard_h) overriding user agent on VM and Flutter not sure why - // for web it use browser user agent - if (!isWeb) { - headers['User-Agent'] = sdkIdentifier; - } - return headers; +/// A transport is in charge of sending the event either via http +/// or caching in the disk. +abstract class Transport { + Future send(SentryEvent event); } diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index afb2b57700..b246b60398 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -245,7 +245,7 @@ class Fixture { }) { final options = SentryOptions(); options.maxBreadcrumbs = maxBreadcrumbs; - options.beforeBreadcrumbCallback = beforeBreadcrumbCallback; + options.beforeBreadcrumb = beforeBreadcrumbCallback; return Scope(options); } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 33e0329a10..b3d0b3c551 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -214,7 +214,7 @@ void main() { }); test('before send drops event', () { - options.beforeSendCallback = beforeSendCallbackDropEvent; + options.beforeSend = beforeSendCallbackDropEvent; final client = SentryClient(options); client.captureEvent(fakeEvent); @@ -222,7 +222,7 @@ void main() { }); test('before send returns an event and event is captured', () { - options.beforeSendCallback = beforeSendCallback; + options.beforeSend = beforeSendCallback; final client = SentryClient(options); client.captureEvent(fakeEvent); diff --git a/dart/test/event_test.dart b/dart/test/sentry_event_test.dart similarity index 100% rename from dart/test/event_test.dart rename to dart/test/sentry_event_test.dart diff --git a/dart/test/sentry_io_test.dart b/dart/test/sentry_io_client_test.dart similarity index 100% rename from dart/test/sentry_io_test.dart rename to dart/test/sentry_io_client_test.dart diff --git a/dart/test/stack_trace_test.dart b/dart/test/stack_trace_test.dart index 947fdc6551..ed7e01508a 100644 --- a/dart/test/stack_trace_test.dart +++ b/dart/test/stack_trace_test.dart @@ -78,26 +78,5 @@ void main() { }, ]); }); - -// TODO: use beforeSend to filter stack frames -// test('allows changing the stack frame list before sending', () { -// // ignore: omit_local_variable_types -// final StackFrameFilter filter = -// (list) => list.where((f) => f['abs_path'] != 'secret.dart').toList(); - -// expect(encodeStackTrace(''' -// #0 baz (file:///pathto/test.dart:50:3) -// #1 bar (file:///pathto/secret.dart:46:9) -// ''', stackFrameFilter: filter), [ -// { -// 'abs_path': 'test.dart', -// 'function': 'baz', -// 'lineno': 50, -// 'colno': 3, -// 'in_app': true, -// 'filename': 'test.dart' -// }, -// ]); -// }); }); } diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index 9852b7aed7..e2ae805539 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -8,6 +8,7 @@ import 'dart:convert'; import 'package:http/http.dart'; import 'package:http/testing.dart'; import 'package:sentry/sentry.dart'; +import 'package:sentry/src/transport/http_transport.dart'; import 'package:test/test.dart'; const String testDsn = 'https://public:secret@sentry.example.com/1'; @@ -88,7 +89,8 @@ Future testCaptureException( expect('$sentryId', 'testeventid'); } - expect(postUri, options.transport.dsn.postUri); + final transport = options.transport as HttpTransport; + expect(postUri, transport.dsn.postUri); testHeaders( headers, @@ -178,55 +180,67 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { test('can parse DSN', () async { final options = SentryOptions(dsn: testDsn); final client = SentryClient(options); - expect(options.transport.dsn.uri, Uri.parse(testDsn)); + + final transport = options.transport as HttpTransport; + + expect(transport.dsn.uri, Uri.parse(testDsn)); expect( - options.transport.dsn.postUri, + transport.dsn.postUri, 'https://sentry.example.com/api/1/store/', ); - expect(options.transport.dsn.publicKey, 'public'); - expect(options.transport.dsn.secretKey, 'secret'); - expect(options.transport.dsn.projectId, '1'); + expect(transport.dsn.publicKey, 'public'); + expect(transport.dsn.secretKey, 'secret'); + expect(transport.dsn.projectId, '1'); await client.close(); }); test('can parse DSN without secret', () async { final options = SentryOptions(dsn: _testDsnWithoutSecret); final client = SentryClient(options); - expect(options.transport.dsn.uri, Uri.parse(_testDsnWithoutSecret)); + + final transport = options.transport as HttpTransport; + + expect(transport.dsn.uri, Uri.parse(_testDsnWithoutSecret)); expect( - options.transport.dsn.postUri, + transport.dsn.postUri, 'https://sentry.example.com/api/1/store/', ); - expect(options.transport.dsn.publicKey, 'public'); - expect(options.transport.dsn.secretKey, null); - expect(options.transport.dsn.projectId, '1'); + expect(transport.dsn.publicKey, 'public'); + expect(transport.dsn.secretKey, null); + expect(transport.dsn.projectId, '1'); await client.close(); }); test('can parse DSN with path', () async { final options = SentryOptions(dsn: _testDsnWithPath); final client = SentryClient(options); - expect(options.transport.dsn.uri, Uri.parse(_testDsnWithPath)); + + final transport = options.transport as HttpTransport; + + expect(transport.dsn.uri, Uri.parse(_testDsnWithPath)); expect( - options.transport.dsn.postUri, + transport.dsn.postUri, 'https://sentry.example.com/path/api/1/store/', ); - expect(options.transport.dsn.publicKey, 'public'); - expect(options.transport.dsn.secretKey, 'secret'); - expect(options.transport.dsn.projectId, '1'); + expect(transport.dsn.publicKey, 'public'); + expect(transport.dsn.secretKey, 'secret'); + expect(transport.dsn.projectId, '1'); await client.close(); }); test('can parse DSN with port', () async { final options = SentryOptions(dsn: _testDsnWithPort); final client = SentryClient(options); - expect(options.transport.dsn.uri, Uri.parse(_testDsnWithPort)); + + final transport = options.transport as HttpTransport; + + expect(transport.dsn.uri, Uri.parse(_testDsnWithPort)); expect( - options.transport.dsn.postUri, + transport.dsn.postUri, 'https://sentry.example.com:8888/api/1/store/', ); - expect(options.transport.dsn.publicKey, 'public'); - expect(options.transport.dsn.secretKey, 'secret'); - expect(options.transport.dsn.projectId, '1'); + expect(transport.dsn.publicKey, 'public'); + expect(transport.dsn.secretKey, 'secret'); + expect(transport.dsn.projectId, '1'); await client.close(); }); test('sends client auth header without secret', () async {