From d577e35db1b63f0e10e515f4661097b9df8a210d Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Mon, 19 Oct 2020 13:19:34 +0200 Subject: [PATCH] Feat : add a Hub class (#113) --- CHANGELOG.md | 1 + dart/example/event_example.dart | 2 +- dart/example/main.dart | 16 +- dart/lib/src/client.dart | 41 ++-- dart/lib/src/hub.dart | 291 +++++++++++++++++++++++++++ dart/lib/src/noop_client.dart | 73 +++++++ dart/lib/src/protocol.dart | 1 + dart/lib/src/protocol/event.dart | 9 + dart/lib/src/protocol/message.dart | 6 +- dart/lib/src/protocol/sentry_id.dart | 17 ++ dart/lib/src/scope.dart | 23 ++- dart/lib/src/sentry.dart | 27 +-- dart/lib/src/sentry_options.dart | 14 +- dart/pubspec.yaml | 1 + dart/test/event_test.dart | 25 ++- dart/test/hub_test.dart | 160 +++++++++++++++ dart/test/mocks.dart | 85 ++++++++ dart/test/scope_test.dart | 12 ++ dart/test/sentry_test.dart | 115 +++-------- dart/test/test_utils.dart | 30 ++- flutter/example/lib/main.dart | 4 +- 21 files changed, 793 insertions(+), 160 deletions(-) create mode 100644 dart/lib/src/hub.dart create mode 100644 dart/lib/src/noop_client.dart create mode 100644 dart/lib/src/protocol/sentry_id.dart create mode 100644 dart/test/hub_test.dart create mode 100644 dart/test/mocks.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a51ed029..448f3c3aba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - new static API : Sentry.init(), Sentry.captureEvent() #108 - expect a sdkName based on the test platform #105 - Added Scope and Breadcrumb ring buffer #109 +- Added Hub to SDK #113 # `package:sentry` changelog diff --git a/dart/example/event_example.dart b/dart/example/event_example.dart index b1115cf7ef..72596107f0 100644 --- a/dart/example/event_example.dart +++ b/dart/example/event_example.dart @@ -5,7 +5,7 @@ final event = Event( serverName: 'server.dart', release: '1.4.0-preview.1', environment: 'Test', - message: Message(formatted: 'This is an example Dart event.'), + message: Message('This is an example Dart event.'), transaction: '/example/app', level: SeverityLevel.warning, tags: const {'project-id': '7371'}, diff --git a/dart/example/main.dart b/dart/example/main.dart index ff859af407..777407967e 100644 --- a/dart/example/main.dart +++ b/dart/example/main.dart @@ -23,29 +23,21 @@ Future main(List rawArgs) async { print('\nReporting a complete event example: '); // Sends a full Sentry event payload to show the different parts of the UI. - final response = await Sentry.captureEvent(event); + final sentryId = await Sentry.captureEvent(event); - if (response.isSuccessful) { - print('SUCCESS\nid: ${response.eventId}'); - } else { - print('FAILURE: ${response.error}'); - } + print('SentryId : ${sentryId}'); try { await foo(); } catch (error, stackTrace) { print('\nReporting the following stack trace: '); print(stackTrace); - final response = await Sentry.captureException( + final sentryId = await Sentry.captureException( error, stackTrace: stackTrace, ); - if (response.isSuccessful) { - print('SUCCESS\nid: ${response.eventId}'); - } else { - print('FAILURE: ${response.error}'); - } + print('SentryId : ${sentryId}'); } finally { await Sentry.close(); } diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index 08cc0f8bc8..eadcff84b9 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:http/http.dart'; import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; import 'client_stub.dart' if (dart.library.html) 'browser_client.dart' @@ -136,9 +137,10 @@ abstract class SentryClient { } /// Reports an [event] to Sentry.io. - Future captureEvent({ - @required Event event, + Future captureEvent( + Event event, { StackFrameFilter stackFrameFilter, + Scope scope, }) async { final now = _clock(); var authHeader = 'Sentry sentry_version=6, sentry_client=$clientId, ' @@ -183,27 +185,38 @@ abstract class SentryClient { ); if (response.statusCode != 200) { - var errorMessage = 'Sentry.io responded with HTTP ${response.statusCode}'; + /*var errorMessage = 'Sentry.io responded with HTTP ${response.statusCode}'; if (response.headers['x-sentry-error'] != null) { errorMessage += ': ${response.headers['x-sentry-error']}'; - } - return SentryResponse.failure(errorMessage); + }*/ + return SentryId.empty(); } - final eventId = '${json.decode(response.body)['id']}'; - return SentryResponse.success(eventId: eventId); + final eventId = json.decode(response.body)['id']; + return eventId != null ? SentryId(eventId) : SentryId.empty(); } - /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. - Future captureException({ - @required dynamic exception, - dynamic stackTrace, - }) { + /// Reports the [throwable] and optionally its [stackTrace] to Sentry.io. + Future captureException(dynamic throwable, {dynamic stackTrace}) { final event = Event( - exception: exception, + exception: throwable, stackTrace: stackTrace, ); - return captureEvent(event: event); + return captureEvent(event); + } + + /// Reports the [template] + Future captureMessage( + String formatted, { + SeverityLevel level = SeverityLevel.info, + String template, + List params, + }) { + final event = Event( + message: Message(formatted, template: template, params: params), + level: level, + ); + return captureEvent(event); } Future close() async { diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart new file mode 100644 index 0000000000..951b62ada7 --- /dev/null +++ b/dart/lib/src/hub.dart @@ -0,0 +1,291 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'client.dart'; +import 'noop_client.dart'; +import 'protocol.dart'; +import 'scope.dart'; +import 'sentry_options.dart'; + +typedef ScopeCallback = void Function(Scope); + +/// SDK API contract which combines a client and scope management +class Hub { + static SentryClient _getClient({SentryOptions fromOptions}) { + return SentryClient( + dsn: fromOptions.dsn, + environmentAttributes: fromOptions.environmentAttributes, + compressPayload: fromOptions.compressPayload, + httpClient: fromOptions.httpClient, + clock: fromOptions.clock, + uuidGenerator: fromOptions.uuidGenerator, + ); + } + + final ListQueue<_StackItem> _stack; + + final SentryOptions _options; + + factory Hub(SentryOptions options) { + _validateOptions(options); + + return Hub._(options); + } + + Hub._(SentryOptions options) + : _options = options, + _stack = ListQueue() { + _stack.add(_StackItem(_getClient(fromOptions: options), Scope(_options))); + _isEnabled = true; + } + + static void _validateOptions(SentryOptions options) { + if (options == null) { + throw ArgumentError.notNull('options'); + } + + if (options.dsn == null) { + throw ArgumentError.notNull('options.dsn'); + } + } + + bool _isEnabled = false; + + /// Check if the Hub is enabled/active. + bool get isEnabled => _isEnabled; + + SentryId _lastEventId; + + /// Last event id recorded in the current scope + SentryId get lastEventId => _lastEventId; + + /// Captures the event. + Future captureEvent(Event event) async { + var sentryId = SentryId.empty(); + + if (!_isEnabled) { + _options.logger( + SeverityLevel.warning, + "Instance is disabled and this 'captureEvent' call is a no-op.", + ); + } else if (event == null) { + _options.logger( + SeverityLevel.warning, + 'captureEvent called with null parameter.', + ); + } else { + final item = _stack.first; + if (item != null) { + try { + sentryId = await item.client.captureEvent(event, scope: item.scope); + } catch (err) { + /* TODO add Event.id */ + _options.logger( + SeverityLevel.error, + 'Error while capturing event with id: ${event}', + ); + } finally { + _lastEventId = sentryId; + } + } else { + _options.logger( + SeverityLevel.fatal, + 'Stack peek was null when captureEvent', + ); + } + } + return sentryId; + } + + /// Captures the exception + Future captureException( + dynamic throwable, { + dynamic stackTrace, + }) async { + var sentryId = SentryId.empty(); + + if (!_isEnabled) { + _options.logger( + SeverityLevel.warning, + "Instance is disabled and this 'captureException' call is a no-op.", + ); + } else if (throwable == null) { + _options.logger( + SeverityLevel.warning, + 'captureException called with null parameter.', + ); + } else { + final item = _stack.first; + if (item != null) { + try { + // TODO pass the scope + sentryId = await item.client.captureException( + throwable, + stackTrace: stackTrace, + ); + } catch (err) { + _options.logger( + SeverityLevel.error, + 'Error while capturing exception : ${throwable}', + ); + } finally { + _lastEventId = sentryId; + } + } else { + _options.logger( + SeverityLevel.fatal, + 'Stack peek was null when captureException', + ); + } + } + + return sentryId; + } + + /// Captures the message. + Future captureMessage( + String message, { + SeverityLevel level = SeverityLevel.info, + String template, + List params, + }) async { + var sentryId = SentryId.empty(); + + if (!_isEnabled) { + _options.logger( + SeverityLevel.warning, + "Instance is disabled and this 'captureMessage' call is a no-op.", + ); + } else if (message == null) { + _options.logger( + SeverityLevel.warning, + 'captureMessage called with null parameter.', + ); + } else { + final item = _stack.first; + if (item != null) { + try { + // TODO pass the scope + sentryId = await item.client.captureMessage( + message, + level: level, + template: template, + params: params, + ); + } catch (err) { + _options.logger( + SeverityLevel.error, + 'Error while capturing message with id: ${message}', + ); + } finally { + _lastEventId = sentryId; + } + } else { + _options.logger( + SeverityLevel.fatal, + 'Stack peek was null when captureMessage', + ); + } + } + return sentryId; + } + + /// Binds a different client to the hub + void bindClient(SentryClient client) { + if (!_isEnabled) { + _options.logger(SeverityLevel.warning, + "Instance is disabled and this 'bindClient' call is a no-op."); + } else { + final item = _stack.first; + if (item != null) { + if (client != null) { + _options.logger(SeverityLevel.debug, 'New client bound to scope.'); + item.client = client; + } else { + _options.logger(SeverityLevel.debug, 'NoOp client bound to scope.'); + item.client = NoOpSentryClient(); + } + } else { + _options.logger( + SeverityLevel.fatal, + 'Stack peek was null when bindClient', + ); + } + } + } + + /// Clones the Hub + Hub clone() { + if (!_isEnabled) { + _options..logger(SeverityLevel.warning, 'Disabled Hub cloned.'); + } + final clone = Hub(_options); + for (final item in _stack) { + clone._stack.add(_StackItem(item.client, item.scope.clone())); + } + return clone; + } + + /// Flushes out the queue for up to timeout seconds and disable the Hub. + void close() { + if (!_isEnabled) { + _options.logger( + SeverityLevel.warning, + "Instance is disabled and this 'close' call is a no-op.", + ); + } else { + final item = _stack.first; + if (item != null) { + try { + item.client.close(); + } catch (err) { + _options.logger( + SeverityLevel.error, + 'Error while closing the Hub.', + ); + } + } else { + _options.logger( + SeverityLevel.fatal, + 'Stack peek was NULL when closing Hub', + ); + } + _isEnabled = false; + } + } + + /// Configures the scope through the callback. + void configureScope(ScopeCallback callback) { + if (!_isEnabled) { + _options.logger( + SeverityLevel.warning, + "Instance is disabled and this 'configureScope' call is a no-op.", + ); + } else { + final item = _stack.first; + if (item != null) { + try { + callback(item.scope); + } catch (err) { + _options.logger( + SeverityLevel.error, + "Error in the 'configureScope' callback.", + ); + } + } else { + _options.logger( + SeverityLevel.fatal, + 'Stack peek was NULL when configureScope', + ); + } + } + } +} + +class _StackItem { + SentryClient client; + + final Scope scope; + + _StackItem(this.client, this.scope); +} diff --git a/dart/lib/src/noop_client.dart b/dart/lib/src/noop_client.dart new file mode 100644 index 0000000000..88293b343b --- /dev/null +++ b/dart/lib/src/noop_client.dart @@ -0,0 +1,73 @@ +import 'dart:async'; + +import 'package:http/src/client.dart'; + +import 'client.dart'; +import 'protocol.dart'; + +class NoOpSentryClient implements SentryClient { + @override + User userContext; + + @override + List bodyEncoder( + Map data, + Map headers, + ) => + []; + + @override + Map buildHeaders(String authHeader) => {}; + + @override + Future captureEvent(Event event, {stackFrameFilter, scope}) => + Future.value(SentryId.empty()); + + @override + Future captureException(throwable, {stackTrace}) => + Future.value(SentryId.empty()); + + @override + Future captureMessage( + String message, { + SeverityLevel level = SeverityLevel.info, + String template, + List params, + }) => + Future.value(SentryId.empty()); + + @override + String get clientId => 'No-op'; + + @override + Future close() async { + return; + } + + @override + Uri get dsnUri => null; + + @override + Event get environmentAttributes => null; + + @override + Client get httpClient => null; + + @override + String get origin => null; + + @override + String get postUri => null; + + @override + String get projectId => null; + + @override + String get publicKey => null; + + @override + Sdk get sdk => null; + + @override + String get secretKey => null; +} diff --git a/dart/lib/src/protocol.dart b/dart/lib/src/protocol.dart index 908f65da83..17a5f47ce6 100644 --- a/dart/lib/src/protocol.dart +++ b/dart/lib/src/protocol.dart @@ -10,5 +10,6 @@ export 'protocol/message.dart'; export 'protocol/package.dart'; export 'protocol/runtime.dart'; export 'protocol/sdk.dart'; +export 'protocol/sentry_id.dart'; export 'protocol/system.dart'; export 'protocol/user.dart'; diff --git a/dart/lib/src/protocol/event.dart b/dart/lib/src/protocol/event.dart index a719ded162..76ee95157f 100644 --- a/dart/lib/src/protocol/event.dart +++ b/dart/lib/src/protocol/event.dart @@ -193,6 +193,15 @@ class Event { 'value': '$exception', } ]; + if (exception is Error && exception.stackTrace != null) { + json['stacktrace'] = { + 'frames': encodeStackTrace( + exception.stackTrace, + stackFrameFilter: stackFrameFilter, + origin: origin, + ), + }; + } } if (stackTrace != null) { diff --git a/dart/lib/src/protocol/message.dart b/dart/lib/src/protocol/message.dart index bca42670a3..2e2c85dc2b 100644 --- a/dart/lib/src/protocol/message.dart +++ b/dart/lib/src/protocol/message.dart @@ -12,17 +12,17 @@ class Message { /// The raw message string (uninterpolated). /// example : "My raw message with interpreted strings like %s", - final String message; + final String template; /// A list of formatting parameters, preferably strings. Non-strings will be coerced to strings. final List params; - Message({this.formatted, this.message, this.params}); + Message(this.formatted, {this.template, this.params}); Map toJson() { return { 'formatted': formatted, - 'message': message, + 'message': template, 'params': params, }; } diff --git a/dart/lib/src/protocol/sentry_id.dart b/dart/lib/src/protocol/sentry_id.dart new file mode 100644 index 0000000000..bc66594404 --- /dev/null +++ b/dart/lib/src/protocol/sentry_id.dart @@ -0,0 +1,17 @@ +/// Sentry response id + +class SentryId { + 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; + + const SentryId(this._id); + + factory SentryId.empty() => SentryId(emptyId); + + @override + String toString() => _id.replaceAll('-', ''); +} diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 63e1c85ec8..128b7d1ff4 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -133,7 +133,26 @@ class Scope { } /// Removes an extra from the Scope - void removeExtra(String key) { - _extra.remove(key); + void removeExtra(String key) => _extra.remove(key); + + Scope clone() { + final clone = Scope(_options) + ..user = user + ..fingerprint = fingerprint != null ? List.from(fingerprint) : null + ..transaction = transaction; + + for (final tag in _tags.keys) { + clone.setTag(tag, _tags[tag]); + } + + for (final extraKey in _extra.keys) { + clone.setExtra(extraKey, _extra[extraKey]); + } + + for (final breadcrumb in _breadcrumbs) { + clone.addBreadcrumb(breadcrumb); + } + + return clone; } } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 382026acd5..7e1b0b4c05 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'client.dart'; +import 'hub.dart'; import 'protocol.dart'; import 'sentry_options.dart'; @@ -12,43 +13,33 @@ typedef OptionsConfiguration = void Function(SentryOptions); /// Sentry SDK main entry point /// class Sentry { - static SentryClient _client; + static Hub _hub; Sentry._(); static void init(OptionsConfiguration optionsConfiguration) { final options = SentryOptions(); optionsConfiguration(options); - _client = SentryClient( - dsn: options.dsn, - environmentAttributes: options.environmentAttributes, - compressPayload: options.compressPayload, - httpClient: options.httpClient, - clock: options.clock, - uuidGenerator: options.uuidGenerator, - ); + _hub = Hub(options); } /// Reports an [event] to Sentry.io. - static Future captureEvent(Event event) async { - return _client.captureEvent(event: event); + static Future captureEvent(Event event) async { + return _hub.captureEvent(event); } /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. - static Future captureException( + static Future captureException( dynamic error, { dynamic stackTrace, }) async { - return _client.captureException( - exception: error, - stackTrace: stackTrace, - ); + return _hub.captureException(error, stackTrace: stackTrace); } /// Close the client SDK - static Future close() async => _client.close(); + static Future close() async => _hub.close(); /// client injector only use for testing @visibleForTesting - static void initClient(SentryClient client) => _client = client; + static void initClient(SentryClient client) => _hub.bindClient(client); } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index dbcfa7ca6a..6305bca2e7 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -1,8 +1,11 @@ import 'package:http/http.dart'; +import 'package:sentry/sentry.dart'; import 'protocol.dart'; import 'utils.dart'; +typedef Logger = void Function(SeverityLevel, String); + /// Sentry SDK options class SentryOptions { /// Default Log level if not specified Default is DEBUG @@ -44,6 +47,10 @@ class SentryOptions { int maxBreadcrumbs; + final Logger _logger; + + Logger get logger => _logger ?? defaultLogger; + SentryOptions({ this.dsn, this.environmentAttributes, @@ -51,6 +58,11 @@ class SentryOptions { this.httpClient, this.clock, this.uuidGenerator, + Logger logger, this.maxBreadcrumbs = 100, - }); + }) : _logger = logger; +} + +void defaultLogger(SeverityLevel level, String message) { + print('[$level] $message'); } diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index 389acefa7e..7c9f4d8624 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -20,3 +20,4 @@ dev_dependencies: test: ^1.15.4 yaml: ^2.2.1 test_coverage: ^0.4.1 + collection: ^1.14.13 diff --git a/dart/test/event_test.dart b/dart/test/event_test.dart index 7bbeab80f7..ff66e2ab54 100644 --- a/dart/test/event_test.dart +++ b/dart/test/event_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:sentry/sentry.dart'; +import 'package:sentry/src/stack_trace.dart'; import 'package:test/test.dart'; void main() { @@ -55,15 +56,19 @@ void main() { level: SeverityLevel.debug, category: 'test'), ]; + final error = StateError('test-error'); + + print('error.stackTrace ${error.stackTrace}'); + expect( Event( message: Message( - formatted: 'test-message 1 2', - message: 'test-message %d %d', + 'test-message 1 2', + template: 'test-message %d %d', params: ['1', '2'], ), transaction: '/test/1', - exception: StateError('test-error'), + exception: error, level: SeverityLevel.debug, culprit: 'Professor Moriarty', tags: const { @@ -112,7 +117,19 @@ void main() { }, ] }, - }, + }..addAll( + error.stackTrace == null + ? {} + : { + 'stacktrace': { + 'frames': encodeStackTrace( + error.stackTrace, + stackFrameFilter: null, + origin: null, + ) + } + }, + ), ); }); }); diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart new file mode 100644 index 0000000000..f66f264f20 --- /dev/null +++ b/dart/test/hub_test.dart @@ -0,0 +1,160 @@ +import 'package:collection/collection.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/hub.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; + +void main() { + bool scopeEquals(Scope a, Scope b) { + return identical(a, b) || + a.level == b.level && + a.transaction == b.transaction && + a.user == b.user && + IterableEquality().equals(a.fingerprint, b.fingerprint) && + IterableEquality().equals(a.breadcrumbs, b.breadcrumbs) && + MapEquality().equals(a.tags, b.tags) && + MapEquality().equals(a.extra, b.extra); + } + + group('Hub instantiation', () { + test('should not instantiate without a sentryOptions', () { + Hub hub; + expect(() => hub = Hub(null), throwsArgumentError); + expect(hub, null); + }); + + test('should not instantiate without a dsn', () { + expect(() => Hub(SentryOptions()), throwsArgumentError); + }); + + test('should instantiate with a dsn', () { + final hub = Hub(SentryOptions(dsn: fakeDsn)); + expect(hub.isEnabled, true); + }); + }); + + group('Hub captures', () { + Hub hub; + SentryOptions options; + MockSentryClient client; + + setUp(() { + options = SentryOptions(dsn: fakeDsn); + hub = Hub(options); + client = MockSentryClient(); + hub.bindClient(client); + }); + + test( + 'should capture event with the default scope', + () { + hub.captureEvent(fakeEvent); + expect( + scopeEquals( + verify( + client.captureEvent( + fakeEvent, + scope: captureAnyNamed('scope'), + stackFrameFilter: null, + ), + ).captured.first, + Scope(options), + ), + true, + ); + }, + ); + + test('should capture exception', () { + hub.captureException(fakeException); + + verify(client.captureException(fakeException)).called(1); + }); + + test('should capture message', () { + hub.captureMessage(fakeMessage.formatted, level: SeverityLevel.info); + verify( + client.captureMessage(fakeMessage.formatted, level: SeverityLevel.info), + ).called(1); + }); + }); + + group('Hub scope', () { + Hub hub; + SentryClient client; + + setUp(() { + hub = Hub(SentryOptions(dsn: fakeDsn)); + client = MockSentryClient(); + hub.bindClient(client); + }); + + test('should configure its scope', () { + hub.configureScope((Scope scope) { + scope + ..level = SeverityLevel.debug + ..user = fakeUser + ..fingerprint = ['1', '2']; + }); + hub.captureEvent(fakeEvent); + + hub.captureEvent(fakeEvent); + expect( + scopeEquals( + verify( + client.captureEvent( + fakeEvent, + scope: captureAnyNamed('scope'), + stackFrameFilter: null, + ), + ).captured.first, + Scope(SentryOptions(dsn: fakeDsn)) + ..level = SeverityLevel.debug + ..user = fakeUser + ..fingerprint = ['1', '2'], + ), + true, + ); + }); + }); + + group('Hub Client', () { + Hub hub; + SentryClient client; + SentryOptions options; + + setUp(() { + options = SentryOptions(dsn: fakeDsn); + hub = Hub(options); + client = MockSentryClient(); + hub.bindClient(client); + }); + + test('should bind a new client', () { + final client2 = MockSentryClient(); + hub.bindClient(client2); + hub.captureEvent(fakeEvent); + verify( + client2.captureEvent( + fakeEvent, + scope: anyNamed('scope'), + stackFrameFilter: null, + ), + ).called(1); + }); + + test('should close its client', () { + hub.close(); + + expect(hub.isEnabled, false); + verify(client.close()).called(1); + }); + }); + + test('clones', () { + // TODO I'm not sure how to test it + // could we set [hub.stack] as @visibleForTesting ? + }); +} diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart new file mode 100644 index 0000000000..fbc090b0c5 --- /dev/null +++ b/dart/test/mocks.dart @@ -0,0 +1,85 @@ +import 'package:mockito/mockito.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/protocol.dart'; + +class MockSentryClient extends Mock implements SentryClient {} + +final fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; + +final fakeException = Exception('Error'); + +final fakeMessage = Message('message 1', template: 'message %d', params: ['1']); + +final fakeUser = User(id: '1', email: 'test@test'); + +final fakeEvent = Event( + loggerName: 'main', + serverName: 'server.dart', + release: '1.4.0-preview.1', + environment: 'Test', + message: Message('This is an example Dart event.'), + transaction: '/example/app', + level: SeverityLevel.warning, + tags: const {'project-id': '7371'}, + extra: const {'company-name': 'Dart Inc'}, + fingerprint: const ['example-dart'], + userContext: const User( + id: '800', + username: 'first-user', + email: 'first@user.lan', + ipAddress: '127.0.0.1', + extras: {'first-sign-in': '2020-01-01'}), + breadcrumbs: [ + Breadcrumb('UI Lifecycle', DateTime.now().toUtc(), + category: 'ui.lifecycle', + type: 'navigation', + data: {'screen': 'MainActivity', 'state': 'created'}, + level: SeverityLevel.info) + ], + contexts: Contexts( + operatingSystem: const OperatingSystem( + name: 'Android', + version: '5.0.2', + build: 'LRX22G.P900XXS0BPL2', + kernelVersion: + 'Linux version 3.4.39-5726670 (dpi@SWHC3807) (gcc version 4.8 (GCC) ) #1 SMP PREEMPT Thu Dec 1 19:42:39 KST 2016', + rooted: false), + runtimes: [const Runtime(name: 'ART', version: '5')], + app: App( + name: 'Example Dart App', + version: '1.42.0', + identifier: 'HGT-App-13', + build: '93785', + buildType: 'release', + deviceAppHash: '5afd3a6', + startTime: DateTime.now().toUtc()), + browser: const Browser(name: 'Firefox', version: '42.0.1'), + device: Device( + name: 'SM-P900', + family: 'SM-P900', + model: 'SM-P900 (LRX22G)', + modelId: 'LRX22G', + arch: 'armeabi-v7a', + batteryLevel: 99, + orientation: Orientation.landscape, + manufacturer: 'samsung', + brand: 'samsung', + screenResolution: '2560x1600', + screenDensity: 2.1, + screenDpi: 320, + online: true, + charging: true, + lowMemory: true, + simulator: false, + memorySize: 1500, + freeMemory: 200, + usableMemory: 4294967296, + storageSize: 4294967296, + freeStorage: 2147483648, + externalStorageSize: 8589934592, + externalFreeStorage: 2863311530, + bootTime: DateTime.now().toUtc(), + timezone: 'America/Toronto', + ), + ), +); diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index 2139b3d772..02dc4e78c8 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; @@ -164,6 +165,17 @@ void main() { expect(sut.extra.length, 0); }); + + test('clones', () { + final sut = fixture.getSut(); + final clone = sut.clone(); + expect(sut.user, clone.user); + expect(sut.transaction, clone.transaction); + expect(sut.extra, clone.extra); + expect(sut.tags, clone.tags); + expect(sut.breadcrumbs, clone.breadcrumbs); + expect(ListEquality().equals(sut.fingerprint, clone.fingerprint), true); + }); } class Fixture { diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 8fa961361a..bd4154b538 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -2,108 +2,53 @@ import 'package:mockito/mockito.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; + void main() { group('Sentry static entry', () { SentryClient client; Exception anException; - final dns = 'https://abc@def.ingest.sentry.io/1234567'; - setUp(() { - Sentry.init((options) => options.dsn = dns); + Sentry.init((options) => options.dsn = fakeDsn); + anException = Exception('anException'); client = MockSentryClient(); Sentry.initClient(client); }); test('should capture the event', () { - Sentry.captureEvent(event); - verify(client.captureEvent(event: event)).called(1); + Sentry.captureEvent(fakeEvent); + verify( + client.captureEvent( + fakeEvent, + scope: anyNamed('scope'), + stackFrameFilter: null, + ), + ).called(1); }); - test('should capture the event', () { - Sentry.captureEvent(event); - verify(client.captureEvent(event: event)).called(1); + test('should not capture a null event', () async { + await Sentry.captureEvent(null); + verifyNever(client.captureEvent(fakeEvent)); }); - test('should capture the exception', () { - Sentry.captureException(anException); - verify(client.captureException(exception: anException)).called(1); + test('should not capture a null exception', () async { + await Sentry.captureException(null); + verifyNever( + client.captureException( + any, + stackTrace: anyNamed('stackTrace'), + ), + ); + }); + + test('should capture the exception', () async { + await Sentry.captureException(anException); + verify( + client.captureException(anException, stackTrace: null), + ).called(1); }); }); } - -class MockSentryClient extends Mock implements SentryClient {} - -final event = Event( - loggerName: 'main', - serverName: 'server.dart', - release: '1.4.0-preview.1', - environment: 'Test', - message: Message(formatted: 'This is an example Dart event.'), - transaction: '/example/app', - level: SeverityLevel.warning, - tags: const {'project-id': '7371'}, - extra: const {'company-name': 'Dart Inc'}, - fingerprint: const ['example-dart'], - userContext: const User( - id: '800', - username: 'first-user', - email: 'first@user.lan', - ipAddress: '127.0.0.1', - extras: {'first-sign-in': '2020-01-01'}), - breadcrumbs: [ - Breadcrumb('UI Lifecycle', DateTime.now().toUtc(), - category: 'ui.lifecycle', - type: 'navigation', - data: {'screen': 'MainActivity', 'state': 'created'}, - level: SeverityLevel.info) - ], - contexts: Contexts( - operatingSystem: const OperatingSystem( - name: 'Android', - version: '5.0.2', - build: 'LRX22G.P900XXS0BPL2', - kernelVersion: - 'Linux version 3.4.39-5726670 (dpi@SWHC3807) (gcc version 4.8 (GCC) ) #1 SMP PREEMPT Thu Dec 1 19:42:39 KST 2016', - rooted: false), - runtimes: [const Runtime(name: 'ART', version: '5')], - app: App( - name: 'Example Dart App', - version: '1.42.0', - identifier: 'HGT-App-13', - build: '93785', - buildType: 'release', - deviceAppHash: '5afd3a6', - startTime: DateTime.now().toUtc()), - browser: const Browser(name: 'Firefox', version: '42.0.1'), - device: Device( - name: 'SM-P900', - family: 'SM-P900', - model: 'SM-P900 (LRX22G)', - modelId: 'LRX22G', - arch: 'armeabi-v7a', - batteryLevel: 99, - orientation: Orientation.landscape, - manufacturer: 'samsung', - brand: 'samsung', - screenResolution: '2560x1600', - screenDensity: 2.1, - screenDpi: 320, - online: true, - charging: true, - lowMemory: true, - simulator: false, - memorySize: 1500, - freeMemory: 200, - usableMemory: 4294967296, - storageSize: 4294967296, - freeStorage: 2147483648, - externalStorageSize: 8589934592, - externalFreeStorage: 2863311530, - bootTime: DateTime.now().toUtc(), - timezone: 'America/Toronto', - ), - ), -); diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index ecc0c676db..b5bc0d8a4e 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -85,11 +85,9 @@ Future testCaptureException( try { throw ArgumentError('Test error'); } catch (error, stackTrace) { - final response = - await client.captureException(exception: error, stackTrace: stackTrace); - expect(response.isSuccessful, true); - expect(response.eventId, 'test-event-id'); - expect(response.error, null); + final sentryId = + await client.captureException(error, stackTrace: stackTrace); + expect('${sentryId.id}', 'test-event-id'); } expect(postUri, client.postUri); @@ -110,6 +108,7 @@ Future testCaptureException( } final Map stacktrace = data.remove('stacktrace') as Map; + expect(stacktrace['frames'], const TypeMatcher()); expect(stacktrace['frames'], isNotEmpty); @@ -245,11 +244,9 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { try { throw ArgumentError('Test error'); } catch (error, stackTrace) { - final response = await client.captureException( - exception: error, stackTrace: stackTrace); - expect(response.isSuccessful, true); - expect(response.eventId, 'test-event-id'); - expect(response.error, null); + final sentryId = + await client.captureException(error, stackTrace: stackTrace); + expect('${sentryId.id}', 'test-event-id'); } testHeaders( @@ -303,12 +300,9 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { try { throw ArgumentError('Test error'); } catch (error, stackTrace) { - final 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'); + final sentryId = + await client.captureException(error, stackTrace: stackTrace); + expect('${sentryId.id}', SentryId.emptyId); } await client.close(); @@ -367,9 +361,9 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { exception: error, stackTrace: stackTrace, userContext: eventUserContext); - await client.captureEvent(event: eventWithoutContext); + await client.captureEvent(eventWithoutContext); expect(loggedUserId, clientUserContext.id); - await client.captureEvent(event: eventWithContext); + await client.captureEvent(eventWithContext); expect(loggedUserId, eventUserContext.id); } diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 7d1c365ad1..4154ccccfe 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -41,7 +41,7 @@ Future main() async { release: _release, sdk: _sentry.sdk, ); - await _sentry.captureEvent(event: event); + await _sentry.captureEvent(event); }); } @@ -68,7 +68,7 @@ extension SentryExtensions on SentryClient { if (stackTrace != null) { stackTrace = StackTrace.fromString(stackTrace as String); } - return captureException(exception: error[0], stackTrace: stackTrace); + return captureException(error[0], stackTrace: stackTrace); } else { return Future.value(); }