diff --git a/CHANGELOG.md b/CHANGELOG.md index 448f3c3aba..6a4c265f91 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 +- feat: sentry options #116 # `package:sentry` changelog diff --git a/dart/example/event_example.dart b/dart/example/event_example.dart index 72596107f0..d4a64dc64f 100644 --- a/dart/example/event_example.dart +++ b/dart/example/event_example.dart @@ -7,7 +7,7 @@ final event = Event( environment: 'Test', message: Message('This is an example Dart event.'), transaction: '/example/app', - level: SeverityLevel.warning, + level: SentryLevel.warning, tags: const {'project-id': '7371'}, extra: const {'company-name': 'Dart Inc'}, fingerprint: const ['example-dart'], @@ -22,7 +22,7 @@ final event = Event( category: 'ui.lifecycle', type: 'navigation', data: {'screen': 'MainActivity', 'state': 'created'}, - level: SeverityLevel.info) + level: SentryLevel.info) ], contexts: Contexts( operatingSystem: const OperatingSystem( diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index eadcff84b9..b6bf185a81 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -208,7 +208,7 @@ abstract class SentryClient { /// Reports the [template] Future captureMessage( String formatted, { - SeverityLevel level = SeverityLevel.info, + SentryLevel level = SentryLevel.info, String template, List params, }) { diff --git a/dart/lib/src/diagnostic_logger.dart b/dart/lib/src/diagnostic_logger.dart new file mode 100644 index 0000000000..bc2737923f --- /dev/null +++ b/dart/lib/src/diagnostic_logger.dart @@ -0,0 +1,18 @@ +import 'package:sentry/sentry.dart'; + +class DiagnosticLogger { + final Logger _logger; + final SentryOptions _options; + + DiagnosticLogger(this._logger, this._options); + + void log(SentryLevel level, String message) { + if (_isEnabled(level)) { + _logger(level, message); + } + } + + bool _isEnabled(SentryLevel level) { + return _options.debug && level.ordinal >= _options.diagnosticLevel.ordinal; + } +} diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 951b62ada7..c3e61aa394 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -65,12 +65,12 @@ class Hub { if (!_isEnabled) { _options.logger( - SeverityLevel.warning, + SentryLevel.warning, "Instance is disabled and this 'captureEvent' call is a no-op.", ); } else if (event == null) { _options.logger( - SeverityLevel.warning, + SentryLevel.warning, 'captureEvent called with null parameter.', ); } else { @@ -81,7 +81,7 @@ class Hub { } catch (err) { /* TODO add Event.id */ _options.logger( - SeverityLevel.error, + SentryLevel.error, 'Error while capturing event with id: ${event}', ); } finally { @@ -89,7 +89,7 @@ class Hub { } } else { _options.logger( - SeverityLevel.fatal, + SentryLevel.fatal, 'Stack peek was null when captureEvent', ); } @@ -106,12 +106,12 @@ class Hub { if (!_isEnabled) { _options.logger( - SeverityLevel.warning, + SentryLevel.warning, "Instance is disabled and this 'captureException' call is a no-op.", ); } else if (throwable == null) { _options.logger( - SeverityLevel.warning, + SentryLevel.warning, 'captureException called with null parameter.', ); } else { @@ -125,7 +125,7 @@ class Hub { ); } catch (err) { _options.logger( - SeverityLevel.error, + SentryLevel.error, 'Error while capturing exception : ${throwable}', ); } finally { @@ -133,7 +133,7 @@ class Hub { } } else { _options.logger( - SeverityLevel.fatal, + SentryLevel.fatal, 'Stack peek was null when captureException', ); } @@ -145,7 +145,7 @@ class Hub { /// Captures the message. Future captureMessage( String message, { - SeverityLevel level = SeverityLevel.info, + SentryLevel level = SentryLevel.info, String template, List params, }) async { @@ -153,12 +153,12 @@ class Hub { if (!_isEnabled) { _options.logger( - SeverityLevel.warning, + SentryLevel.warning, "Instance is disabled and this 'captureMessage' call is a no-op.", ); } else if (message == null) { _options.logger( - SeverityLevel.warning, + SentryLevel.warning, 'captureMessage called with null parameter.', ); } else { @@ -174,7 +174,7 @@ class Hub { ); } catch (err) { _options.logger( - SeverityLevel.error, + SentryLevel.error, 'Error while capturing message with id: ${message}', ); } finally { @@ -182,7 +182,7 @@ class Hub { } } else { _options.logger( - SeverityLevel.fatal, + SentryLevel.fatal, 'Stack peek was null when captureMessage', ); } @@ -193,21 +193,21 @@ class Hub { /// Binds a different client to the hub void bindClient(SentryClient client) { if (!_isEnabled) { - _options.logger(SeverityLevel.warning, + _options.logger(SentryLevel.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.'); + _options.logger(SentryLevel.debug, 'New client bound to scope.'); item.client = client; } else { - _options.logger(SeverityLevel.debug, 'NoOp client bound to scope.'); + _options.logger(SentryLevel.debug, 'NoOp client bound to scope.'); item.client = NoOpSentryClient(); } } else { _options.logger( - SeverityLevel.fatal, + SentryLevel.fatal, 'Stack peek was null when bindClient', ); } @@ -217,7 +217,7 @@ class Hub { /// Clones the Hub Hub clone() { if (!_isEnabled) { - _options..logger(SeverityLevel.warning, 'Disabled Hub cloned.'); + _options..logger(SentryLevel.warning, 'Disabled Hub cloned.'); } final clone = Hub(_options); for (final item in _stack) { @@ -230,7 +230,7 @@ class Hub { void close() { if (!_isEnabled) { _options.logger( - SeverityLevel.warning, + SentryLevel.warning, "Instance is disabled and this 'close' call is a no-op.", ); } else { @@ -240,13 +240,13 @@ class Hub { item.client.close(); } catch (err) { _options.logger( - SeverityLevel.error, + SentryLevel.error, 'Error while closing the Hub.', ); } } else { _options.logger( - SeverityLevel.fatal, + SentryLevel.fatal, 'Stack peek was NULL when closing Hub', ); } @@ -258,7 +258,7 @@ class Hub { void configureScope(ScopeCallback callback) { if (!_isEnabled) { _options.logger( - SeverityLevel.warning, + SentryLevel.warning, "Instance is disabled and this 'configureScope' call is a no-op.", ); } else { @@ -268,13 +268,13 @@ class Hub { callback(item.scope); } catch (err) { _options.logger( - SeverityLevel.error, + SentryLevel.error, "Error in the 'configureScope' callback.", ); } } else { _options.logger( - SeverityLevel.fatal, + SentryLevel.fatal, 'Stack peek was NULL when configureScope', ); } diff --git a/dart/lib/src/noop_client.dart b/dart/lib/src/noop_client.dart index 88293b343b..a7be5701a6 100644 --- a/dart/lib/src/noop_client.dart +++ b/dart/lib/src/noop_client.dart @@ -30,7 +30,7 @@ class NoOpSentryClient implements SentryClient { @override Future captureMessage( String message, { - SeverityLevel level = SeverityLevel.info, + SentryLevel level = SentryLevel.info, String template, List params, }) => diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart new file mode 100644 index 0000000000..2c479c4492 --- /dev/null +++ b/dart/lib/src/noop_hub.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'client.dart'; +import 'hub.dart'; +import 'protocol/event.dart'; +import 'protocol/sentry_level.dart'; +import 'protocol/sentry_id.dart'; + +class NoOpHub implements Hub { + NoOpHub._(); + + static final NoOpHub _instance = NoOpHub._(); + + factory NoOpHub() { + return _instance; + } + + @override + void bindClient(SentryClient client) {} + + @override + Future captureEvent(Event event) => Future.value(SentryId.empty()); + + @override + Future captureException(throwable, {stackTrace}) => + Future.value(SentryId.empty()); + + @override + Future captureMessage( + String message, { + SentryLevel level = SentryLevel.info, + String template, + List params, + }) => + Future.value(SentryId.empty()); + + @override + Hub clone() => this; + + @override + void close() {} + + @override + void configureScope(callback) {} + + @override + bool get isEnabled => false; + + @override + SentryId get lastEventId => SentryId.empty(); +} diff --git a/dart/lib/src/protocol.dart b/dart/lib/src/protocol.dart index 17a5f47ce6..2aa0d3a3c8 100644 --- a/dart/lib/src/protocol.dart +++ b/dart/lib/src/protocol.dart @@ -5,7 +5,7 @@ export 'protocol/contexts.dart'; export 'protocol/device.dart'; export 'protocol/dsn.dart'; export 'protocol/event.dart'; -export 'protocol/level.dart'; +export 'protocol/sentry_level.dart'; export 'protocol/message.dart'; export 'protocol/package.dart'; export 'protocol/runtime.dart'; diff --git a/dart/lib/src/protocol/breadcrumb.dart b/dart/lib/src/protocol/breadcrumb.dart index 1d9c4937b0..a458ba7c69 100644 --- a/dart/lib/src/protocol/breadcrumb.dart +++ b/dart/lib/src/protocol/breadcrumb.dart @@ -1,5 +1,5 @@ import '../utils.dart'; -import 'level.dart'; +import 'sentry_level.dart'; /// Structed data to describe more information pior to the event [captured][SentryClient.captureEvent]. /// @@ -24,7 +24,7 @@ class Breadcrumb { this.timestamp, { this.category, this.data, - this.level = SeverityLevel.info, + this.level = SentryLevel.info, this.type, }) : assert(timestamp != null); @@ -52,7 +52,7 @@ class Breadcrumb { /// Severity of the breadcrumb. /// /// This field is optional and may be set to null. - final SeverityLevel level; + final SentryLevel level; /// Describes what type of breadcrumb this is. /// diff --git a/dart/lib/src/protocol/event.dart b/dart/lib/src/protocol/event.dart index 76ee95157f..a1720391b8 100644 --- a/dart/lib/src/protocol/event.dart +++ b/dart/lib/src/protocol/event.dart @@ -67,7 +67,7 @@ class Event { final String transaction; /// How important this event is. - final SeverityLevel level; + final SentryLevel level; /// What caused this event to be logged. final String culprit; @@ -125,7 +125,7 @@ class Event { String transaction, dynamic exception, dynamic stackTrace, - SeverityLevel level, + SentryLevel level, String culprit, Map tags, Map extra, diff --git a/dart/lib/src/protocol/level.dart b/dart/lib/src/protocol/level.dart deleted file mode 100644 index 9e0befe070..0000000000 --- a/dart/lib/src/protocol/level.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:meta/meta.dart'; - -/// Severity of the logged [Event]. -@immutable -class SeverityLevel { - const SeverityLevel._(this.name); - - static const fatal = SeverityLevel._('fatal'); - static const error = SeverityLevel._('error'); - static const warning = SeverityLevel._('warning'); - static const info = SeverityLevel._('info'); - static const debug = SeverityLevel._('debug'); - - /// API name of the level as it is encoded in the JSON protocol. - final String name; -} diff --git a/dart/lib/src/protocol/sentry_level.dart b/dart/lib/src/protocol/sentry_level.dart new file mode 100644 index 0000000000..a0e59e32fc --- /dev/null +++ b/dart/lib/src/protocol/sentry_level.dart @@ -0,0 +1,17 @@ +import 'package:meta/meta.dart'; + +/// Severity of the logged [Event]. +@immutable +class SentryLevel { + const SentryLevel._(this.name, this.ordinal); + + static const fatal = SentryLevel._('fatal', 5); + static const error = SentryLevel._('error', 4); + static const warning = SentryLevel._('warning', 3); + static const info = SentryLevel._('info', 2); + static const debug = SentryLevel._('debug', 1); + + /// API name of the level as it is encoded in the JSON protocol. + final String name; + final int ordinal; +} diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 128b7d1ff4..11c2530795 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -6,11 +6,11 @@ import 'sentry_options.dart'; /// Scope data to be sent with the event class Scope { /// How important this event is. - SeverityLevel _level; + SentryLevel _level; - SeverityLevel get level => _level; + SentryLevel get level => _level; - set level(SeverityLevel level) { + set level(SentryLevel level) { _level = level; } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 7e1b0b4c05..ffd94eace6 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -20,6 +20,7 @@ class Sentry { static void init(OptionsConfiguration optionsConfiguration) { final options = SentryOptions(); optionsConfiguration(options); + _setDefaultConfiguration(options); _hub = Hub(options); } @@ -39,6 +40,14 @@ class Sentry { /// Close the client SDK static Future close() async => _hub.close(); + static void _setDefaultConfiguration(SentryOptions options) { + // TODO: check DSN nullability and empty + + if (options.debug && options.logger == noOpLogger) { + options.logger = dartLogger; + } + } + /// client injector only use for testing @visibleForTesting 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 6305bca2e7..e02345c42b 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -1,15 +1,14 @@ import 'package:http/http.dart'; import 'package:sentry/sentry.dart'; - +import 'diagnostic_logger.dart'; +import 'hub.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 - static final SeverityLevel defaultDiagnosticLevel = SeverityLevel.debug; + static final SentryLevel defaultDiagnosticLevel = SentryLevel.debug; /// The DSN tells the SDK where to send the events to. If this value is not provided, the SDK will /// just not send any events. @@ -27,7 +26,7 @@ class SentryOptions { /// If [compressPayload] is `true` the outgoing HTTP payloads are compressed /// using gzip. Otherwise, the payloads are sent in plain UTF8-encoded JSON /// text. If not specified, the compression is enabled by default. - bool compressPayload; + bool compressPayload = false; /// If [httpClient] is provided, it is used instead of the default client to /// make HTTP calls to Sentry.io. This is useful in tests. @@ -47,10 +46,99 @@ class SentryOptions { int maxBreadcrumbs; - final Logger _logger; + /// Logger interface to log useful debugging information if debug is enabled + Logger _logger = noOpLogger; + + Logger get logger => _logger; + + set logger(Logger logger) { + _logger = logger != null ? DiagnosticLogger(logger, this) : noOpLogger; + } + + /// Are callbacks that run for every event. They can either return a new event which in most cases + /// means just adding data OR return null in case the event will be dropped and not sent. + final List _eventProcessors = []; + + List get eventProcessors => + List.unmodifiable(_eventProcessors); + + /// Code that provides middlewares, bindings or hooks into certain frameworks or environments, + /// along with code that inserts those bindings and activates them. + final List _integrations = []; + + // TODO: shutdownTimeout, flushTimeoutMillis + // https://api.dart.dev/stable/2.10.2/dart-io/HttpClient/close.html doesn't have a timeout param, we'd need to implement manually + + List get integrations => List.unmodifiable(_integrations); + + /// Turns debug mode on or off. If debug is enabled SDK will attempt to print out useful debugging + /// information if something goes wrong. Default is disabled. + bool debug = false; + + /// minimum LogLevel to be used if debug is enabled + SentryLevel _diagnosticLevel = defaultDiagnosticLevel; + + set diagnosticLevel(SentryLevel level) { + _diagnosticLevel = level ?? defaultDiagnosticLevel; + } + + SentryLevel get diagnosticLevel => _diagnosticLevel; + + /// Sentry client name used for the HTTP authHeader and userAgent eg + /// sentry.{language}.{platform}/{version} eg sentry.java.android/2.0.0 would be a valid case + String sentryClientName; + + /// 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; + + /// 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; + + /// Sets the release. SDK will try to automatically configure a release out of the box + String release; + +// TODO: probably its part of environmentAttributes + /// Sets the environment. This string is freeform and not set by default. A release can be + /// associated with more than one environment to separate them in the UI Think staging vs prod or + /// similar. + String environment; + + /// Configures the sample rate as a percentage of events to be sent in the range of 0.0 to 1.0. if + /// 1.0 is set it means that 100% of events are sent. If set to 0.1 only 10% of events will be + /// sent. Events are picked randomly. Default is 1.0 (disabled) + double sampleRate = 1.0; + + /// A list of string prefixes of module names that do not belong to the app, but rather third-party + /// packages. Modules considered not to be part of the app will be hidden from stack traces by + /// default. + final List _inAppExcludes = []; - Logger get logger => _logger ?? defaultLogger; + List get inAppExcludes => List.unmodifiable(_inAppExcludes); + /// A list of string prefixes of module names that belong to the app. This option takes precedence + /// over inAppExcludes. + final List _inAppIncludes = []; + + List get inAppIncludes => List.unmodifiable(_inAppIncludes); + + // TODO: transport, transportGate, connectionTimeoutMillis, readTimeoutMillis, hostnameVerifier, sslSocketFactory, proxy + + /// Sets the distribution. Think about it together with release and environment + String dist; + + /// The server name used in the Sentry messages. + String serverName; + + /// SdkVersion object that contains the Sentry Client Name and its version + Sdk sdkVersion; + + // TODO: Scope observers, enableScopeSync + + // TODO: sendDefaultPii + + // TODO: those ctor params could be set on Sentry._setDefaultConfiguration or instantiate by default here SentryOptions({ this.dsn, this.environmentAttributes, @@ -58,11 +146,54 @@ class SentryOptions { this.httpClient, this.clock, this.uuidGenerator, - Logger logger, - this.maxBreadcrumbs = 100, - }) : _logger = logger; + }); + + /// Adds an event processor + void addEventProcessor(EventProcessor eventProcessor) { + _eventProcessors.add(eventProcessor); + } + + /// Removes an event processor + void removeEventProcessor(EventProcessor eventProcessor) { + _eventProcessors.remove(eventProcessor); + } + + /// Adds an integration + void addIntegration(Integration integration) { + _integrations.add(integration); + } + + /// Removes an integration + void removeIntegration(Integration integration) { + _integrations.remove(integration); + } + + /// Adds an inAppExclude + void addInAppExclude(String inApp) { + _inAppExcludes.add(inApp); + } + + /// Adds an inAppIncludes + void addInAppInclude(String inApp) { + _inAppIncludes.add(inApp); + } } -void defaultLogger(SeverityLevel level, String message) { +typedef BeforeSendCallback = Event Function(Event event, dynamic hint); + +typedef BeforeBreadcrumbCallback = Breadcrumb Function( + Breadcrumb breadcrumb, + dynamic hint, +); + +typedef EventProcessor = Event Function(Event event, dynamic hint); + +typedef Integration = Function(Hub hub, SentryOptions options); + +typedef Logger = Function(SentryLevel level, String message); + +void noOpLogger(SentryLevel level, String message) {} + +void dartLogger(SentryLevel level, String message) { print('[$level] $message'); } diff --git a/dart/test/event_test.dart b/dart/test/event_test.dart index ff66e2ab54..6a249c91ed 100644 --- a/dart/test/event_test.dart +++ b/dart/test/event_test.dart @@ -13,7 +13,7 @@ void main() { Breadcrumb( 'example log', DateTime.utc(2019), - level: SeverityLevel.debug, + level: SentryLevel.debug, category: 'test', ).toJson(), { @@ -53,7 +53,7 @@ void main() { final breadcrumbs = [ Breadcrumb('test log', DateTime.utc(2019), - level: SeverityLevel.debug, category: 'test'), + level: SentryLevel.debug, category: 'test'), ]; final error = StateError('test-error'); @@ -69,7 +69,7 @@ void main() { ), transaction: '/test/1', exception: error, - level: SeverityLevel.debug, + level: SentryLevel.debug, culprit: 'Professor Moriarty', tags: const { 'a': 'b', diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index f66f264f20..79dd59d69d 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -74,9 +74,9 @@ void main() { }); test('should capture message', () { - hub.captureMessage(fakeMessage.formatted, level: SeverityLevel.info); + hub.captureMessage(fakeMessage.formatted, level: SentryLevel.info); verify( - client.captureMessage(fakeMessage.formatted, level: SeverityLevel.info), + client.captureMessage(fakeMessage.formatted, level: SentryLevel.info), ).called(1); }); }); @@ -94,7 +94,7 @@ void main() { test('should configure its scope', () { hub.configureScope((Scope scope) { scope - ..level = SeverityLevel.debug + ..level = SentryLevel.debug ..user = fakeUser ..fingerprint = ['1', '2']; }); @@ -111,7 +111,7 @@ void main() { ), ).captured.first, Scope(SentryOptions(dsn: fakeDsn)) - ..level = SeverityLevel.debug + ..level = SentryLevel.debug ..user = fakeUser ..fingerprint = ['1', '2'], ), diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index fbc090b0c5..b5a4c44b5d 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -19,7 +19,7 @@ final fakeEvent = Event( environment: 'Test', message: Message('This is an example Dart event.'), transaction: '/example/app', - level: SeverityLevel.warning, + level: SentryLevel.warning, tags: const {'project-id': '7371'}, extra: const {'company-name': 'Dart Inc'}, fingerprint: const ['example-dart'], @@ -34,7 +34,7 @@ final fakeEvent = Event( category: 'ui.lifecycle', type: 'navigation', data: {'screen': 'MainActivity', 'state': 'created'}, - level: SeverityLevel.info) + level: SentryLevel.info) ], contexts: Contexts( operatingSystem: const OperatingSystem( diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index 02dc4e78c8..d7fcea8edf 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -5,12 +5,12 @@ import 'package:test/test.dart'; void main() { final fixture = Fixture(); - test('sets $SeverityLevel', () { + test('sets $SentryLevel', () { final sut = fixture.getSut(); - sut.level = SeverityLevel.debug; + sut.level = SentryLevel.debug; - expect(sut.level, SeverityLevel.debug); + expect(sut.level, SentryLevel.debug); }); test('sets transaction', () { @@ -137,7 +137,7 @@ void main() { final breadcrumb1 = Breadcrumb('test log', DateTime.utc(2019)); sut.addBreadcrumb(breadcrumb1); - sut.level = SeverityLevel.debug; + sut.level = SentryLevel.debug; sut.transaction = 'test'; final user = User(id: 'test');