diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a4c265f91..6493302ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - expect a sdkName based on the test platform #105 - Added Scope and Breadcrumb ring buffer #109 - Added Hub to SDK #113 +- Ref: Hub passes the Scope to SentryClient - feat: sentry options #116 # `package:sentry` changelog diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index b6bf185a81..6f19a952ed 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -197,12 +197,13 @@ abstract class SentryClient { } /// Reports the [throwable] and optionally its [stackTrace] to Sentry.io. - Future captureException(dynamic throwable, {dynamic stackTrace}) { + Future captureException(dynamic throwable, + {dynamic stackTrace, Scope scope}) { final event = Event( exception: throwable, stackTrace: stackTrace, ); - return captureEvent(event); + return captureEvent(event, scope: scope); } /// Reports the [template] @@ -211,12 +212,13 @@ abstract class SentryClient { SentryLevel level = SentryLevel.info, String template, List params, + Scope scope, }) { final event = Event( message: Message(formatted, template: template, params: params), level: level, ); - return captureEvent(event); + return captureEvent(event, scope: scope); } Future close() async { diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index c3e61aa394..30df85fc68 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -22,7 +22,10 @@ class Hub { ); } - final ListQueue<_StackItem> _stack; + final ListQueue<_StackItem> _stack = ListQueue(); + + /// if stack is empty, it throws IterableElementError.noElement() + _StackItem _peek() => _stack.isNotEmpty ? _stack.first : null; final SentryOptions _options; @@ -32,20 +35,18 @@ class Hub { return Hub._(options); } - Hub._(SentryOptions options) - : _options = options, - _stack = ListQueue() { + Hub._(SentryOptions options) : _options = options { _stack.add(_StackItem(_getClient(fromOptions: options), Scope(_options))); _isEnabled = true; } static void _validateOptions(SentryOptions options) { if (options == null) { - throw ArgumentError.notNull('options'); + throw ArgumentError.notNull('SentryOptions is required.'); } - if (options.dsn == null) { - throw ArgumentError.notNull('options.dsn'); + if (options.dsn?.isNotEmpty != true) { + throw ArgumentError.notNull('DSN is required.'); } } @@ -54,7 +55,7 @@ class Hub { /// Check if the Hub is enabled/active. bool get isEnabled => _isEnabled; - SentryId _lastEventId; + SentryId _lastEventId = SentryId.empty(); /// Last event id recorded in the current scope SentryId get lastEventId => _lastEventId; @@ -74,7 +75,7 @@ class Hub { 'captureEvent called with null parameter.', ); } else { - final item = _stack.first; + final item = _peek(); if (item != null) { try { sentryId = await item.client.captureEvent(event, scope: item.scope); @@ -115,14 +116,11 @@ class Hub { 'captureException called with null parameter.', ); } else { - final item = _stack.first; + final item = _peek(); if (item != null) { try { - // TODO pass the scope - sentryId = await item.client.captureException( - throwable, - stackTrace: stackTrace, - ); + sentryId = await item.client.captureException(throwable, + stackTrace: stackTrace, scope: item.scope); } catch (err) { _options.logger( SentryLevel.error, @@ -162,15 +160,15 @@ class Hub { 'captureMessage called with null parameter.', ); } else { - final item = _stack.first; + final item = _peek(); if (item != null) { try { - // TODO pass the scope sentryId = await item.client.captureMessage( message, level: level, template: template, params: params, + scope: item.scope, ); } catch (err) { _options.logger( @@ -196,7 +194,7 @@ class Hub { _options.logger(SentryLevel.warning, "Instance is disabled and this 'bindClient' call is a no-op."); } else { - final item = _stack.first; + final item = _peek(); if (item != null) { if (client != null) { _options.logger(SentryLevel.debug, 'New client bound to scope.'); @@ -234,7 +232,7 @@ class Hub { "Instance is disabled and this 'close' call is a no-op.", ); } else { - final item = _stack.first; + final item = _peek(); if (item != null) { try { item.client.close(); @@ -262,7 +260,7 @@ class Hub { "Instance is disabled and this 'configureScope' call is a no-op.", ); } else { - final item = _stack.first; + final item = _peek(); if (item != null) { try { callback(item.scope); diff --git a/dart/lib/src/noop_client.dart b/dart/lib/src/noop_client.dart index a7be5701a6..3f7f4c9119 100644 --- a/dart/lib/src/noop_client.dart +++ b/dart/lib/src/noop_client.dart @@ -4,8 +4,17 @@ import 'package:http/src/client.dart'; import 'client.dart'; import 'protocol.dart'; +import 'scope.dart'; class NoOpSentryClient implements SentryClient { + NoOpSentryClient._(); + + static final NoOpSentryClient _instance = NoOpSentryClient._(); + + factory NoOpSentryClient() { + return _instance; + } + @override User userContext; @@ -24,7 +33,7 @@ class NoOpSentryClient implements SentryClient { Future.value(SentryId.empty()); @override - Future captureException(throwable, {stackTrace}) => + Future captureException(throwable, {stackTrace, scope}) => Future.value(SentryId.empty()); @override @@ -33,6 +42,7 @@ class NoOpSentryClient implements SentryClient { SentryLevel level = SentryLevel.info, String template, List params, + Scope scope, }) => Future.value(SentryId.empty()); diff --git a/dart/lib/src/protocol/sentry_id.dart b/dart/lib/src/protocol/sentry_id.dart index bc66594404..e1e1abe75a 100644 --- a/dart/lib/src/protocol/sentry_id.dart +++ b/dart/lib/src/protocol/sentry_id.dart @@ -1,16 +1,16 @@ /// Sentry response id class SentryId { - static const String emptyId = '00000000-0000-0000-0000-000000000000'; + static const String _emptyId = '00000000-0000-0000-0000-000000000000'; /// The ID Sentry.io assigned to the submitted event for future reference. final String _id; - String get id => _id; + // TODO: should we generate the new UUID here with an empty ctor? const SentryId(this._id); - factory SentryId.empty() => SentryId(emptyId); + factory SentryId.empty() => SentryId(_emptyId); @override String toString() => _id.replaceAll('-', ''); diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index ffd94eace6..23a8ea6580 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -6,6 +6,7 @@ import 'client.dart'; import 'hub.dart'; import 'protocol.dart'; import 'sentry_options.dart'; +import 'noop_hub.dart'; /// Configuration options callback typedef OptionsConfiguration = void Function(SentryOptions); @@ -13,20 +14,39 @@ typedef OptionsConfiguration = void Function(SentryOptions); /// Sentry SDK main entry point /// class Sentry { - static Hub _hub; + static Hub _hub = NoOpHub(); Sentry._(); + /// Returns the current hub + static Hub get currentHub => _hub; + + /// Initializes the SDK static void init(OptionsConfiguration optionsConfiguration) { final options = SentryOptions(); optionsConfiguration(options); + _init(options); + } + + /// Initializes the SDK + static void _init(SentryOptions options) { + if (isEnabled) { + options.logger( + SentryLevel.warning, + 'Sentry has been already initialized. Previous configuration will be overwritten.', + ); + } + _setDefaultConfiguration(options); + + final hub = currentHub; _hub = Hub(options); + hub.close(); } /// Reports an [event] to Sentry.io. static Future captureEvent(Event event) async { - return _hub.captureEvent(event); + return currentHub.captureEvent(event); } /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. @@ -34,11 +54,28 @@ class Sentry { dynamic error, { dynamic stackTrace, }) async { - return _hub.captureException(error, stackTrace: stackTrace); + return currentHub.captureException(error, stackTrace: stackTrace); + } + + Future captureMessage( + String message, { + SentryLevel level, + String template, + List params, + }) async { + return currentHub.captureMessage( + message, + level: level, + template: template, + params: params, + ); } /// Close the client SDK - static Future close() async => _hub.close(); + static Future close() async => currentHub.close(); + + /// Check if the current Hub is enabled/active. + static bool get isEnabled => currentHub.isEnabled; static void _setDefaultConfiguration(SentryOptions options) { // TODO: check DSN nullability and empty @@ -50,5 +87,5 @@ class Sentry { /// client injector only use for testing @visibleForTesting - static void initClient(SentryClient client) => _hub.bindClient(client); + static void initClient(SentryClient client) => currentHub.bindClient(client); } diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 79dd59d69d..97cb566081 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -70,13 +70,18 @@ void main() { test('should capture exception', () { hub.captureException(fakeException); - verify(client.captureException(fakeException)).called(1); + verify(client.captureException(fakeException, scope: anyNamed('scope'))) + .called(1); }); test('should capture message', () { hub.captureMessage(fakeMessage.formatted, level: SentryLevel.info); verify( - client.captureMessage(fakeMessage.formatted, level: SentryLevel.info), + client.captureMessage( + fakeMessage.formatted, + level: SentryLevel.info, + scope: anyNamed('scope'), + ), ).called(1); }); }); diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index bd4154b538..6f4067a1eb 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -47,7 +47,11 @@ void main() { test('should capture the exception', () async { await Sentry.captureException(anException); verify( - client.captureException(anException, stackTrace: null), + client.captureException( + anException, + stackTrace: null, + scope: anyNamed('scope'), + ), ).called(1); }); }); diff --git a/dart/test/stack_trace_test.dart b/dart/test/stack_trace_test.dart index e3d80b9206..d0aa7b94ff 100644 --- a/dart/test/stack_trace_test.dart +++ b/dart/test/stack_trace_test.dart @@ -80,6 +80,7 @@ void main() { }); 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(); diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index b5bc0d8a4e..5a34b86ee5 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -87,7 +87,7 @@ Future testCaptureException( } catch (error, stackTrace) { final sentryId = await client.captureException(error, stackTrace: stackTrace); - expect('${sentryId.id}', 'test-event-id'); + expect('$sentryId', 'testeventid'); } expect(postUri, client.postUri); @@ -106,13 +106,12 @@ Future testCaptureException( } else { data = json.decode(utf8.decode(body)) as Map; } - final Map stacktrace = - data.remove('stacktrace') as Map; + final stacktrace = data.remove('stacktrace') as Map; expect(stacktrace['frames'], const TypeMatcher()); expect(stacktrace['frames'], isNotEmpty); - final Map topFrame = + final topFrame = (stacktrace['frames'] as Iterable).last as Map; expect(topFrame.keys, [ 'abs_path', @@ -222,7 +221,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { final httpMock = MockClient((Request request) async { if (request.method == 'POST') { headers = request.headers; - return Response('{"id": "test-event-id"}', 200); + return Response('{"id": "testeventid"}', 200); } fail( 'Unexpected request on ${request.method} ${request.url} in HttpMock'); @@ -246,7 +245,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { } catch (error, stackTrace) { final sentryId = await client.captureException(error, stackTrace: stackTrace); - expect('${sentryId.id}', 'test-event-id'); + expect('$sentryId', 'testeventid'); } testHeaders( @@ -302,7 +301,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { } catch (error, stackTrace) { final sentryId = await client.captureException(error, stackTrace: stackTrace); - expect('${sentryId.id}', SentryId.emptyId); + expect('$sentryId', '00000000000000000000000000000000'); } await client.close();