diff --git a/CHANGELOG.md b/CHANGELOG.md index b313feb811..67625453d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Base the sdk name on the platform (`sentry.dart` for io & flutter, `sentry.dart.browser` in a browser context) #103 - Single changelog and readme for both packages #105 - expect a sdkName based on the test platform #105 +- Added Scope and Breadcrumb ring buffer #109 # `package:sentry` changelog diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index dcaa278d42..d81860c038 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -6,3 +6,5 @@ export 'src/client.dart'; export 'src/protocol.dart'; export 'src/version.dart'; +export 'src/scope.dart'; +export 'src/sentry_options.dart'; diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart new file mode 100644 index 0000000000..63e1c85ec8 --- /dev/null +++ b/dart/lib/src/scope.dart @@ -0,0 +1,139 @@ +import 'dart:collection'; + +import 'protocol.dart'; +import 'sentry_options.dart'; + +/// Scope data to be sent with the event +class Scope { + /// How important this event is. + SeverityLevel _level; + + SeverityLevel get level => _level; + + set level(SeverityLevel level) { + _level = level; + } + + /// The name of the transaction which generated this event, + /// for example, the route name: `"/users//"`. + String _transaction; + + String get transaction => _transaction; + + set transaction(String transaction) { + _transaction = transaction; + } + + /// Information about the current user. + User _user; + + User get user => _user; + + set user(User user) { + _user = user; + } + + /// Used to deduplicate events by grouping ones with the same fingerprint + /// together. + /// + /// Example: + /// + /// // A completely custom fingerprint: + /// var custom = ['foo', 'bar', 'baz']; + List _fingerprint; + + List get fingerprint => + _fingerprint != null ? List.unmodifiable(_fingerprint) : null; + + set fingerprint(List fingerprint) { + _fingerprint = fingerprint; + } + + /// List of breadcrumbs for this scope. + /// + /// See also: + /// * https://docs.sentry.io/enriching-error-data/breadcrumbs/?platform=javascript + final Queue _breadcrumbs = Queue(); + + /// Unmodifiable List of breadcrumbs + List get breadcrumbs => List.unmodifiable(_breadcrumbs); + + /// Name/value pairs that events can be searched by. + final Map _tags = {}; + + Map get tags => Map.unmodifiable(_tags); + + /// Arbitrary name/value pairs attached to the scope. + /// + /// Sentry.io docs do not talk about restrictions on the values, other than + /// they must be JSON-serializable. + final Map _extra = {}; + + Map get extra => Map.unmodifiable(_extra); + + // TODO: EventProcessors, Contexts, BeforeBreadcrumbCallback, Breadcrumb Hint, clone + + final SentryOptions _options; + + Scope(this._options) : assert(_options != null, 'SentryOptions is required'); + + /// Adds a breadcrumb to the breadcrumbs queue + void addBreadcrumb(Breadcrumb breadcrumb) { + assert(breadcrumb != null, "Breadcrumb can't be null"); + + // bail out if maxBreadcrumbs is zero + if (_options.maxBreadcrumbs == 0) { + return; + } + + // remove first item if list if full + if (_breadcrumbs.length >= _options.maxBreadcrumbs && + _breadcrumbs.isNotEmpty) { + _breadcrumbs.removeFirst(); + } + + _breadcrumbs.add(breadcrumb); + } + + /// Clear all the breadcrumbs + void clearBreadcrumbs() { + _breadcrumbs.clear(); + } + + /// Resets the Scope to its default state + void clear() { + clearBreadcrumbs(); + _level = null; + _transaction = null; + _user = null; + _fingerprint = null; + _tags.clear(); + _extra.clear(); + } + + /// Sets a tag to the Scope + void setTag(String key, String value) { + assert(key != null, "Key can't be null"); + assert(value != null, "Key can't be null"); + + _tags[key] = value; + } + + /// Removes a tag from the Scope + void removeTag(String key) { + _tags.remove(key); + } + + /// Sets an extra to the Scope + void setExtra(String key, dynamic value) { + assert(key != null, "Key can't be null"); + assert(value != null, "Value can't be null"); + + _extra[key] = value; + } + + /// Removes an extra from the Scope + void removeExtra(String key) { + _extra.remove(key); + } +} diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart new file mode 100644 index 0000000000..cde1d36fe5 --- /dev/null +++ b/dart/lib/src/sentry_options.dart @@ -0,0 +1,5 @@ +/// Sentry SDK options +class SentryOptions { + /// This variable controls the total amount of breadcrumbs that should be captured Default is 100 + int maxBreadcrumbs = 100; +} diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index e0fd10bec6..389acefa7e 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: uuid: ^2.0.0 dev_dependencies: - mockito: ^4.1.2 + mockito: ^4.1.1 pedantic: ^1.9.2 test: ^1.15.4 yaml: ^2.2.1 diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart new file mode 100644 index 0000000000..2139b3d772 --- /dev/null +++ b/dart/test/scope_test.dart @@ -0,0 +1,175 @@ +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final fixture = Fixture(); + + test('sets $SeverityLevel', () { + final sut = fixture.getSut(); + + sut.level = SeverityLevel.debug; + + expect(sut.level, SeverityLevel.debug); + }); + + test('sets transaction', () { + final sut = fixture.getSut(); + + sut.transaction = 'test'; + + expect(sut.transaction, 'test'); + }); + + test('sets $User', () { + final sut = fixture.getSut(); + + final user = User(id: 'test'); + sut.user = user; + + expect(sut.user, user); + }); + + test('sets fingerprint', () { + final sut = fixture.getSut(); + + final fingerprints = ['test']; + sut.fingerprint = fingerprints; + + expect(sut.fingerprint, fingerprints); + }); + + test('adds $Breadcrumb', () { + final sut = fixture.getSut(); + + final breadcrumb = Breadcrumb('test log', DateTime.utc(2019)); + sut.addBreadcrumb(breadcrumb); + + expect(sut.breadcrumbs.last, breadcrumb); + }); + + test('respects max $Breadcrumb', () { + final maxBreadcrumbs = 2; + final sut = fixture.getSut(maxBreadcrumbs: maxBreadcrumbs); + + final breadcrumb1 = Breadcrumb('test log', DateTime.utc(2019)); + final breadcrumb2 = Breadcrumb('test log', DateTime.utc(2019)); + final breadcrumb3 = Breadcrumb('test log', DateTime.utc(2019)); + sut.addBreadcrumb(breadcrumb1); + sut.addBreadcrumb(breadcrumb2); + sut.addBreadcrumb(breadcrumb3); + + expect(sut.breadcrumbs.length, maxBreadcrumbs); + }); + + test('rotates $Breadcrumb', () { + final sut = fixture.getSut(maxBreadcrumbs: 2); + + final breadcrumb1 = Breadcrumb('test log', DateTime.utc(2019)); + final breadcrumb2 = Breadcrumb('test log', DateTime.utc(2019)); + final breadcrumb3 = Breadcrumb('test log', DateTime.utc(2019)); + sut.addBreadcrumb(breadcrumb1); + sut.addBreadcrumb(breadcrumb2); + sut.addBreadcrumb(breadcrumb3); + + expect(sut.breadcrumbs.first, breadcrumb2); + + expect(sut.breadcrumbs.last, breadcrumb3); + }); + + test('empty $Breadcrumb list', () { + final maxBreadcrumbs = 0; + final sut = fixture.getSut(maxBreadcrumbs: maxBreadcrumbs); + + final breadcrumb1 = Breadcrumb('test log', DateTime.utc(2019)); + sut.addBreadcrumb(breadcrumb1); + + expect(sut.breadcrumbs.length, maxBreadcrumbs); + }); + + test('clears $Breadcrumb list', () { + final sut = fixture.getSut(); + + final breadcrumb1 = Breadcrumb('test log', DateTime.utc(2019)); + sut.addBreadcrumb(breadcrumb1); + sut.clear(); + + expect(sut.breadcrumbs.length, 0); + }); + + test('sets tag', () { + final sut = fixture.getSut(); + + sut.setTag('test', 'test'); + + expect(sut.tags['test'], 'test'); + }); + + test('removes tag', () { + final sut = fixture.getSut(); + + sut.setTag('test', 'test'); + sut.removeTag('test'); + + expect(sut.tags['test'], null); + }); + + test('sets extra', () { + final sut = fixture.getSut(); + + sut.setExtra('test', 'test'); + + expect(sut.extra['test'], 'test'); + }); + + test('removes extra', () { + final sut = fixture.getSut(); + + sut.setExtra('test', 'test'); + sut.removeExtra('test'); + + expect(sut.extra['test'], null); + }); + + test('clears $Scope', () { + final sut = fixture.getSut(); + + final breadcrumb1 = Breadcrumb('test log', DateTime.utc(2019)); + sut.addBreadcrumb(breadcrumb1); + + sut.level = SeverityLevel.debug; + sut.transaction = 'test'; + + final user = User(id: 'test'); + sut.user = user; + + final fingerprints = ['test']; + sut.fingerprint = fingerprints; + + sut.setTag('test', 'test'); + sut.setExtra('test', 'test'); + + sut.clear(); + + expect(sut.breadcrumbs.length, 0); + + expect(sut.level, null); + + expect(sut.transaction, null); + + expect(sut.user, null); + + expect(sut.fingerprint, null); + + expect(sut.tags.length, 0); + + expect(sut.extra.length, 0); + }); +} + +class Fixture { + Scope getSut({int maxBreadcrumbs = 100}) { + final options = SentryOptions(); + options.maxBreadcrumbs = maxBreadcrumbs; + return Scope(options); + } +}