From 501897806185cb0c4151d435d88455c51fc6b828 Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Wed, 14 Oct 2020 17:41:19 +0200 Subject: [PATCH 01/34] feat : static SDK main entry withSentry.init(options),... (#108) * feat : add a static SDK main entry withSentry.init(options), .captureEvent(...), .captureException(...) * feat: add Scope class * add a OptionsConfiguration typedef Co-authored-by: Manoel Aranda Neto --- CHANGELOG.md | 2 + dart/example/event_example.dart | 73 +++++++++++++ dart/example/main.dart | 101 +++--------------- dart/lib/sentry.dart | 4 + dart/lib/src/scope.dart | 139 ++++++++++++++++++++++++ dart/lib/src/sentry.dart | 54 ++++++++++ dart/lib/src/sentry_options.dart | 56 ++++++++++ dart/pubspec.yaml | 2 +- dart/test/scope_test.dart | 175 +++++++++++++++++++++++++++++++ dart/test/sentry_test.dart | 109 +++++++++++++++++++ 10 files changed, 629 insertions(+), 86 deletions(-) create mode 100644 dart/example/event_example.dart create mode 100644 dart/lib/src/scope.dart create mode 100644 dart/lib/src/sentry.dart create mode 100644 dart/lib/src/sentry_options.dart create mode 100644 dart/test/scope_test.dart create mode 100644 dart/test/sentry_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b313feb811..c5a51ed029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,9 @@ - new Dart code file structure #96 - 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 +- new static API : Sentry.init(), Sentry.captureEvent() #108 - expect a sdkName based on the test platform #105 +- Added Scope and Breadcrumb ring buffer #109 # `package:sentry` changelog diff --git a/dart/example/event_example.dart b/dart/example/event_example.dart new file mode 100644 index 0000000000..b1115cf7ef --- /dev/null +++ b/dart/example/event_example.dart @@ -0,0 +1,73 @@ +import 'package:sentry/src/protocol.dart'; + +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/example/main.dart b/dart/example/main.dart index 14ee2dbb3d..ff859af407 100644 --- a/dart/example/main.dart +++ b/dart/example/main.dart @@ -7,6 +7,8 @@ import 'dart:io'; import 'package:sentry/sentry.dart'; +import 'event_example.dart'; + /// Sends a test exception report to Sentry.io using this Dart client. Future main(List rawArgs) async { if (rawArgs.length != 1) { @@ -16,18 +18,26 @@ Future main(List rawArgs) async { } final dsn = rawArgs.single; - final client = SentryClient(dsn: dsn); + Sentry.init((options) => options.dsn = dsn); + + print('\nReporting a complete event example: '); // Sends a full Sentry event payload to show the different parts of the UI. - await captureCompleteExampleEvent(client); + final response = await Sentry.captureEvent(event); + + if (response.isSuccessful) { + print('SUCCESS\nid: ${response.eventId}'); + } else { + print('FAILURE: ${response.error}'); + } try { await foo(); } catch (error, stackTrace) { print('\nReporting the following stack trace: '); print(stackTrace); - final response = await client.captureException( - exception: error, + final response = await Sentry.captureException( + error, stackTrace: stackTrace, ); @@ -37,89 +47,10 @@ Future main(List rawArgs) async { print('FAILURE: ${response.error}'); } } finally { - await client.close(); + await Sentry.close(); } -} - -Future captureCompleteExampleEvent(SentryClient client) async { - 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', - ))); - - final response = await client.captureEvent(event: event); - print('\nReporting a complete event example: '); - if (response.isSuccessful) { - print('SUCCESS\nid: ${response.eventId}'); - } else { - print('FAILURE: ${response.error}'); - } + /* TODO(rxlabz) Sentry CaptureMessage(message, level) */ } Future foo() async { diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index dcaa278d42..471852847b 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -5,4 +5,8 @@ /// A pure Dart client for Sentry.io crash reporting. export 'src/client.dart'; export 'src/protocol.dart'; +export 'src/sentry.dart'; +export 'src/sentry_options.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.dart b/dart/lib/src/sentry.dart new file mode 100644 index 0000000000..382026acd5 --- /dev/null +++ b/dart/lib/src/sentry.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import 'client.dart'; +import 'protocol.dart'; +import 'sentry_options.dart'; + +/// Configuration options callback +typedef OptionsConfiguration = void Function(SentryOptions); + +/// Sentry SDK main entry point +/// +class Sentry { + static SentryClient _client; + + 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, + ); + } + + /// Reports an [event] to Sentry.io. + static Future captureEvent(Event event) async { + return _client.captureEvent(event: event); + } + + /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. + static Future captureException( + dynamic error, { + dynamic stackTrace, + }) async { + return _client.captureException( + exception: error, + stackTrace: stackTrace, + ); + } + + /// Close the client SDK + static Future close() async => _client.close(); + + /// client injector only use for testing + @visibleForTesting + static void initClient(SentryClient client) => _client = client; +} diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart new file mode 100644 index 0000000000..dbcfa7ca6a --- /dev/null +++ b/dart/lib/src/sentry_options.dart @@ -0,0 +1,56 @@ +import 'package:http/http.dart'; + +import 'protocol.dart'; +import 'utils.dart'; + +/// Sentry SDK options +class SentryOptions { + /// Default Log level if not specified Default is DEBUG + static final SeverityLevel defaultDiagnosticLevel = SeverityLevel.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. + String dsn; + + /// Contains [Event] attributes that are automatically mixed into all events + /// captured through this client. + /// + /// This event is designed to contain static values that do not change from + /// event to event, such as local operating system version, the version of + /// Dart/Flutter SDK, etc. These attributes have lower precedence than those + /// supplied in the even passed to [capture]. + Event environmentAttributes; + + /// 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; + + /// If [httpClient] is provided, it is used instead of the default client to + /// make HTTP calls to Sentry.io. This is useful in tests. + Client httpClient; + + /// If [clock] is provided, it is used to get time instead of the system + /// clock. This is useful in tests. Should be an implementation of [ClockProvider]. + /// This parameter is dynamic to maintain backwards compatibility with + /// previous use of [Clock](https://pub.dartlang.org/documentation/quiver/latest/quiver.time/Clock-class.html) + /// from [`package:quiver`](https://pub.dartlang.org/packages/quiver). + dynamic clock; + + /// If [uuidGenerator] is provided, it is used to generate the "event_id" + /// field instead of the built-in random UUID v4 generator. This is useful in + /// tests. + UuidGenerator uuidGenerator; + + int maxBreadcrumbs; + + SentryOptions({ + this.dsn, + this.environmentAttributes, + this.compressPayload, + this.httpClient, + this.clock, + this.uuidGenerator, + this.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); + } +} diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart new file mode 100644 index 0000000000..8fa961361a --- /dev/null +++ b/dart/test/sentry_test.dart @@ -0,0 +1,109 @@ +import 'package:mockito/mockito.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.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); + + client = MockSentryClient(); + Sentry.initClient(client); + }); + + test('should capture the event', () { + Sentry.captureEvent(event); + verify(client.captureEvent(event: event)).called(1); + }); + + test('should capture the event', () { + Sentry.captureEvent(event); + verify(client.captureEvent(event: event)).called(1); + }); + + test('should capture the exception', () { + Sentry.captureException(anException); + verify(client.captureException(exception: anException)).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', + ), + ), +); From d577e35db1b63f0e10e515f4661097b9df8a210d Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Mon, 19 Oct 2020 13:19:34 +0200 Subject: [PATCH 02/34] 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(); } From fabf56fe7a7682135fcd5dc17092f6a18a420543 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Tue, 20 Oct 2020 15:48:31 +0200 Subject: [PATCH 03/34] feat: sentry options (#116) --- CHANGELOG.md | 1 + dart/example/event_example.dart | 4 +- dart/lib/src/client.dart | 2 +- dart/lib/src/diagnostic_logger.dart | 18 +++ dart/lib/src/hub.dart | 48 ++++---- dart/lib/src/noop_client.dart | 2 +- dart/lib/src/noop_hub.dart | 51 ++++++++ dart/lib/src/protocol.dart | 2 +- dart/lib/src/protocol/breadcrumb.dart | 6 +- dart/lib/src/protocol/event.dart | 4 +- dart/lib/src/protocol/level.dart | 16 --- dart/lib/src/protocol/sentry_level.dart | 17 +++ dart/lib/src/scope.dart | 6 +- dart/lib/src/sentry.dart | 9 ++ dart/lib/src/sentry_options.dart | 153 ++++++++++++++++++++++-- dart/test/event_test.dart | 6 +- dart/test/hub_test.dart | 8 +- dart/test/mocks.dart | 4 +- dart/test/scope_test.dart | 8 +- 19 files changed, 288 insertions(+), 77 deletions(-) create mode 100644 dart/lib/src/diagnostic_logger.dart create mode 100644 dart/lib/src/noop_hub.dart delete mode 100644 dart/lib/src/protocol/level.dart create mode 100644 dart/lib/src/protocol/sentry_level.dart 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'); From 2b36bec62601f1f013bcd6900516fbca9da4e2a7 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Tue, 20 Oct 2020 16:18:32 +0200 Subject: [PATCH 04/34] ref: Hub passes the Scope (#114) --- CHANGELOG.md | 1 + dart/lib/src/client.dart | 8 +++-- dart/lib/src/hub.dart | 38 +++++++++++----------- dart/lib/src/noop_client.dart | 12 ++++++- dart/lib/src/protocol/sentry_id.dart | 6 ++-- dart/lib/src/sentry.dart | 47 +++++++++++++++++++++++++--- dart/test/hub_test.dart | 9 ++++-- dart/test/sentry_test.dart | 6 +++- dart/test/stack_trace_test.dart | 1 + dart/test/test_utils.dart | 13 ++++---- 10 files changed, 99 insertions(+), 42 deletions(-) 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(); From af3ecc069f3da17344ce002aca7bbf17048e61c9 Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Tue, 20 Oct 2020 19:07:50 +0200 Subject: [PATCH 05/34] Refacto : initialize SentryClient with SentryOptions (#118) --- CHANGELOG.md | 1 + dart/lib/src/browser_client.dart | 60 ++++---------------- dart/lib/src/client.dart | 75 +++++++------------------ dart/lib/src/client_stub.dart | 15 +---- dart/lib/src/hub.dart | 13 +---- dart/lib/src/io_client.dart | 72 ++---------------------- dart/lib/src/noop_client.dart | 18 +++--- dart/test/sentry_browser_test.dart | 2 +- dart/test/sentry_io_test.dart | 2 +- dart/test/test_utils.dart | 88 ++++++++++++++++-------------- flutter/example/lib/main.dart | 7 ++- 11 files changed, 105 insertions(+), 248 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6493302ba7..9763bc1a82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - BREAKING CHANGE: `package:http` min version bumped to 0.12.0 #104 - BREAKING CHANGE: replace the `package:usage` by `package:uuid` #94 - BREAKING CHANGE: `Event.message` must now be an instance of `Message` +- BREAKING CHANGE: SentryClient must now be initialized with a SentryOptions #118 - By default no logger it set #63 - Added missing Contexts to Event.copyWith() #62 - remove the `package:args` dependency #94 diff --git a/dart/lib/src/browser_client.dart b/dart/lib/src/browser_client.dart index 5ca60a0323..918b8f9d35 100644 --- a/dart/lib/src/browser_client.dart +++ b/dart/lib/src/browser_client.dart @@ -7,29 +7,15 @@ import 'dart:convert'; import 'dart:html' show window; import 'package:http/browser_client.dart'; -import 'package:http/http.dart'; -import 'package:meta/meta.dart'; import 'client.dart'; import 'protocol.dart'; +import 'sentry_options.dart'; import 'utils.dart'; import 'version.dart'; -SentryClient createSentryClient({ - @required String dsn, - Event environmentAttributes, - bool compressPayload, - Client httpClient, - dynamic clock, - UuidGenerator uuidGenerator, -}) => - SentryBrowserClient( - dsn: dsn, - environmentAttributes: environmentAttributes, - httpClient: httpClient, - clock: clock, - uuidGenerator: uuidGenerator, - ); +SentryClient createSentryClient(SentryOptions options) => + SentryBrowserClient(options); /// Logs crash reports and events to the Sentry.io service. class SentryBrowserClient extends SentryClient { @@ -53,49 +39,27 @@ class SentryBrowserClient extends SentryClient { /// If [uuidGenerator] is provided, it is used to generate the "event_id" /// field instead of the built-in random UUID v4 generator. This is useful in /// tests. - factory SentryBrowserClient({ - @required String dsn, - Event environmentAttributes, - Client httpClient, - dynamic clock, - UuidGenerator uuidGenerator, - String origin, - }) { - httpClient ??= BrowserClient(); - clock ??= getUtcDateTime; - uuidGenerator ??= generateUuidV4WithoutDashes; + factory SentryBrowserClient(SentryOptions options, {String origin}) { + options.httpClient ??= BrowserClient(); + options.clock ??= getUtcDateTime; + options.uuidGenerator ??= generateUuidV4WithoutDashes; // origin is necessary for sentry to resolve stacktrace origin ??= '${window.location.origin}/'; return SentryBrowserClient._( - httpClient: httpClient, - clock: clock, - uuidGenerator: uuidGenerator, - environmentAttributes: environmentAttributes, - dsn: dsn, + options, origin: origin, platform: browserPlatform, ); } - SentryBrowserClient._({ - Client httpClient, - dynamic clock, - UuidGenerator uuidGenerator, - Event environmentAttributes, - String dsn, - String platform, - String origin, - }) : super.base( - httpClient: httpClient, - clock: clock, - uuidGenerator: uuidGenerator, - environmentAttributes: environmentAttributes, - dsn: dsn, - platform: platform, + SentryBrowserClient._(SentryOptions options, {String origin, String platform}) + : super.base( + options, origin: origin, sdk: Sdk(name: browserSdkName, version: sdkVersion), + platform: platform, ); @override diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index 6f19a952ed..93061c6065 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; -import 'package:http/http.dart'; import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; @@ -22,60 +21,30 @@ abstract class SentryClient { /// /// Creates an `SentryIOClient` if `dart:io` is available and a `SentryBrowserClient` if /// `dart:html` is available, otherwise it will throw an unsupported error. - factory SentryClient({ - @required String dsn, - Event environmentAttributes, - bool compressPayload, - Client httpClient, - dynamic clock, - UuidGenerator uuidGenerator, - }) => - createSentryClient( - dsn: dsn, - environmentAttributes: environmentAttributes, - httpClient: httpClient, - clock: clock, - uuidGenerator: uuidGenerator, - compressPayload: compressPayload, - ); - - SentryClient.base({ - this.httpClient, - dynamic clock, - UuidGenerator uuidGenerator, - String dsn, - this.environmentAttributes, + factory SentryClient(SentryOptions options) => createSentryClient(options); + + SentryClient.base( + this.options, { String platform, this.origin, Sdk sdk, - }) : _dsn = Dsn.parse(dsn), - _uuidGenerator = uuidGenerator ?? generateUuidV4WithoutDashes, + }) : _dsn = Dsn.parse(options.dsn), _platform = platform ?? sdkPlatform, sdk = sdk ?? Sdk(name: sdkName, version: sdkVersion) { - if (clock == null) { - _clock = getUtcDateTime; + if (options.clock == null) { + options.clock = getUtcDateTime; } else { - _clock = (clock is ClockProvider ? clock : clock.get) as ClockProvider; + options.clock = (options.clock is ClockProvider + ? options.clock + : options.clock.get) as ClockProvider; } } - @protected - final Client httpClient; - - ClockProvider _clock; - final UuidGenerator _uuidGenerator; - - /// Contains [Event] attributes that are automatically mixed into all events - /// captured through this client. - /// - /// This event is designed to contain static values that do not change from - /// event to event, such as local operating system version, the version of - /// Dart/Flutter SDK, etc. These attributes have lower precedence than those - /// supplied in the even passed to [capture]. - final Event environmentAttributes; - final Dsn _dsn; + @protected + SentryOptions options; + /// The DSN URI. @visibleForTesting Uri get dsnUri => _dsn.uri; @@ -108,7 +77,7 @@ abstract class SentryClient { User userContext; /// Use for browser stacktrace - final String origin; + String origin; /// Used by sentry to differentiate browser from io environment final String _platform; @@ -142,7 +111,7 @@ abstract class SentryClient { StackFrameFilter stackFrameFilter, Scope scope, }) async { - final now = _clock(); + final now = options.clock(); var authHeader = 'Sentry sentry_version=6, sentry_client=$clientId, ' 'sentry_timestamp=${now.millisecondsSinceEpoch}, sentry_key=$publicKey'; if (secretKey != null) { @@ -153,12 +122,12 @@ abstract class SentryClient { final data = { 'project': projectId, - 'event_id': _uuidGenerator(), + 'event_id': options.uuidGenerator(), 'timestamp': formatDateAsIso8601WithSecondPrecision(now), }; - if (environmentAttributes != null) { - mergeAttributes(environmentAttributes.toJson(), into: data); + if (options.environmentAttributes != null) { + mergeAttributes(options.environmentAttributes.toJson(), into: data); } // Merge the user context. @@ -178,17 +147,13 @@ abstract class SentryClient { final body = bodyEncoder(data, headers); - final response = await httpClient.post( + final response = await options.httpClient.post( postUri, headers: headers, body: body, ); if (response.statusCode != 200) { - /*var errorMessage = 'Sentry.io responded with HTTP ${response.statusCode}'; - if (response.headers['x-sentry-error'] != null) { - errorMessage += ': ${response.headers['x-sentry-error']}'; - }*/ return SentryId.empty(); } @@ -222,7 +187,7 @@ abstract class SentryClient { } Future close() async { - httpClient.close(); + options.httpClient?.close(); } @override diff --git a/dart/lib/src/client_stub.dart b/dart/lib/src/client_stub.dart index 882268db74..67fdfbda69 100644 --- a/dart/lib/src/client_stub.dart +++ b/dart/lib/src/client_stub.dart @@ -2,21 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:http/http.dart'; -import 'package:meta/meta.dart'; - import 'client.dart'; -import 'protocol.dart'; -import 'utils.dart'; +import 'sentry_options.dart'; /// Implemented in `browser_client.dart` and `io_client.dart`. -SentryClient createSentryClient({ - @required String dsn, - Event environmentAttributes, - bool compressPayload, - Client httpClient, - dynamic clock, - UuidGenerator uuidGenerator, -}) => +SentryClient createSentryClient(SentryOptions options) => throw UnsupportedError( 'Cannot create a client without dart:html or dart:io.'); diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 30df85fc68..feac240ae2 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -11,15 +11,8 @@ 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, - ); + static SentryClient _getClient(SentryOptions options) { + return SentryClient(options); } final ListQueue<_StackItem> _stack = ListQueue(); @@ -36,7 +29,7 @@ class Hub { } Hub._(SentryOptions options) : _options = options { - _stack.add(_StackItem(_getClient(fromOptions: options), Scope(_options))); + _stack.add(_StackItem(_getClient(_options), Scope(_options))); _isEnabled = true; } diff --git a/dart/lib/src/io_client.dart b/dart/lib/src/io_client.dart index f45ecc57d2..b46f54839e 100644 --- a/dart/lib/src/io_client.dart +++ b/dart/lib/src/io_client.dart @@ -6,30 +6,13 @@ import 'dart:convert'; import 'dart:io'; -import 'package:http/http.dart'; -import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; import 'client.dart'; import 'protocol.dart'; -import 'utils.dart'; -import 'version.dart'; -SentryClient createSentryClient({ - @required String dsn, - Event environmentAttributes, - bool compressPayload, - Client httpClient, - dynamic clock, - UuidGenerator uuidGenerator, -}) => - SentryIOClient( - dsn: dsn, - environmentAttributes: environmentAttributes, - compressPayload: compressPayload, - httpClient: httpClient, - clock: clock, - uuidGenerator: uuidGenerator, - ); +SentryClient createSentryClient(SentryOptions options) => + SentryIOClient(options); /// Logs crash reports and events to the Sentry.io service. class SentryIOClient extends SentryClient { @@ -57,52 +40,9 @@ class SentryIOClient extends SentryClient { /// If [uuidGenerator] is provided, it is used to generate the "event_id" /// field instead of the built-in random UUID v4 generator. This is useful in /// tests. - factory SentryIOClient({ - @required String dsn, - Event environmentAttributes, - bool compressPayload, - Client httpClient, - dynamic clock, - UuidGenerator uuidGenerator, - }) { - httpClient ??= Client(); - clock ??= getUtcDateTime; - uuidGenerator ??= generateUuidV4WithoutDashes; - compressPayload ??= true; + factory SentryIOClient(SentryOptions options) => SentryIOClient._(options); - return SentryIOClient._( - httpClient: httpClient, - clock: clock, - uuidGenerator: uuidGenerator, - environmentAttributes: environmentAttributes, - dsn: dsn, - compressPayload: compressPayload, - platform: sdkPlatform, - ); - } - - SentryIOClient._({ - Client httpClient, - dynamic clock, - UuidGenerator uuidGenerator, - Event environmentAttributes, - String dsn, - this.compressPayload = true, - String platform, - String origin, - }) : super.base( - httpClient: httpClient, - clock: clock, - uuidGenerator: uuidGenerator, - environmentAttributes: environmentAttributes, - dsn: dsn, - platform: platform, - origin: origin, - sdk: Sdk(name: sdkName, version: sdkVersion), - ); - - /// Whether to compress payloads sent to Sentry.io. - final bool compressPayload; + SentryIOClient._(SentryOptions options) : super.base(options); @override Map buildHeaders(String authHeader) { @@ -123,7 +63,7 @@ class SentryIOClient extends SentryClient { // [SentryIOClient] implement gzip compression // gzip compression is not available on browser var body = utf8.encode(json.encode(data)); - if (compressPayload) { + if (options.compressPayload) { headers['Content-Encoding'] = 'gzip'; body = gzip.encode(body); } diff --git a/dart/lib/src/noop_client.dart b/dart/lib/src/noop_client.dart index 3f7f4c9119..8f7f3a4fcc 100644 --- a/dart/lib/src/noop_client.dart +++ b/dart/lib/src/noop_client.dart @@ -1,10 +1,9 @@ import 'dart:async'; -import 'package:http/src/client.dart'; - import 'client.dart'; import 'protocol.dart'; import 'scope.dart'; +import 'sentry_options.dart'; class NoOpSentryClient implements SentryClient { NoOpSentryClient._(); @@ -18,6 +17,12 @@ class NoOpSentryClient implements SentryClient { @override User userContext; + @override + SentryOptions options; + + @override + String origin; + @override List bodyEncoder( Map data, @@ -57,15 +62,6 @@ class NoOpSentryClient implements SentryClient { @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; diff --git a/dart/test/sentry_browser_test.dart b/dart/test/sentry_browser_test.dart index 0531f7ec6b..e4c4711ce4 100644 --- a/dart/test/sentry_browser_test.dart +++ b/dart/test/sentry_browser_test.dart @@ -11,7 +11,7 @@ import 'test_utils.dart'; void main() { group('SentryBrowserClient', () { test('SentryClient constructor build browser client', () { - final client = SentryClient(dsn: testDsn); + final client = SentryClient(SentryOptions(dsn: testDsn)); expect(client is SentryBrowserClient, isTrue); }); diff --git a/dart/test/sentry_io_test.dart b/dart/test/sentry_io_test.dart index cc19693602..10a5b70e00 100644 --- a/dart/test/sentry_io_test.dart +++ b/dart/test/sentry_io_test.dart @@ -13,7 +13,7 @@ import 'test_utils.dart'; void main() { group(SentryIOClient, () { test('SentryClient constructor build io client', () { - final client = SentryClient(dsn: testDsn); + final client = SentryClient(SentryOptions(dsn: testDsn)); expect(client is SentryIOClient, isTrue); }); diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index 5a34b86ee5..13107a7ded 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -70,15 +70,17 @@ Future testCaptureException( }); final client = SentryClient( - dsn: testDsn, - httpClient: httpMock, - clock: fakeClockProvider, - uuidGenerator: () => 'X' * 32, - compressPayload: compressPayload, - environmentAttributes: const Event( - serverName: 'test.server.com', - release: '1.2.3', - environment: 'staging', + SentryOptions( + dsn: testDsn, + httpClient: httpMock, + clock: fakeClockProvider, + uuidGenerator: () => 'X' * 32, + compressPayload: compressPayload, + environmentAttributes: const Event( + serverName: 'test.server.com', + release: '1.2.3', + environment: 'staging', + ), ), ); @@ -176,7 +178,7 @@ Future testCaptureException( void runTest({Codec, List> gzip, bool isWeb = false}) { test('can parse DSN', () async { - final client = SentryClient(dsn: testDsn); + final client = SentryClient(SentryOptions(dsn: testDsn)); expect(client.dsnUri, Uri.parse(testDsn)); expect(client.postUri, 'https://sentry.example.com/api/1/store/'); expect(client.publicKey, 'public'); @@ -186,7 +188,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { }); test('can parse DSN without secret', () async { - final client = SentryClient(dsn: _testDsnWithoutSecret); + final client = SentryClient(SentryOptions(dsn: _testDsnWithoutSecret)); expect(client.dsnUri, Uri.parse(_testDsnWithoutSecret)); expect(client.postUri, 'https://sentry.example.com/api/1/store/'); expect(client.publicKey, 'public'); @@ -196,7 +198,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { }); test('can parse DSN with path', () async { - final client = SentryClient(dsn: _testDsnWithPath); + final client = SentryClient(SentryOptions(dsn: _testDsnWithPath)); expect(client.dsnUri, Uri.parse(_testDsnWithPath)); expect(client.postUri, 'https://sentry.example.com/path/api/1/store/'); expect(client.publicKey, 'public'); @@ -205,7 +207,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { await client.close(); }); test('can parse DSN with port', () async { - final client = SentryClient(dsn: _testDsnWithPort); + final client = SentryClient(SentryOptions(dsn: _testDsnWithPort)); expect(client.dsnUri, Uri.parse(_testDsnWithPort)); expect(client.postUri, 'https://sentry.example.com:8888/api/1/store/'); expect(client.publicKey, 'public'); @@ -228,15 +230,17 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { }); final client = SentryClient( - dsn: _testDsnWithoutSecret, - httpClient: httpMock, - clock: fakeClockProvider, - compressPayload: false, - uuidGenerator: () => 'X' * 32, - environmentAttributes: const Event( - serverName: 'test.server.com', - release: '1.2.3', - environment: 'staging', + SentryOptions( + dsn: _testDsnWithoutSecret, + httpClient: httpMock, + clock: fakeClockProvider, + compressPayload: false, + uuidGenerator: () => 'X' * 32, + environmentAttributes: const Event( + serverName: 'test.server.com', + release: '1.2.3', + environment: 'staging', + ), ), ); @@ -284,15 +288,17 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { }); final client = SentryClient( - dsn: testDsn, - httpClient: httpMock, - clock: fakeClockProvider, - uuidGenerator: () => 'X' * 32, - compressPayload: false, - environmentAttributes: const Event( - serverName: 'test.server.com', - release: '1.2.3', - environment: 'staging', + SentryOptions( + dsn: testDsn, + httpClient: httpMock, + clock: fakeClockProvider, + uuidGenerator: () => 'X' * 32, + compressPayload: false, + environmentAttributes: const Event( + serverName: 'test.server.com', + release: '1.2.3', + environment: 'staging', + ), ), ); @@ -338,15 +344,17 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { extras: {'foo': 'bar'}); final client = SentryClient( - dsn: testDsn, - httpClient: httpMock, - clock: fakeClockProvider, - uuidGenerator: () => 'X' * 32, - compressPayload: false, - environmentAttributes: const Event( - serverName: 'test.server.com', - release: '1.2.3', - environment: 'staging', + SentryOptions( + dsn: testDsn, + httpClient: httpMock, + clock: fakeClockProvider, + uuidGenerator: () => 'X' * 32, + compressPayload: false, + environmentAttributes: const Event( + serverName: 'test.server.com', + release: '1.2.3', + environment: 'staging', + ), ), ); client.userContext = clientUserContext; diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 4154ccccfe..94b288ce09 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -11,10 +11,11 @@ import 'package:universal_platform/universal_platform.dart'; const String _release = String.fromEnvironment('SENTRY_RELEASE', defaultValue: 'unknown'); +const String exampleDsn = + 'https://cb0fad6f5d4e42ebb9c956cb0463edc9@o447951.ingest.sentry.io/5428562'; + // NOTE: Add your DSN below to get the events in your Sentry project. -final SentryClient _sentry = SentryClient( - dsn: - 'https://cb0fad6f5d4e42ebb9c956cb0463edc9@o447951.ingest.sentry.io/5428562'); +final SentryClient _sentry = SentryClient(SentryOptions(dsn: exampleDsn)); // Proposed init: // https://github.com/bruno-garcia/badges.bar/blob/2450ed9125f7b73d2baad1fa6d676cc71858116c/lib/src/sentry.dart#L9-L32 From 19f55924bfaf25eb2841db0529ebef9536c3babd Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Wed, 21 Oct 2020 09:33:14 +0200 Subject: [PATCH 06/34] ref: SentryId generates UUID (#119) --- CHANGELOG.md | 3 ++- dart/lib/src/browser_client.dart | 5 ---- dart/lib/src/client.dart | 4 +-- dart/lib/src/hub.dart | 3 +-- dart/lib/src/io_client.dart | 4 --- dart/lib/src/protocol/event.dart | 14 ++++++++-- dart/lib/src/protocol/sentry_id.dart | 22 +++++++++++----- dart/lib/src/sentry_options.dart | 7 ----- dart/lib/src/utils.dart | 5 ---- dart/test/event_test.dart | 8 +++--- dart/test/test_utils.dart | 39 ++++++++++++++++------------ 11 files changed, 60 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9763bc1a82..c1148c4570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,8 @@ - Added Scope and Breadcrumb ring buffer #109 - Added Hub to SDK #113 - Ref: Hub passes the Scope to SentryClient -- feat: sentry options #116 +- Feat: sentry options #116 +- Ref: SentryId generates UUID # `package:sentry` changelog diff --git a/dart/lib/src/browser_client.dart b/dart/lib/src/browser_client.dart index 918b8f9d35..25ccfd8374 100644 --- a/dart/lib/src/browser_client.dart +++ b/dart/lib/src/browser_client.dart @@ -35,14 +35,9 @@ class SentryBrowserClient extends SentryClient { /// This parameter is dynamic to maintain backwards compatibility with /// previous use of [Clock](https://pub.dartlang.org/documentation/quiver/latest/quiver.time/Clock-class.html) /// from [`package:quiver`](https://pub.dartlang.org/packages/quiver). - /// - /// If [uuidGenerator] is provided, it is used to generate the "event_id" - /// field instead of the built-in random UUID v4 generator. This is useful in - /// tests. factory SentryBrowserClient(SentryOptions options, {String origin}) { options.httpClient ??= BrowserClient(); options.clock ??= getUtcDateTime; - options.uuidGenerator ??= generateUuidV4WithoutDashes; // origin is necessary for sentry to resolve stacktrace origin ??= '${window.location.origin}/'; diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index 93061c6065..32ad24678e 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -122,7 +122,7 @@ abstract class SentryClient { final data = { 'project': projectId, - 'event_id': options.uuidGenerator(), + 'event_id': event.eventId.toString(), 'timestamp': formatDateAsIso8601WithSecondPrecision(now), }; @@ -158,7 +158,7 @@ abstract class SentryClient { } final eventId = json.decode(response.body)['id']; - return eventId != null ? SentryId(eventId) : SentryId.empty(); + return eventId != null ? SentryId.fromId(eventId) : SentryId.empty(); } /// Reports the [throwable] and optionally its [stackTrace] to Sentry.io. diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index feac240ae2..e71190e2c5 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -73,10 +73,9 @@ class Hub { try { sentryId = await item.client.captureEvent(event, scope: item.scope); } catch (err) { - /* TODO add Event.id */ _options.logger( SentryLevel.error, - 'Error while capturing event with id: ${event}', + 'Error while capturing event with id: ${event.eventId.toString()}', ); } finally { _lastEventId = sentryId; diff --git a/dart/lib/src/io_client.dart b/dart/lib/src/io_client.dart index b46f54839e..0e342dd685 100644 --- a/dart/lib/src/io_client.dart +++ b/dart/lib/src/io_client.dart @@ -36,10 +36,6 @@ class SentryIOClient extends SentryClient { /// This parameter is dynamic to maintain backwards compatibility with /// previous use of [Clock](https://pub.dartlang.org/documentation/quiver/latest/quiver.time/Clock-class.html) /// from [`package:quiver`](https://pub.dartlang.org/packages/quiver). - /// - /// If [uuidGenerator] is provided, it is used to generate the "event_id" - /// field instead of the built-in random UUID v4 generator. This is useful in - /// tests. factory SentryIOClient(SentryOptions options) => SentryIOClient._(options); SentryIOClient._(SentryOptions options) : super.base(options); diff --git a/dart/lib/src/protocol/event.dart b/dart/lib/src/protocol/event.dart index a1720391b8..ee4b3403d7 100644 --- a/dart/lib/src/protocol/event.dart +++ b/dart/lib/src/protocol/event.dart @@ -8,7 +8,8 @@ import '../version.dart'; @immutable class Event { /// Creates an event. - const Event({ + Event({ + SentryId eventId, this.loggerName, this.serverName, this.release, @@ -26,7 +27,7 @@ class Event { this.contexts, this.breadcrumbs, this.sdk, - }); + }) : eventId = eventId ?? SentryId.newId(); /// Refers to the default fingerprinting algorithm. /// @@ -34,6 +35,9 @@ class Event { /// fingerprint with custom fingerprints. static const String defaultFingerprint = '{{ default }}'; + /// The ID Sentry.io assigned to the submitted event for future reference. + final SentryId eventId; + /// The logger that logged the event. final String loggerName; @@ -117,6 +121,7 @@ class Event { final Sdk sdk; Event copyWith({ + SentryId eventId, String loggerName, String serverName, String release, @@ -136,6 +141,7 @@ class Event { Sdk sdk, }) => Event( + eventId: eventId ?? this.eventId, loggerName: loggerName ?? this.loggerName, serverName: serverName ?? this.serverName, release: release ?? this.release, @@ -162,6 +168,10 @@ class Event { 'platform': sdkPlatform, }; + if (eventId != null) { + json['event_id'] = eventId.toString(); + } + if (loggerName != null) { json['logger'] = loggerName; } diff --git a/dart/lib/src/protocol/sentry_id.dart b/dart/lib/src/protocol/sentry_id.dart index e1e1abe75a..4be270ca50 100644 --- a/dart/lib/src/protocol/sentry_id.dart +++ b/dart/lib/src/protocol/sentry_id.dart @@ -1,16 +1,26 @@ -/// Sentry response id +import 'package:uuid/uuid.dart'; class SentryId { - static const String _emptyId = '00000000-0000-0000-0000-000000000000'; + static final SentryId _emptyId = + SentryId.fromId('00000000-0000-0000-0000-000000000000'); /// The ID Sentry.io assigned to the submitted event for future reference. - final String _id; + String _id; - // TODO: should we generate the new UUID here with an empty ctor? + final Uuid _uuidGenerator = Uuid(); - const SentryId(this._id); + SentryId._internal({String id}) { + _id = id ?? _uuidGenerator.v4(); + } - factory SentryId.empty() => SentryId(_emptyId); + /// Generates a new SentryId + factory SentryId.newId() => SentryId._internal(); + + /// Generates a SentryId with the given UUID + factory SentryId.fromId(String id) => SentryId._internal(id: id); + + /// SentryId with an empty UUID + factory SentryId.empty() => _emptyId; @override String toString() => _id.replaceAll('-', ''); diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index e02345c42b..154145b6a5 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -3,7 +3,6 @@ import 'package:sentry/sentry.dart'; import 'diagnostic_logger.dart'; import 'hub.dart'; import 'protocol.dart'; -import 'utils.dart'; /// Sentry SDK options class SentryOptions { @@ -39,11 +38,6 @@ class SentryOptions { /// from [`package:quiver`](https://pub.dartlang.org/packages/quiver). dynamic clock; - /// If [uuidGenerator] is provided, it is used to generate the "event_id" - /// field instead of the built-in random UUID v4 generator. This is useful in - /// tests. - UuidGenerator uuidGenerator; - int maxBreadcrumbs; /// Logger interface to log useful debugging information if debug is enabled @@ -145,7 +139,6 @@ class SentryOptions { this.compressPayload, this.httpClient, this.clock, - this.uuidGenerator, }); /// Adds an event processor diff --git a/dart/lib/src/utils.dart b/dart/lib/src/utils.dart index 224256e959..d8ce547bbe 100644 --- a/dart/lib/src/utils.dart +++ b/dart/lib/src/utils.dart @@ -3,11 +3,6 @@ // found in the LICENSE file. import 'package:meta/meta.dart'; -import 'package:uuid/uuid.dart'; - -typedef UuidGenerator = String Function(); - -String generateUuidV4WithoutDashes() => Uuid().v4().replaceAll('-', ''); /// Sentry does not take a timezone and instead expects the date-time to be /// submitted in UTC timezone. diff --git a/dart/test/event_test.dart b/dart/test/event_test.dart index 6a249c91ed..a4a7447a57 100644 --- a/dart/test/event_test.dart +++ b/dart/test/event_test.dart @@ -25,7 +25,8 @@ void main() { ); }); test('$Sdk serializes', () { - const event = Event( + final event = Event( + eventId: SentryId.empty(), sdk: Sdk( name: 'sentry.dart.flutter', version: '4.3.2', @@ -33,6 +34,7 @@ void main() { packages: [Package('npm:@sentry/javascript', '1.3.4')])); expect(event.toJson(), { 'platform': 'dart', + 'event_id': '00000000000000000000000000000000', 'sdk': { 'name': 'sentry.dart.flutter', 'version': '4.3.2', @@ -58,10 +60,9 @@ void main() { final error = StateError('test-error'); - print('error.stackTrace ${error.stackTrace}'); - expect( Event( + eventId: SentryId.empty(), message: Message( 'test-message 1 2', template: 'test-message %d %d', @@ -85,6 +86,7 @@ void main() { ).toJson(), { 'platform': 'dart', + 'event_id': '00000000000000000000000000000000', 'sdk': {'version': sdkVersion, 'name': 'sentry.dart'}, 'message': { 'formatted': 'test-message 1 2', diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index 13107a7ded..50182c6e4b 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -69,14 +69,14 @@ Future testCaptureException( fail('Unexpected request on ${request.method} ${request.url} in HttpMock'); }); + var sentryId = SentryId.empty(); final client = SentryClient( SentryOptions( dsn: testDsn, httpClient: httpMock, clock: fakeClockProvider, - uuidGenerator: () => 'X' * 32, compressPayload: compressPayload, - environmentAttributes: const Event( + environmentAttributes: Event( serverName: 'test.server.com', release: '1.2.3', environment: 'staging', @@ -87,8 +87,7 @@ Future testCaptureException( try { throw ArgumentError('Test error'); } catch (error, stackTrace) { - final sentryId = - await client.captureException(error, stackTrace: stackTrace); + sentryId = await client.captureException(error, stackTrace: stackTrace); expect('$sentryId', 'testeventid'); } @@ -108,6 +107,10 @@ Future testCaptureException( } else { data = json.decode(utf8.decode(body)) as Map; } + + // so we assert the generated and returned id + data['event_id'] = sentryId.toString(); + final stacktrace = data.remove('stacktrace') as Map; expect(stacktrace['frames'], const TypeMatcher()); @@ -139,7 +142,7 @@ Future testCaptureException( expect(data, { 'project': '1', - 'event_id': 'X' * 32, + 'event_id': sentryId.toString(), 'timestamp': '2017-01-02T00:00:00', 'platform': 'javascript', 'sdk': {'version': sdkVersion, 'name': 'sentry.dart'}, @@ -157,7 +160,7 @@ Future testCaptureException( expect(data, { 'project': '1', - 'event_id': 'X' * 32, + 'event_id': sentryId.toString(), 'timestamp': '2017-01-02T00:00:00', 'platform': 'dart', 'exception': [ @@ -235,8 +238,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { httpClient: httpMock, clock: fakeClockProvider, compressPayload: false, - uuidGenerator: () => 'X' * 32, - environmentAttributes: const Event( + environmentAttributes: Event( serverName: 'test.server.com', release: '1.2.3', environment: 'staging', @@ -292,9 +294,8 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { dsn: testDsn, httpClient: httpMock, clock: fakeClockProvider, - uuidGenerator: () => 'X' * 32, compressPayload: false, - environmentAttributes: const Event( + environmentAttributes: Event( serverName: 'test.server.com', release: '1.2.3', environment: 'staging', @@ -348,9 +349,8 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { dsn: testDsn, httpClient: httpMock, clock: fakeClockProvider, - uuidGenerator: () => 'X' * 32, compressPayload: false, - environmentAttributes: const Event( + environmentAttributes: Event( serverName: 'test.server.com', release: '1.2.3', environment: 'staging', @@ -362,12 +362,17 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { try { throw ArgumentError('Test error'); } catch (error, stackTrace) { - final eventWithoutContext = - Event(exception: error, stackTrace: stackTrace); + final eventWithoutContext = Event( + eventId: SentryId.empty(), + exception: error, + stackTrace: stackTrace, + ); final eventWithContext = Event( - exception: error, - stackTrace: stackTrace, - userContext: eventUserContext); + eventId: SentryId.empty(), + exception: error, + stackTrace: stackTrace, + userContext: eventUserContext, + ); await client.captureEvent(eventWithoutContext); expect(loggedUserId, clientUserContext.id); await client.captureEvent(eventWithContext); From b51b0dab39d5c39cdec6cb2a8f5da7dc8be4acf7 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Thu, 22 Oct 2020 09:47:41 +0200 Subject: [PATCH 07/34] ref: Event now is SentryEvent and added GPU (#121) --- CHANGELOG.md | 3 +- dart/example/event_example.dart | 4 +- dart/lib/src/browser_client.dart | 8 -- dart/lib/src/client.dart | 36 ++++--- dart/lib/src/hub.dart | 2 +- dart/lib/src/io_client.dart | 7 -- dart/lib/src/noop_client.dart | 2 +- dart/lib/src/noop_hub.dart | 5 +- dart/lib/src/protocol.dart | 2 +- dart/lib/src/protocol/contexts.dart | 12 +++ dart/lib/src/protocol/gpu.dart | 95 +++++++++++++++++++ .../{event.dart => sentry_event.dart} | 75 ++++++++++++--- dart/lib/src/sentry.dart | 2 +- dart/lib/src/sentry_options.dart | 27 ++++-- dart/test/contexts_test.dart | 2 +- dart/test/event_test.dart | 15 ++- dart/test/mocks.dart | 4 +- dart/test/test_utils.dart | 14 +-- flutter/example/lib/main.dart | 2 +- 19 files changed, 234 insertions(+), 83 deletions(-) create mode 100644 dart/lib/src/protocol/gpu.dart rename dart/lib/src/protocol/{event.dart => sentry_event.dart} (79%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1148c4570..4917265a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,9 @@ - Added Scope and Breadcrumb ring buffer #109 - Added Hub to SDK #113 - Ref: Hub passes the Scope to SentryClient -- Feat: sentry options #116 +- Feature: sentry options #116 - Ref: SentryId generates UUID +- Ref: Event now is SentryEvent and added GPU # `package:sentry` changelog diff --git a/dart/example/event_example.dart b/dart/example/event_example.dart index d4a64dc64f..a02c92a9b3 100644 --- a/dart/example/event_example.dart +++ b/dart/example/event_example.dart @@ -1,7 +1,7 @@ import 'package:sentry/src/protocol.dart'; -final event = Event( - loggerName: 'main', +final event = SentryEvent( + logger: 'main', serverName: 'server.dart', release: '1.4.0-preview.1', environment: 'Test', diff --git a/dart/lib/src/browser_client.dart b/dart/lib/src/browser_client.dart index 25ccfd8374..b34c862e9d 100644 --- a/dart/lib/src/browser_client.dart +++ b/dart/lib/src/browser_client.dart @@ -11,7 +11,6 @@ import 'package:http/browser_client.dart'; import 'client.dart'; import 'protocol.dart'; import 'sentry_options.dart'; -import 'utils.dart'; import 'version.dart'; SentryClient createSentryClient(SentryOptions options) => @@ -29,15 +28,8 @@ class SentryBrowserClient extends SentryClient { /// /// If [httpClient] is provided, it is used instead of the default client to /// make HTTP calls to Sentry.io. This is useful in tests. - /// - /// If [clock] is provided, it is used to get time instead of the system - /// clock. This is useful in tests. Should be an implementation of [ClockProvider]. - /// This parameter is dynamic to maintain backwards compatibility with - /// previous use of [Clock](https://pub.dartlang.org/documentation/quiver/latest/quiver.time/Clock-class.html) - /// from [`package:quiver`](https://pub.dartlang.org/packages/quiver). factory SentryBrowserClient(SentryOptions options, {String origin}) { options.httpClient ??= BrowserClient(); - options.clock ??= getUtcDateTime; // origin is necessary for sentry to resolve stacktrace origin ??= '${window.location.origin}/'; diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index 32ad24678e..12ff28b3b1 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -12,9 +12,6 @@ import 'stack_trace.dart'; import 'utils.dart'; import 'version.dart'; -/// Used to provide timestamp for logging. -typedef ClockProvider = DateTime Function(); - /// Logs crash reports and events to the Sentry.io service. abstract class SentryClient { /// Creates a new platform appropriate client. @@ -30,15 +27,7 @@ abstract class SentryClient { Sdk sdk, }) : _dsn = Dsn.parse(options.dsn), _platform = platform ?? sdkPlatform, - sdk = sdk ?? Sdk(name: sdkName, version: sdkVersion) { - if (options.clock == null) { - options.clock = getUtcDateTime; - } else { - options.clock = (options.clock is ClockProvider - ? options.clock - : options.clock.get) as ClockProvider; - } - } + sdk = sdk ?? Sdk(name: sdkName, version: sdkVersion); final Dsn _dsn; @@ -107,7 +96,7 @@ abstract class SentryClient { /// Reports an [event] to Sentry.io. Future captureEvent( - Event event, { + SentryEvent event, { StackFrameFilter stackFrameFilter, Scope scope, }) async { @@ -123,7 +112,7 @@ abstract class SentryClient { final data = { 'project': projectId, 'event_id': event.eventId.toString(), - 'timestamp': formatDateAsIso8601WithSecondPrecision(now), + 'timestamp': formatDateAsIso8601WithSecondPrecision(event.timestamp), }; if (options.environmentAttributes != null) { @@ -162,11 +151,15 @@ abstract class SentryClient { } /// Reports the [throwable] and optionally its [stackTrace] to Sentry.io. - Future captureException(dynamic throwable, - {dynamic stackTrace, Scope scope}) { - final event = Event( + Future captureException( + dynamic throwable, { + dynamic stackTrace, + Scope scope, + }) { + final event = SentryEvent( exception: throwable, stackTrace: stackTrace, + timestamp: options.clock(), ); return captureEvent(event, scope: scope); } @@ -179,9 +172,14 @@ abstract class SentryClient { List params, Scope scope, }) { - final event = Event( - message: Message(formatted, template: template, params: params), + final event = SentryEvent( + message: Message( + formatted, + template: template, + params: params, + ), level: level, + timestamp: options.clock(), ); return captureEvent(event, scope: scope); } diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index e71190e2c5..a93b09aa71 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -54,7 +54,7 @@ class Hub { SentryId get lastEventId => _lastEventId; /// Captures the event. - Future captureEvent(Event event) async { + Future captureEvent(SentryEvent event) async { var sentryId = SentryId.empty(); if (!_isEnabled) { diff --git a/dart/lib/src/io_client.dart b/dart/lib/src/io_client.dart index 0e342dd685..a7f5d59297 100644 --- a/dart/lib/src/io_client.dart +++ b/dart/lib/src/io_client.dart @@ -9,7 +9,6 @@ import 'dart:io'; import 'package:sentry/sentry.dart'; import 'client.dart'; -import 'protocol.dart'; SentryClient createSentryClient(SentryOptions options) => SentryIOClient(options); @@ -30,12 +29,6 @@ class SentryIOClient extends SentryClient { /// /// If [httpClient] is provided, it is used instead of the default client to /// make HTTP calls to Sentry.io. This is useful in tests. - /// - /// If [clock] is provided, it is used to get time instead of the system - /// clock. This is useful in tests. Should be an implementation of [ClockProvider]. - /// This parameter is dynamic to maintain backwards compatibility with - /// previous use of [Clock](https://pub.dartlang.org/documentation/quiver/latest/quiver.time/Clock-class.html) - /// from [`package:quiver`](https://pub.dartlang.org/packages/quiver). factory SentryIOClient(SentryOptions options) => SentryIOClient._(options); SentryIOClient._(SentryOptions options) : super.base(options); diff --git a/dart/lib/src/noop_client.dart b/dart/lib/src/noop_client.dart index 8f7f3a4fcc..c5f5c7d438 100644 --- a/dart/lib/src/noop_client.dart +++ b/dart/lib/src/noop_client.dart @@ -34,7 +34,7 @@ class NoOpSentryClient implements SentryClient { Map buildHeaders(String authHeader) => {}; @override - Future captureEvent(Event event, {stackFrameFilter, scope}) => + Future captureEvent(SentryEvent event, {stackFrameFilter, scope}) => Future.value(SentryId.empty()); @override diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 2c479c4492..30dbb422fe 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'client.dart'; import 'hub.dart'; -import 'protocol/event.dart'; +import 'protocol/sentry_event.dart'; import 'protocol/sentry_level.dart'; import 'protocol/sentry_id.dart'; @@ -19,7 +19,8 @@ class NoOpHub implements Hub { void bindClient(SentryClient client) {} @override - Future captureEvent(Event event) => Future.value(SentryId.empty()); + Future captureEvent(SentryEvent event) => + Future.value(SentryId.empty()); @override Future captureException(throwable, {stackTrace}) => diff --git a/dart/lib/src/protocol.dart b/dart/lib/src/protocol.dart index 2aa0d3a3c8..bbf63dda97 100644 --- a/dart/lib/src/protocol.dart +++ b/dart/lib/src/protocol.dart @@ -4,7 +4,7 @@ export 'protocol/browser.dart'; export 'protocol/contexts.dart'; export 'protocol/device.dart'; export 'protocol/dsn.dart'; -export 'protocol/event.dart'; +export 'protocol/sentry_event.dart'; export 'protocol/sentry_level.dart'; export 'protocol/message.dart'; export 'protocol/package.dart'; diff --git a/dart/lib/src/protocol/contexts.dart b/dart/lib/src/protocol/contexts.dart index 898aef185d..2f1cdb5cdc 100644 --- a/dart/lib/src/protocol/contexts.dart +++ b/dart/lib/src/protocol/contexts.dart @@ -2,6 +2,7 @@ import '../protocol.dart'; import 'app.dart'; import 'browser.dart'; import 'device.dart'; +import 'gpu.dart'; import 'runtime.dart'; /// The context interfaces provide additional context data. @@ -17,6 +18,7 @@ class Contexts { this.runtimes, this.app, this.browser, + this.gpu, }); /// This describes the device that caused the event. @@ -48,6 +50,11 @@ class Contexts { /// agent of a web request that triggered the event. final Browser browser; + /// GPU context describes the GPU of the device. + final Gpu gpu; + + // TODO: contexts should accept arbitrary values + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; @@ -73,6 +80,11 @@ class Contexts { json['browser'] = browserMap; } + Map gpuMap; + if (gpu != null && (gpuMap = gpu.toJson()).isNotEmpty) { + json['gpu'] = gpuMap; + } + if (runtimes != null) { if (runtimes.length == 1) { final runtime = runtimes[0]; diff --git a/dart/lib/src/protocol/gpu.dart b/dart/lib/src/protocol/gpu.dart new file mode 100644 index 0000000000..c88600b753 --- /dev/null +++ b/dart/lib/src/protocol/gpu.dart @@ -0,0 +1,95 @@ +// https://develop.sentry.dev/sdk/event-payloads/contexts/#gpu-context +// Example: +// "gpu": { +// "name": "AMD Radeon Pro 560", +// "vendor_name": "Apple", +// "memory_size": 4096, +// "api_type": "Metal", +// "multi_threaded_rendering": true, +// "version": "Metal", +// "npot_support": "Full" +// } + +class Gpu { + /// The name of the graphics device. + final String name; + + /// The PCI identifier of the graphics device. + final int id; + + /// The PCI vendor identifier of the graphics device. + final int vendorId; + + /// The vendor name as reported by the graphics device. + final String vendorName; + + /// The total GPU memory available in Megabytes. + final int memorySize; + + /// The device low-level API type. + final String apiType; + + /// Whether the GPU has multi-threaded rendering or not. + final bool multiThreadedRendering; + + /// The Version of the graphics device. + final String version; + + /// The Non-Power-Of-Two-Support support. + final String npotSupport; + + const Gpu({ + this.name, + this.id, + this.vendorId, + this.vendorName, + this.memorySize, + this.apiType, + this.multiThreadedRendering, + this.version, + this.npotSupport, + }); + + /// Produces a [Map] that can be serialized to JSON. + Map toJson() { + final json = {}; + + if (name != null) { + json['name'] = name; + } + + if (id != null) { + json['id'] = id; + } + + if (vendorId != null) { + json['vendor_id'] = vendorId; + } + + if (vendorName != null) { + json['vendor_name'] = vendorName; + } + + if (memorySize != null) { + json['memory_size'] = memorySize; + } + + if (apiType != null) { + json['api_type'] = apiType; + } + + if (multiThreadedRendering != null) { + json['multi_threaded_rendering'] = multiThreadedRendering; + } + + if (version != null) { + json['version'] = version; + } + + if (npotSupport != null) { + json['npot_support'] = npotSupport; + } + + return json; + } +} diff --git a/dart/lib/src/protocol/event.dart b/dart/lib/src/protocol/sentry_event.dart similarity index 79% rename from dart/lib/src/protocol/event.dart rename to dart/lib/src/protocol/sentry_event.dart index ee4b3403d7..142ce218d2 100644 --- a/dart/lib/src/protocol/event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -2,18 +2,23 @@ import 'package:meta/meta.dart'; import '../protocol.dart'; import '../stack_trace.dart'; +import '../utils.dart'; import '../version.dart'; /// An event to be reported to Sentry.io. @immutable -class Event { +class SentryEvent { /// Creates an event. - Event({ + SentryEvent({ SentryId eventId, - this.loggerName, + DateTime timestamp, + String platform, + this.logger, this.serverName, this.release, + this.dist, this.environment, + this.modules, this.message, this.transaction, this.exception, @@ -27,7 +32,9 @@ class Event { this.contexts, this.breadcrumbs, this.sdk, - }) : eventId = eventId ?? SentryId.newId(); + }) : eventId = eventId ?? SentryId.newId(), + platform = platform ?? sdkPlatform, + timestamp = timestamp ?? getUtcDateTime(); /// Refers to the default fingerprinting algorithm. /// @@ -38,8 +45,14 @@ class Event { /// The ID Sentry.io assigned to the submitted event for future reference. final SentryId eventId; + /// A timestamp representing when the breadcrumb occurred. + final DateTime timestamp; + + /// A string representing the platform the SDK is submitting from. This will be used by the Sentry interface to customize various components in the interface. + final String platform; + /// The logger that logged the event. - final String loggerName; + final String logger; /// Identifies the server that logged this event. final String serverName; @@ -47,9 +60,15 @@ class Event { /// The version of the application that logged the event. final String release; + /// The distribution of the application. + final String dist; + /// The environment that logged the event, e.g. "production", "staging". final String environment; + /// A list of relevant modules and their versions. + final Map modules; + /// Event message. /// /// Generally an event either contains a [message] or an [exception]. @@ -118,14 +137,22 @@ class Event { /// var supplemented = [Event.defaultFingerprint, 'foo']; final List fingerprint; + /// The SDK Interface describes the Sentry SDK and its configuration used to capture and transmit an event. final Sdk sdk; - Event copyWith({ + // TODO: Request and DebugMeta Interface + // TODO: do we need a Threads interface? + + SentryEvent copyWith({ SentryId eventId, - String loggerName, + DateTime timestamp, + String platform, + String logger, String serverName, String release, + String dist, String environment, + Map modules, Message message, String transaction, dynamic exception, @@ -140,12 +167,16 @@ class Event { List breadcrumbs, Sdk sdk, }) => - Event( + SentryEvent( eventId: eventId ?? this.eventId, - loggerName: loggerName ?? this.loggerName, + timestamp: timestamp ?? this.timestamp, + platform: platform ?? this.platform, + logger: logger ?? this.logger, serverName: serverName ?? this.serverName, release: release ?? this.release, + dist: dist ?? this.dist, environment: environment ?? this.environment, + modules: modules ?? this.modules, message: message ?? this.message, transaction: transaction ?? this.transaction, exception: exception ?? this.exception, @@ -164,16 +195,22 @@ class Event { /// Serializes this event to JSON. Map toJson( {StackFrameFilter stackFrameFilter, String origin}) { - final json = { - 'platform': sdkPlatform, - }; + final json = {}; if (eventId != null) { json['event_id'] = eventId.toString(); } - if (loggerName != null) { - json['logger'] = loggerName; + if (timestamp != null) { + json['timestamp'] = formatDateAsIso8601WithSecondPrecision(timestamp); + } + + if (platform != null) { + json['platform'] = platform; + } + + if (logger != null) { + json['logger'] = logger; } if (serverName != null) { @@ -184,10 +221,18 @@ class Event { json['release'] = release; } + if (dist != null) { + json['dist'] = dist; + } + if (environment != null) { json['environment'] = environment; } + if (modules != null && modules.isNotEmpty) { + json['modules'] = modules; + } + if (message != null) { json['message'] = message.toJson(); } @@ -197,6 +242,7 @@ class Event { } if (exception != null) { + // TODO: create Exception and Mechanism Interface class json['exception'] = [ { 'type': '${exception.runtimeType}', @@ -204,6 +250,7 @@ class Event { } ]; if (exception is Error && exception.stackTrace != null) { + // TODO: create Stack Trace and Frame Interface json['stacktrace'] = { 'frames': encodeStackTrace( exception.stackTrace, diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 23a8ea6580..3778b56b3d 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -45,7 +45,7 @@ class Sentry { } /// Reports an [event] to Sentry.io. - static Future captureEvent(Event event) async { + static Future captureEvent(SentryEvent event) async { return currentHub.captureEvent(event); } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 154145b6a5..58a8ccb725 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -3,6 +3,7 @@ import 'package:sentry/sentry.dart'; import 'diagnostic_logger.dart'; import 'hub.dart'; import 'protocol.dart'; +import 'utils.dart'; /// Sentry SDK options class SentryOptions { @@ -20,7 +21,7 @@ class SentryOptions { /// event to event, such as local operating system version, the version of /// Dart/Flutter SDK, etc. These attributes have lower precedence than those /// supplied in the even passed to [capture]. - Event environmentAttributes; + SentryEvent environmentAttributes; /// If [compressPayload] is `true` the outgoing HTTP payloads are compressed /// using gzip. Otherwise, the payloads are sent in plain UTF8-encoded JSON @@ -33,12 +34,12 @@ class SentryOptions { /// If [clock] is provided, it is used to get time instead of the system /// clock. This is useful in tests. Should be an implementation of [ClockProvider]. - /// This parameter is dynamic to maintain backwards compatibility with - /// previous use of [Clock](https://pub.dartlang.org/documentation/quiver/latest/quiver.time/Clock-class.html) - /// from [`package:quiver`](https://pub.dartlang.org/packages/quiver). - dynamic clock; + ClockProvider _clock; - int maxBreadcrumbs; + ClockProvider get clock => _clock; + + /// This variable controls the total amount of breadcrumbs that should be captured Default is 100 + int maxBreadcrumbs = 100; /// Logger interface to log useful debugging information if debug is enabled Logger _logger = noOpLogger; @@ -138,8 +139,10 @@ class SentryOptions { this.environmentAttributes, this.compressPayload, this.httpClient, - this.clock, - }); + ClockProvider clock = getUtcDateTime, + }) { + _clock = clock; + } /// Adds an event processor void addEventProcessor(EventProcessor eventProcessor) { @@ -172,19 +175,23 @@ class SentryOptions { } } -typedef BeforeSendCallback = Event Function(Event event, dynamic hint); +typedef BeforeSendCallback = SentryEvent Function( + SentryEvent event, dynamic hint); typedef BeforeBreadcrumbCallback = Breadcrumb Function( Breadcrumb breadcrumb, dynamic hint, ); -typedef EventProcessor = Event Function(Event event, dynamic hint); +typedef EventProcessor = SentryEvent Function(SentryEvent event, dynamic hint); typedef Integration = Function(Hub hub, SentryOptions options); typedef Logger = Function(SentryLevel level, String message); +/// Used to provide timestamp for logging. +typedef ClockProvider = DateTime Function(); + void noOpLogger(SentryLevel level, String message) {} void dartLogger(SentryLevel level, String message) { diff --git a/dart/test/contexts_test.dart b/dart/test/contexts_test.dart index 042c2a8125..87ed57ed90 100644 --- a/dart/test/contexts_test.dart +++ b/dart/test/contexts_test.dart @@ -52,7 +52,7 @@ void main() { browser: testBrowser, ); - final event = Event(contexts: contexts); + final event = SentryEvent(contexts: contexts); expect( event.toJson()['contexts'], diff --git a/dart/test/event_test.dart b/dart/test/event_test.dart index a4a7447a57..b9ed7b73e1 100644 --- a/dart/test/event_test.dart +++ b/dart/test/event_test.dart @@ -7,7 +7,7 @@ import 'package:sentry/src/stack_trace.dart'; import 'package:test/test.dart'; void main() { - group(Event, () { + group(SentryEvent, () { test('$Breadcrumb serializes', () { expect( Breadcrumb( @@ -25,8 +25,9 @@ void main() { ); }); test('$Sdk serializes', () { - final event = Event( + final event = SentryEvent( eventId: SentryId.empty(), + timestamp: DateTime.utc(2019), sdk: Sdk( name: 'sentry.dart.flutter', version: '4.3.2', @@ -35,6 +36,7 @@ void main() { expect(event.toJson(), { 'platform': 'dart', 'event_id': '00000000000000000000000000000000', + 'timestamp': '2019-01-01T00:00:00', 'sdk': { 'name': 'sentry.dart.flutter', 'version': '4.3.2', @@ -46,6 +48,7 @@ void main() { }); }); test('serializes to JSON', () { + final timestamp = DateTime.utc(2019); const user = User( id: 'user_id', username: 'username', @@ -54,15 +57,16 @@ void main() { extras: {'foo': 'bar'}); final breadcrumbs = [ - Breadcrumb('test log', DateTime.utc(2019), + Breadcrumb('test log', timestamp, level: SentryLevel.debug, category: 'test'), ]; final error = StateError('test-error'); expect( - Event( + SentryEvent( eventId: SentryId.empty(), + timestamp: timestamp, message: Message( 'test-message 1 2', template: 'test-message %d %d', @@ -80,13 +84,14 @@ void main() { 'e': 'f', 'g': 2, }, - fingerprint: const [Event.defaultFingerprint, 'foo'], + fingerprint: const [SentryEvent.defaultFingerprint, 'foo'], userContext: user, breadcrumbs: breadcrumbs, ).toJson(), { 'platform': 'dart', 'event_id': '00000000000000000000000000000000', + 'timestamp': '2019-01-01T00:00:00', 'sdk': {'version': sdkVersion, 'name': 'sentry.dart'}, 'message': { 'formatted': 'test-message 1 2', diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index b5a4c44b5d..e50a8f0abf 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -12,8 +12,8 @@ final fakeMessage = Message('message 1', template: 'message %d', params: ['1']); final fakeUser = User(id: '1', email: 'test@test'); -final fakeEvent = Event( - loggerName: 'main', +final fakeEvent = SentryEvent( + logger: 'main', serverName: 'server.dart', release: '1.4.0-preview.1', environment: 'Test', diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index 50182c6e4b..1ffd29a6cc 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -76,7 +76,7 @@ Future testCaptureException( httpClient: httpMock, clock: fakeClockProvider, compressPayload: compressPayload, - environmentAttributes: Event( + environmentAttributes: SentryEvent( serverName: 'test.server.com', release: '1.2.3', environment: 'staging', @@ -238,7 +238,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { httpClient: httpMock, clock: fakeClockProvider, compressPayload: false, - environmentAttributes: Event( + environmentAttributes: SentryEvent( serverName: 'test.server.com', release: '1.2.3', environment: 'staging', @@ -295,7 +295,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { httpClient: httpMock, clock: fakeClockProvider, compressPayload: false, - environmentAttributes: Event( + environmentAttributes: SentryEvent( serverName: 'test.server.com', release: '1.2.3', environment: 'staging', @@ -314,7 +314,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { await client.close(); }); - test('$Event userContext overrides client', () async { + test('$SentryEvent userContext overrides client', () async { final fakeClockProvider = () => DateTime.utc(2017, 1, 2); String loggedUserId; // used to find out what user context was sent @@ -350,7 +350,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { httpClient: httpMock, clock: fakeClockProvider, compressPayload: false, - environmentAttributes: Event( + environmentAttributes: SentryEvent( serverName: 'test.server.com', release: '1.2.3', environment: 'staging', @@ -362,12 +362,12 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { try { throw ArgumentError('Test error'); } catch (error, stackTrace) { - final eventWithoutContext = Event( + final eventWithoutContext = SentryEvent( eventId: SentryId.empty(), exception: error, stackTrace: stackTrace, ); - final eventWithContext = Event( + final eventWithContext = SentryEvent( eventId: SentryId.empty(), exception: error, stackTrace: stackTrace, diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 94b288ce09..4b12dd8ca7 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -35,7 +35,7 @@ Future main() async { runApp(MyApp()); }, (error, stackTrace) async { print('Capture from runZonedGuarded $error'); - final event = Event( + final event = SentryEvent( exception: error, stackTrace: stackTrace, // release is required on Web to match the source maps From 3fa0b7927b3096d0d2afd1f88e632340624fed03 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Thu, 22 Oct 2020 10:13:42 +0200 Subject: [PATCH 08/34] feat: before breadcrumb and scope ref (#122) --- CHANGELOG.md | 9 +++--- dart/lib/src/scope.dart | 64 +++++++++++++++++++++++---------------- dart/test/scope_test.dart | 36 +++++++++++++++++++++- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4917265a24..bbcd28ab28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ - remove the `package:args` dependency #94 - move the `package:pedantic` to dev depencies #94 - Added GH Action Changelog verifier #95 -- Added GH Action (CI) for Dart +- Added GH Action (CI) for Dart #97 - new Dart code file structure #96 - 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 @@ -22,10 +22,11 @@ - 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 +- Ref: Hub passes the Scope to SentryClient #114 - Feature: sentry options #116 -- Ref: SentryId generates UUID -- Ref: Event now is SentryEvent and added GPU +- Ref: SentryId generates UUID #119 +- Ref: Event now is SentryEvent and added GPU #121 +- Feat: before breadcrumb and scope ref. #122 # `package:sentry` changelog diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 11c2530795..4927fcaa5b 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -6,32 +6,14 @@ import 'sentry_options.dart'; /// Scope data to be sent with the event class Scope { /// How important this event is. - SentryLevel _level; - - SentryLevel get level => _level; - - set level(SentryLevel level) { - _level = level; - } + SentryLevel 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; - } + String transaction; /// Information about the current user. - User _user; - - User get user => _user; - - set user(User user) { - _user = user; - } + User user; /// Used to deduplicate events by grouping ones with the same fingerprint /// together. @@ -71,14 +53,20 @@ class Scope { Map get extra => Map.unmodifiable(_extra); - // TODO: EventProcessors, Contexts, BeforeBreadcrumbCallback, Breadcrumb Hint, clone + // TODO: Contexts + + /// Scope's event processor list + final List _eventProcessors = []; + + List get eventProcessors => + List.unmodifiable(_eventProcessors); final SentryOptions _options; Scope(this._options) : assert(_options != null, 'SentryOptions is required'); /// Adds a breadcrumb to the breadcrumbs queue - void addBreadcrumb(Breadcrumb breadcrumb) { + void addBreadcrumb(Breadcrumb breadcrumb, {dynamic hint}) { assert(breadcrumb != null, "Breadcrumb can't be null"); // bail out if maxBreadcrumbs is zero @@ -86,6 +74,17 @@ class Scope { return; } + // run before breadcrumb callback if set + if (_options.beforeBreadcrumbCallback != null) { + breadcrumb = _options.beforeBreadcrumbCallback(breadcrumb, hint); + + if (breadcrumb == null) { + _options.logger( + SentryLevel.info, 'Breadcrumb was dropped by beforeBreadcrumb'); + return; + } + } + // remove first item if list if full if (_breadcrumbs.length >= _options.maxBreadcrumbs && _breadcrumbs.isNotEmpty) { @@ -100,15 +99,23 @@ class Scope { _breadcrumbs.clear(); } + /// Adds an event processor + void addEventProcessor(EventProcessor eventProcessor) { + assert(eventProcessor != null, "EventProcessor can't be null"); + + _eventProcessors.add(eventProcessor); + } + /// Resets the Scope to its default state void clear() { clearBreadcrumbs(); - _level = null; - _transaction = null; - _user = null; + level = null; + transaction = null; + user = null; _fingerprint = null; _tags.clear(); _extra.clear(); + _eventProcessors.clear(); } /// Sets a tag to the Scope @@ -135,6 +142,7 @@ class Scope { /// Removes an extra from the Scope void removeExtra(String key) => _extra.remove(key); + /// Clones the current Scope Scope clone() { final clone = Scope(_options) ..user = user @@ -153,6 +161,10 @@ class Scope { clone.addBreadcrumb(breadcrumb); } + for (final eventProcessor in _eventProcessors) { + clone.addEventProcessor(eventProcessor); + } + return clone; } } diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index d7fcea8edf..f539930405 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -48,6 +48,25 @@ void main() { expect(sut.breadcrumbs.last, breadcrumb); }); + test('Executes and drops $Breadcrumb', () { + final sut = fixture.getSut( + beforeBreadcrumbCallback: fixture.beforeBreadcrumbCallback, + ); + + final breadcrumb = Breadcrumb('test log', DateTime.utc(2019)); + sut.addBreadcrumb(breadcrumb); + + expect(sut.breadcrumbs.length, 0); + }); + + test('adds $EventProcessor', () { + final sut = fixture.getSut(); + + sut.addEventProcessor(fixture.processor); + + expect(sut.eventProcessors.last, fixture.processor); + }); + test('respects max $Breadcrumb', () { final maxBreadcrumbs = 2; final sut = fixture.getSut(maxBreadcrumbs: maxBreadcrumbs); @@ -149,6 +168,8 @@ void main() { sut.setTag('test', 'test'); sut.setExtra('test', 'test'); + sut.addEventProcessor(fixture.processor); + sut.clear(); expect(sut.breadcrumbs.length, 0); @@ -164,6 +185,8 @@ void main() { expect(sut.tags.length, 0); expect(sut.extra.length, 0); + + expect(sut.eventProcessors.length, 0); }); test('clones', () { @@ -175,13 +198,24 @@ void main() { expect(sut.tags, clone.tags); expect(sut.breadcrumbs, clone.breadcrumbs); expect(ListEquality().equals(sut.fingerprint, clone.fingerprint), true); + expect(ListEquality().equals(sut.eventProcessors, clone.eventProcessors), + true); }); } class Fixture { - Scope getSut({int maxBreadcrumbs = 100}) { + Scope getSut({ + int maxBreadcrumbs = 100, + BeforeBreadcrumbCallback beforeBreadcrumbCallback, + }) { final options = SentryOptions(); options.maxBreadcrumbs = maxBreadcrumbs; + options.beforeBreadcrumbCallback = beforeBreadcrumbCallback; return Scope(options); } + + SentryEvent processor(SentryEvent event, dynamic hint) => null; + + Breadcrumb beforeBreadcrumbCallback(Breadcrumb breadcrumb, dynamic hint) => + null; } From 2cc1d7895f7c29c0c48d6d0659db7164337312c1 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Thu, 22 Oct 2020 12:35:58 +0200 Subject: [PATCH 09/34] Ref: Hint is passed across Sentry static class, Hub and Client (#124) --- CHANGELOG.md | 1 + dart/lib/src/client.dart | 7 +++++-- dart/lib/src/hub.dart | 19 +++++++++++++++---- dart/lib/src/noop_client.dart | 16 ++++++++++++++-- dart/lib/src/noop_hub.dart | 9 +++++++-- dart/lib/src/sentry.dart | 16 +++++++++++++--- 6 files changed, 55 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbcd28ab28..790ffdd688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Ref: SentryId generates UUID #119 - Ref: Event now is SentryEvent and added GPU #121 - Feat: before breadcrumb and scope ref. #122 +- Ref: Hint is passed across Sentry static class, Hub and Client #124 # `package:sentry` changelog diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index 12ff28b3b1..cfc05058d7 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -99,6 +99,7 @@ abstract class SentryClient { SentryEvent event, { StackFrameFilter stackFrameFilter, Scope scope, + dynamic hint, }) async { final now = options.clock(); var authHeader = 'Sentry sentry_version=6, sentry_client=$clientId, ' @@ -155,13 +156,14 @@ abstract class SentryClient { dynamic throwable, { dynamic stackTrace, Scope scope, + dynamic hint, }) { final event = SentryEvent( exception: throwable, stackTrace: stackTrace, timestamp: options.clock(), ); - return captureEvent(event, scope: scope); + return captureEvent(event, scope: scope, hint: hint); } /// Reports the [template] @@ -171,6 +173,7 @@ abstract class SentryClient { String template, List params, Scope scope, + dynamic hint, }) { final event = SentryEvent( message: Message( @@ -181,7 +184,7 @@ abstract class SentryClient { level: level, timestamp: options.clock(), ); - return captureEvent(event, scope: scope); + return captureEvent(event, scope: scope, hint: hint); } Future close() async { diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index a93b09aa71..2244149fe3 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -54,7 +54,7 @@ class Hub { SentryId get lastEventId => _lastEventId; /// Captures the event. - Future captureEvent(SentryEvent event) async { + Future captureEvent(SentryEvent event, {dynamic hint}) async { var sentryId = SentryId.empty(); if (!_isEnabled) { @@ -71,7 +71,11 @@ class Hub { final item = _peek(); if (item != null) { try { - sentryId = await item.client.captureEvent(event, scope: item.scope); + sentryId = await item.client.captureEvent( + event, + scope: item.scope, + hint: hint, + ); } catch (err) { _options.logger( SentryLevel.error, @@ -94,6 +98,7 @@ class Hub { Future captureException( dynamic throwable, { dynamic stackTrace, + dynamic hint, }) async { var sentryId = SentryId.empty(); @@ -111,8 +116,12 @@ class Hub { final item = _peek(); if (item != null) { try { - sentryId = await item.client.captureException(throwable, - stackTrace: stackTrace, scope: item.scope); + sentryId = await item.client.captureException( + throwable, + stackTrace: stackTrace, + scope: item.scope, + hint: hint, + ); } catch (err) { _options.logger( SentryLevel.error, @@ -138,6 +147,7 @@ class Hub { SentryLevel level = SentryLevel.info, String template, List params, + dynamic hint, }) async { var sentryId = SentryId.empty(); @@ -161,6 +171,7 @@ class Hub { template: template, params: params, scope: item.scope, + hint: hint, ); } catch (err) { _options.logger( diff --git a/dart/lib/src/noop_client.dart b/dart/lib/src/noop_client.dart index c5f5c7d438..06a2a2e8af 100644 --- a/dart/lib/src/noop_client.dart +++ b/dart/lib/src/noop_client.dart @@ -4,6 +4,7 @@ import 'client.dart'; import 'protocol.dart'; import 'scope.dart'; import 'sentry_options.dart'; +import 'stack_trace.dart'; class NoOpSentryClient implements SentryClient { NoOpSentryClient._(); @@ -34,11 +35,21 @@ class NoOpSentryClient implements SentryClient { Map buildHeaders(String authHeader) => {}; @override - Future captureEvent(SentryEvent event, {stackFrameFilter, scope}) => + Future captureEvent( + SentryEvent event, { + StackFrameFilter stackFrameFilter, + Scope scope, + dynamic hint, + }) => Future.value(SentryId.empty()); @override - Future captureException(throwable, {stackTrace, scope}) => + Future captureException( + dynamic throwable, { + dynamic stackTrace, + Scope scope, + dynamic hint, + }) => Future.value(SentryId.empty()); @override @@ -48,6 +59,7 @@ class NoOpSentryClient implements SentryClient { String template, List params, Scope scope, + dynamic hint, }) => Future.value(SentryId.empty()); diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 30dbb422fe..f8e528430e 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -19,11 +19,15 @@ class NoOpHub implements Hub { void bindClient(SentryClient client) {} @override - Future captureEvent(SentryEvent event) => + Future captureEvent(SentryEvent event, {dynamic hint}) => Future.value(SentryId.empty()); @override - Future captureException(throwable, {stackTrace}) => + Future captureException( + dynamic throwable, { + dynamic stackTrace, + dynamic hint, + }) => Future.value(SentryId.empty()); @override @@ -32,6 +36,7 @@ class NoOpHub implements Hub { SentryLevel level = SentryLevel.info, String template, List params, + dynamic hint, }) => Future.value(SentryId.empty()); diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 3778b56b3d..31409f156a 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -45,16 +45,24 @@ class Sentry { } /// Reports an [event] to Sentry.io. - static Future captureEvent(SentryEvent event) async { - return currentHub.captureEvent(event); + static Future captureEvent( + SentryEvent event, { + dynamic hint, + }) async { + return currentHub.captureEvent(event, hint: hint); } /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. static Future captureException( dynamic error, { dynamic stackTrace, + dynamic hint, }) async { - return currentHub.captureException(error, stackTrace: stackTrace); + return currentHub.captureException( + error, + stackTrace: stackTrace, + hint: hint, + ); } Future captureMessage( @@ -62,12 +70,14 @@ class Sentry { SentryLevel level, String template, List params, + dynamic hint, }) async { return currentHub.captureMessage( message, level: level, template: template, params: params, + hint: hint, ); } From 11b821bc0048ea99582fcd2d583f3334e7c4e355 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Thu, 22 Oct 2020 16:36:57 +0200 Subject: [PATCH 10/34] Ref: Remove stackFrameFilter in favor of beforeSendCallback (#125) --- CHANGELOG.md | 1 + dart/lib/src/client.dart | 3 -- dart/lib/src/noop_client.dart | 4 +-- dart/lib/src/protocol/sentry_event.dart | 5 +--- dart/lib/src/stack_trace.dart | 14 +--------- dart/test/event_test.dart | 1 - dart/test/hub_test.dart | 3 -- dart/test/sentry_test.dart | 1 - dart/test/stack_trace_test.dart | 37 +++++++++++++------------ 9 files changed, 23 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 790ffdd688..db00731adb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - Ref: Event now is SentryEvent and added GPU #121 - Feat: before breadcrumb and scope ref. #122 - Ref: Hint is passed across Sentry static class, Hub and Client #124 +- Ref: Remove stackFrameFilter in favor of beforeSendCallback #125 # `package:sentry` changelog diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index cfc05058d7..437306c8e5 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -8,7 +8,6 @@ import 'client_stub.dart' if (dart.library.html) 'browser_client.dart' if (dart.library.io) 'io_client.dart'; import 'protocol.dart'; -import 'stack_trace.dart'; import 'utils.dart'; import 'version.dart'; @@ -97,7 +96,6 @@ abstract class SentryClient { /// Reports an [event] to Sentry.io. Future captureEvent( SentryEvent event, { - StackFrameFilter stackFrameFilter, Scope scope, dynamic hint, }) async { @@ -128,7 +126,6 @@ abstract class SentryClient { mergeAttributes( event.toJson( - stackFrameFilter: stackFrameFilter, origin: origin, ), into: data, diff --git a/dart/lib/src/noop_client.dart b/dart/lib/src/noop_client.dart index 06a2a2e8af..223ee17bc4 100644 --- a/dart/lib/src/noop_client.dart +++ b/dart/lib/src/noop_client.dart @@ -4,7 +4,6 @@ import 'client.dart'; import 'protocol.dart'; import 'scope.dart'; import 'sentry_options.dart'; -import 'stack_trace.dart'; class NoOpSentryClient implements SentryClient { NoOpSentryClient._(); @@ -37,7 +36,6 @@ class NoOpSentryClient implements SentryClient { @override Future captureEvent( SentryEvent event, { - StackFrameFilter stackFrameFilter, Scope scope, dynamic hint, }) => @@ -45,7 +43,7 @@ class NoOpSentryClient implements SentryClient { @override Future captureException( - dynamic throwable, { + throwable, { dynamic stackTrace, Scope scope, dynamic hint, diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index 142ce218d2..25452363b1 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -193,8 +193,7 @@ class SentryEvent { ); /// Serializes this event to JSON. - Map toJson( - {StackFrameFilter stackFrameFilter, String origin}) { + Map toJson({String origin}) { final json = {}; if (eventId != null) { @@ -254,7 +253,6 @@ class SentryEvent { json['stacktrace'] = { 'frames': encodeStackTrace( exception.stackTrace, - stackFrameFilter: stackFrameFilter, origin: origin, ), }; @@ -265,7 +263,6 @@ class SentryEvent { json['stacktrace'] = { 'frames': encodeStackTrace( stackTrace, - stackFrameFilter: stackFrameFilter, origin: origin, ), }; diff --git a/dart/lib/src/stack_trace.dart b/dart/lib/src/stack_trace.dart index 0edc106a7e..299c9bb10d 100644 --- a/dart/lib/src/stack_trace.dart +++ b/dart/lib/src/stack_trace.dart @@ -4,16 +4,6 @@ import 'package:stack_trace/stack_trace.dart'; -/// Used to filter or modify stack frames before sending the stack trace. -/// -/// The input stack frames are in the Sentry.io JSON format. The output -/// stack frames must follow the same format. -/// -/// Detailed documentation about the stack trace format is on Sentry.io's -/// web-site: https://docs.sentry.io/development/sdk-dev/overview/. -typedef StackFrameFilter = List> Function( - List>); - /// Sentry.io JSON encoding of a stack frame for the asynchronous suspension, /// which is the gap between asynchronous calls. const Map asynchronousGapFrameJson = { @@ -25,7 +15,6 @@ const Map asynchronousGapFrameJson = { /// [stackTrace] must be [String] or [StackTrace]. List> encodeStackTrace( dynamic stackTrace, { - StackFrameFilter stackFrameFilter, String origin, }) { assert(stackTrace is String || stackTrace is StackTrace); @@ -47,8 +36,7 @@ List> encodeStackTrace( } } - final jsonFrames = frames.reversed.toList(); - return stackFrameFilter != null ? stackFrameFilter(jsonFrames) : jsonFrames; + return frames.reversed.toList(); } Map encodeStackTraceFrame(Frame frame, {String origin}) { diff --git a/dart/test/event_test.dart b/dart/test/event_test.dart index b9ed7b73e1..81abef84c8 100644 --- a/dart/test/event_test.dart +++ b/dart/test/event_test.dart @@ -131,7 +131,6 @@ void main() { 'stacktrace': { 'frames': encodeStackTrace( error.stackTrace, - stackFrameFilter: null, origin: null, ) } diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 97cb566081..24527b7364 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -57,7 +57,6 @@ void main() { client.captureEvent( fakeEvent, scope: captureAnyNamed('scope'), - stackFrameFilter: null, ), ).captured.first, Scope(options), @@ -112,7 +111,6 @@ void main() { client.captureEvent( fakeEvent, scope: captureAnyNamed('scope'), - stackFrameFilter: null, ), ).captured.first, Scope(SentryOptions(dsn: fakeDsn)) @@ -145,7 +143,6 @@ void main() { client2.captureEvent( fakeEvent, scope: anyNamed('scope'), - stackFrameFilter: null, ), ).called(1); }); diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 6f4067a1eb..de992ff0f6 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -24,7 +24,6 @@ void main() { client.captureEvent( fakeEvent, scope: anyNamed('scope'), - stackFrameFilter: null, ), ).called(1); }); diff --git a/dart/test/stack_trace_test.dart b/dart/test/stack_trace_test.dart index d0aa7b94ff..947fdc6551 100644 --- a/dart/test/stack_trace_test.dart +++ b/dart/test/stack_trace_test.dart @@ -79,24 +79,25 @@ 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(); +// 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' - }, - ]); - }); +// 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' +// }, +// ]); +// }); }); } From ee737a24fd1905eb8b89f947e5fb014292615aba Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Thu, 22 Oct 2020 16:59:56 +0200 Subject: [PATCH 11/34] Ref: Sentry init with null and empty DSN and close method (#126) --- .gitattributes | 1 + CHANGELOG.md | 1 + dart/lib/src/client.dart | 2 +- dart/lib/src/sentry.dart | 23 +++++++++++++++++++---- dart/test/sentry_test.dart | 35 ++++++++++++++++++++++++++++++++--- 5 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..a19ade077d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=union diff --git a/CHANGELOG.md b/CHANGELOG.md index db00731adb..74df90e45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Ref: SentryId generates UUID #119 - Ref: Event now is SentryEvent and added GPU #121 - Feat: before breadcrumb and scope ref. #122 +- Ref: Sentry init with null and empty DSN and close method #126 - Ref: Hint is passed across Sentry static class, Hub and Client #124 - Ref: Remove stackFrameFilter in favor of beforeSendCallback #125 diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index 437306c8e5..9318898207 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -184,7 +184,7 @@ abstract class SentryClient { return captureEvent(event, scope: scope, hint: hint); } - Future close() async { + void close() { options.httpClient?.close(); } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 31409f156a..18856f6183 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -37,7 +37,10 @@ class Sentry { ); } - _setDefaultConfiguration(options); + // if there's an empty DSN, SDK is disabled + if (!_setDefaultConfiguration(options)) { + return; + } final hub = currentHub; _hub = Hub(options); @@ -82,17 +85,29 @@ class Sentry { } /// Close the client SDK - static Future close() async => currentHub.close(); + static void close() { + final hub = currentHub; + _hub = NoOpHub(); + return hub.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 + static bool _setDefaultConfiguration(SentryOptions options) { + if (options.dsn == null) { + throw ArgumentError.notNull( + 'DSN is required. Use empty string to disable SDK.'); + } + if (options.dsn.isEmpty) { + close(); + return false; + } if (options.debug && options.logger == noOpLogger) { options.logger = dartLogger; } + return true; } /// client injector only use for testing diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index de992ff0f6..346d4e2156 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -5,7 +5,7 @@ import 'package:test/test.dart'; import 'mocks.dart'; void main() { - group('Sentry static entry', () { + group('Sentry capture methods', () { SentryClient client; Exception anException; @@ -17,9 +17,12 @@ void main() { client = MockSentryClient(); Sentry.initClient(client); }); + tearDown(() { + Sentry.close(); + }); - test('should capture the event', () { - Sentry.captureEvent(fakeEvent); + test('should capture the event', () async { + await Sentry.captureEvent(fakeEvent); verify( client.captureEvent( fakeEvent, @@ -54,4 +57,30 @@ void main() { ).called(1); }); }); + group('Sentry is enabled or closed', () { + test('null DSN', () { + expect( + () => Sentry.init((options) => options.dsn = null), + throwsArgumentError, + ); + expect(Sentry.isEnabled, false); + }); + + test('empty DSN', () { + Sentry.init((options) => options.dsn = ''); + expect(Sentry.isEnabled, false); + }); + + test('empty DSN', () { + Sentry.init((options) => options.dsn = fakeDsn); + + Sentry.initClient(MockSentryClient()); + + expect(Sentry.isEnabled, true); + + Sentry.close(); + + expect(Sentry.isEnabled, false); + }); + }); } From eab4ccf71ff96ac23020e3247afb537f74dc766f Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Fri, 23 Oct 2020 19:19:31 +0200 Subject: [PATCH 12/34] Refacto : add a Transport class (#123) --- CHANGELOG.md | 2 + dart/lib/sentry.dart | 4 +- dart/lib/src/browser_client.dart | 30 +- dart/lib/src/client.dart | 266 +++++++----------- dart/lib/src/io_client.dart | 52 +--- dart/lib/src/noop_client.dart | 43 +-- dart/lib/src/protocol/dsn.dart | 22 +- dart/lib/src/protocol/sentry_event.dart | 8 +- dart/lib/src/sentry_options.dart | 29 +- dart/lib/src/transport/body_encoder.dart | 17 ++ .../src/transport/body_encoder_browser.dart | 12 + dart/lib/src/transport/noop_transport.dart | 11 + dart/lib/src/transport/transport.dart | 114 ++++++++ dart/lib/src/utils.dart | 3 + dart/lib/src/version.dart | 17 +- dart/test/event_test.dart | 27 +- dart/test/mocks.dart | 2 + dart/test/sentry_client_test.dart | 44 +++ dart/test/test_utils.dart | 176 ++++++------ flutter/example/lib/main.dart | 3 +- 20 files changed, 485 insertions(+), 397 deletions(-) create mode 100644 dart/lib/src/transport/body_encoder.dart create mode 100644 dart/lib/src/transport/body_encoder_browser.dart create mode 100644 dart/lib/src/transport/noop_transport.dart create mode 100644 dart/lib/src/transport/transport.dart create mode 100644 dart/test/sentry_client_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 74df90e45e..441bb07705 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ - Ref: Sentry init with null and empty DSN and close method #126 - Ref: Hint is passed across Sentry static class, Hub and Client #124 - Ref: Remove stackFrameFilter in favor of beforeSendCallback #125 +- Ref: added Transport #123 +- Feat: apply sample rate # `package:sentry` changelog diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 471852847b..85a27fd53e 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -5,8 +5,8 @@ /// A pure Dart client for Sentry.io crash reporting. export 'src/client.dart'; export 'src/protocol.dart'; +export 'src/scope.dart'; export 'src/sentry.dart'; export 'src/sentry_options.dart'; +export 'src/transport/transport.dart'; export 'src/version.dart'; -export 'src/scope.dart'; -export 'src/sentry_options.dart'; diff --git a/dart/lib/src/browser_client.dart b/dart/lib/src/browser_client.dart index b34c862e9d..26a8240d50 100644 --- a/dart/lib/src/browser_client.dart +++ b/dart/lib/src/browser_client.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. /// A pure Dart client for Sentry.io crash reporting. -import 'dart:convert'; import 'dart:html' show window; import 'package:http/browser_client.dart'; @@ -28,32 +27,15 @@ class SentryBrowserClient extends SentryClient { /// /// If [httpClient] is provided, it is used instead of the default client to /// make HTTP calls to Sentry.io. This is useful in tests. - factory SentryBrowserClient(SentryOptions options, {String origin}) { + factory SentryBrowserClient(SentryOptions options) { options.httpClient ??= BrowserClient(); - // origin is necessary for sentry to resolve stacktrace - origin ??= '${window.location.origin}/'; + options.sdk ??= Sdk(name: sdkName, version: sdkVersion); - return SentryBrowserClient._( - options, - origin: origin, - platform: browserPlatform, - ); + // origin is necessary for sentry to resolve stacktrace + return SentryBrowserClient._(options); } - SentryBrowserClient._(SentryOptions options, {String origin, String platform}) - : super.base( - options, - origin: origin, - sdk: Sdk(name: browserSdkName, version: sdkVersion), - platform: platform, - ); - - @override - List bodyEncoder( - Map data, - Map headers, - ) => - // Gzip compression is implicit on browser - utf8.encode(json.encode(data)); + SentryBrowserClient._(SentryOptions options) + : super.base(options, origin: '${window.location.origin}/'); } diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index 9318898207..9f100fda14 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -1,15 +1,14 @@ import 'dart:async'; -import 'dart:convert'; +import 'dart:math'; import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; +import 'package:sentry/src/transport/noop_transport.dart'; import 'client_stub.dart' if (dart.library.html) 'browser_client.dart' if (dart.library.io) 'io_client.dart'; import 'protocol.dart'; -import 'utils.dart'; -import 'version.dart'; /// Logs crash reports and events to the Sentry.io service. abstract class SentryClient { @@ -19,133 +18,41 @@ abstract class SentryClient { /// `dart:html` is available, otherwise it will throw an unsupported error. factory SentryClient(SentryOptions options) => createSentryClient(options); - SentryClient.base( - this.options, { - String platform, - this.origin, - Sdk sdk, - }) : _dsn = Dsn.parse(options.dsn), - _platform = platform ?? sdkPlatform, - sdk = sdk ?? Sdk(name: sdkName, version: sdkVersion); - - final Dsn _dsn; - - @protected - SentryOptions options; - - /// The DSN URI. - @visibleForTesting - Uri get dsnUri => _dsn.uri; - - /// The Sentry.io public key for the project. - @visibleForTesting - // ignore: invalid_use_of_visible_for_testing_member - String get publicKey => _dsn.publicKey; - - /// The Sentry.io secret key for the project. - @visibleForTesting - // ignore: invalid_use_of_visible_for_testing_member - String get secretKey => _dsn.secretKey; - - /// The ID issued by Sentry.io to your project. - /// - /// Attached to the event payload. - String get projectId => _dsn.projectId; - - /// Information about the current user. - /// - /// This information is sent with every logged event. If the value - /// of this field is updated, all subsequent events will carry the - /// new information. - /// - /// [Event.userContext] overrides the [User] context set here. - /// - /// See also: - /// * https://docs.sentry.io/learn/context/#capturing-the-user - User userContext; - - /// Use for browser stacktrace - String origin; - - /// Used by sentry to differentiate browser from io environment - final String _platform; - - final Sdk sdk; - - String get clientId => sdk.identifier; - - @visibleForTesting - String get postUri { - final port = dsnUri.hasPort && - ((dsnUri.scheme == 'http' && dsnUri.port != 80) || - (dsnUri.scheme == 'https' && dsnUri.port != 443)) - ? ':${dsnUri.port}' - : ''; - final pathLength = dsnUri.pathSegments.length; - String apiPath; - if (pathLength > 1) { - // some paths would present before the projectID in the dsnUri - apiPath = - (dsnUri.pathSegments.sublist(0, pathLength - 1) + ['api']).join('/'); - } else { - apiPath = 'api'; + SentryClient.base(this._options, {String origin}) { + _random = _options.sampleRate == null ? null : Random(); + if (_options.transport is NoOpTransport) { + _options.transport = Transport(options: _options, origin: origin); } - return '${dsnUri.scheme}://${dsnUri.host}$port/$apiPath/$projectId/store/'; } + SentryOptions _options; + + Random _random; + /// Reports an [event] to Sentry.io. Future captureEvent( SentryEvent event, { Scope scope, dynamic hint, }) async { - final now = options.clock(); - var authHeader = 'Sentry sentry_version=6, sentry_client=$clientId, ' - 'sentry_timestamp=${now.millisecondsSinceEpoch}, sentry_key=$publicKey'; - if (secretKey != null) { - authHeader += ', sentry_secret=$secretKey'; - } - - final headers = buildHeaders(authHeader); - - final data = { - 'project': projectId, - 'event_id': event.eventId.toString(), - 'timestamp': formatDateAsIso8601WithSecondPrecision(event.timestamp), - }; - - if (options.environmentAttributes != null) { - mergeAttributes(options.environmentAttributes.toJson(), into: data); - } + event = _processEvent(event, eventProcessors: _options.eventProcessors); - // Merge the user context. - if (userContext != null) { - mergeAttributes({'user': userContext.toJson()}, - into: data); + // dropped by sampling or event processors + if (event == null) { + return Future.value(SentryId.empty()); } - mergeAttributes( - event.toJson( - origin: origin, - ), - into: data, - ); - mergeAttributes({'platform': _platform}, into: data); - - final body = bodyEncoder(data, headers); + event = _applyScope(event: event, scope: scope); - final response = await options.httpClient.post( - postUri, - headers: headers, - body: body, + // TODO create eventProcessors ? + event = event.copyWith( + serverName: _options.serverName, + environment: _options.environment, + release: _options.release, + platform: event.platform ?? sdkPlatform, ); - if (response.statusCode != 200) { - return SentryId.empty(); - } - - final eventId = json.decode(response.body)['id']; - return eventId != null ? SentryId.fromId(eventId) : SentryId.empty(); + return _options.transport.send(event); } /// Reports the [throwable] and optionally its [stackTrace] to Sentry.io. @@ -158,7 +65,7 @@ abstract class SentryClient { final event = SentryEvent( exception: throwable, stackTrace: stackTrace, - timestamp: options.clock(), + timestamp: _options.clock(), ); return captureEvent(event, scope: scope, hint: hint); } @@ -173,63 +80,94 @@ abstract class SentryClient { dynamic hint, }) { final event = SentryEvent( - message: Message( - formatted, - template: template, - params: params, - ), + message: Message(formatted, template: template, params: params), level: level, - timestamp: options.clock(), + timestamp: _options.clock(), ); - return captureEvent(event, scope: scope, hint: hint); - } - void close() { - options.httpClient?.close(); + return captureEvent(event, scope: scope, hint: hint); } - @override - String toString() => '$SentryClient("$postUri")'; + void close() => _options.httpClient?.close(); - @protected - List bodyEncoder(Map data, Map headers); - - @protected - @mustCallSuper - Map buildHeaders(String authHeader) { - final headers = { - 'Content-Type': 'application/json', - }; + SentryEvent _processEvent( + SentryEvent event, { + dynamic hint, + List eventProcessors, + }) { + if (_sampleRate()) { + _options.logger( + SentryLevel.debug, + 'Event ${event.eventId.toString()} was dropped due to sampling decision.', + ); + return null; + } - if (authHeader != null) { - headers['X-Sentry-Auth'] = authHeader; + for (final processor in eventProcessors) { + try { + event = processor(event, hint); + } catch (err) { + _options.logger( + SentryLevel.error, + 'An exception occurred while processing event by a processor : $err', + ); + } + if (event == null) { + _options.logger(SentryLevel.debug, 'Event was dropped by a processor'); + break; + } } + return event; + } - return headers; + SentryEvent _applyScope({ + @required SentryEvent event, + @required Scope scope, + }) { + if (scope != null) { + // Merge the scope transaction. + if (event.transaction == null) { + event = event.copyWith(transaction: scope.transaction); + } + + // Merge the user context. + if (event.userContext == null) { + event = event.copyWith(userContext: scope.user); + } + + // Merge the scope fingerprint. + if (event.fingerprint == null) { + event = event.copyWith(fingerprint: scope.fingerprint); + } + + // Merge the scope breadcrumbs. + if (event.breadcrumbs == null) { + event = event.copyWith(breadcrumbs: scope.breadcrumbs); + } + + // TODO add tests + // Merge the scope tags. + event = event.copyWith( + tags: scope.tags.map((key, value) => MapEntry(key, value)) + ..addAll(event.tags ?? {})); + + // Merge the scope extra. + event = event.copyWith( + extra: scope.extra.map((key, value) => MapEntry(key, value)) + ..addAll(event.extra ?? {})); + + // Merge the scope level. + if (scope.level != null) { + event = event.copyWith(level: scope.level); + } + } + return event; } -} -/// A response from Sentry.io. -/// -/// If [isSuccessful] the [eventId] field will contain the ID assigned to the -/// captured event by the Sentry.io backend. Otherwise, the [error] field will -/// contain the description of the error. -@immutable -class SentryResponse { - const SentryResponse.success({@required this.eventId}) - : isSuccessful = true, - error = null; - - const SentryResponse.failure(this.error) - : isSuccessful = false, - eventId = null; - - /// Whether event was submitted successfully. - final bool isSuccessful; - - /// The ID Sentry.io assigned to the submitted event for future reference. - final String eventId; - - /// Error message, if the response is not successful. - final String error; + bool _sampleRate() { + if (_options.sampleRate != null && _random != null) { + return (_options.sampleRate < _random.nextDouble()); + } + return false; + } } diff --git a/dart/lib/src/io_client.dart b/dart/lib/src/io_client.dart index a7f5d59297..8511553c06 100644 --- a/dart/lib/src/io_client.dart +++ b/dart/lib/src/io_client.dart @@ -2,60 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// A pure Dart client for Sentry.io crash reporting. -import 'dart:convert'; -import 'dart:io'; - import 'package:sentry/sentry.dart'; +/// A pure Dart client for Sentry.io crash reporting. import 'client.dart'; +import 'sentry_options.dart'; SentryClient createSentryClient(SentryOptions options) => SentryIOClient(options); /// Logs crash reports and events to the Sentry.io service. class SentryIOClient extends SentryClient { - /// Instantiates a client using [dsn] issued to your project by Sentry.io as - /// the endpoint for submitting events. - /// - /// [environmentAttributes] contain event attributes that do not change over - /// the course of a program's lifecycle. These attributes will be added to - /// all events captured via this client. The following attributes often fall - /// under this category: [Event.serverName], [Event.release], [Event.environment]. - /// - /// 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. - /// - /// If [httpClient] is provided, it is used instead of the default client to - /// make HTTP calls to Sentry.io. This is useful in tests. - factory SentryIOClient(SentryOptions options) => SentryIOClient._(options); - - SentryIOClient._(SentryOptions options) : super.base(options); - - @override - Map buildHeaders(String authHeader) { - final headers = super.buildHeaders(authHeader); - - // NOTE(lejard_h) overriding user agent on VM and Flutter not sure why - // for web it use browser user agent - headers['User-Agent'] = clientId; - - return headers; + /// Instantiates a client using [SentryOptions] + factory SentryIOClient(SentryOptions options) { + options.sdk ??= Sdk(name: sdkName, version: sdkVersion); + return SentryIOClient._(options); } - @override - List bodyEncoder( - Map data, - Map headers, - ) { - // [SentryIOClient] implement gzip compression - // gzip compression is not available on browser - var body = utf8.encode(json.encode(data)); - if (options.compressPayload) { - headers['Content-Encoding'] = 'gzip'; - body = gzip.encode(body); - } - return body; - } + SentryIOClient._(SentryOptions options) : super.base(options); } diff --git a/dart/lib/src/noop_client.dart b/dart/lib/src/noop_client.dart index 223ee17bc4..412495fec0 100644 --- a/dart/lib/src/noop_client.dart +++ b/dart/lib/src/noop_client.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'client.dart'; import 'protocol.dart'; import 'scope.dart'; -import 'sentry_options.dart'; class NoOpSentryClient implements SentryClient { NoOpSentryClient._(); @@ -14,25 +13,6 @@ class NoOpSentryClient implements SentryClient { return _instance; } - @override - User userContext; - - @override - SentryOptions options; - - @override - String origin; - - @override - List bodyEncoder( - Map data, - Map headers, - ) => - []; - - @override - Map buildHeaders(String authHeader) => {}; - @override Future captureEvent( SentryEvent event, { @@ -43,7 +23,7 @@ class NoOpSentryClient implements SentryClient { @override Future captureException( - throwable, { + dynamic throwable, { dynamic stackTrace, Scope scope, dynamic hint, @@ -61,29 +41,8 @@ class NoOpSentryClient implements SentryClient { }) => Future.value(SentryId.empty()); - @override - String get clientId => 'No-op'; - @override Future close() async { return; } - - @override - Uri get dsnUri => 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/dsn.dart b/dart/lib/src/protocol/dsn.dart index 35dc93c6dd..23c5b3edf2 100644 --- a/dart/lib/src/protocol/dsn.dart +++ b/dart/lib/src/protocol/dsn.dart @@ -9,11 +9,9 @@ class Dsn { }); /// The Sentry.io public key for the project. - @visibleForTesting final String publicKey; /// The Sentry.io secret key for the project. - @visibleForTesting final String secretKey; /// The ID issued by Sentry.io to your project. @@ -24,6 +22,26 @@ class Dsn { /// The DSN URI. final Uri uri; + String get postUri { + final port = uri.hasPort && + ((uri.scheme == 'http' && uri.port != 80) || + (uri.scheme == 'https' && uri.port != 443)) + ? ':${uri.port}' + : ''; + + final pathLength = uri.pathSegments.length; + + String apiPath; + if (pathLength > 1) { + // some paths would present before the projectID in the uri + apiPath = + (uri.pathSegments.sublist(0, pathLength - 1) + ['api']).join('/'); + } else { + apiPath = 'api'; + } + return '${uri.scheme}://${uri.host}$port/$apiPath/$projectId/store/'; + } + static Dsn parse(String dsn) { final uri = Uri.parse(dsn); final userInfo = uri.userInfo.split(':'); diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index 25452363b1..e721a485fc 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -12,7 +12,8 @@ class SentryEvent { SentryEvent({ SentryId eventId, DateTime timestamp, - String platform, + Sdk sdk, + this.platform, this.logger, this.serverName, this.release, @@ -31,10 +32,9 @@ class SentryEvent { this.userContext, this.contexts, this.breadcrumbs, - this.sdk, }) : eventId = eventId ?? SentryId.newId(), - platform = platform ?? sdkPlatform, - timestamp = timestamp ?? getUtcDateTime(); + timestamp = timestamp ?? getUtcDateTime(), + sdk = sdk ?? Sdk(name: sdkName, version: sdkVersion); /// Refers to the default fingerprinting algorithm. /// diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 58a8ccb725..8adadbe9d7 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -1,5 +1,7 @@ import 'package:http/http.dart'; import 'package:sentry/sentry.dart'; +import 'package:sentry/src/transport/noop_transport.dart'; + import 'diagnostic_logger.dart'; import 'hub.dart'; import 'protocol.dart'; @@ -14,15 +16,6 @@ class SentryOptions { /// just not send any events. String dsn; - /// Contains [Event] attributes that are automatically mixed into all events - /// captured through this client. - /// - /// This event is designed to contain static values that do not change from - /// event to event, such as local operating system version, the version of - /// Dart/Flutter SDK, etc. These attributes have lower precedence than those - /// supplied in the even passed to [capture]. - SentryEvent environmentAttributes; - /// 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. @@ -102,8 +95,8 @@ class SentryOptions { /// 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; + /// sent. Events are picked randomly. Default is null (disabled) + double sampleRate; /// 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 @@ -118,7 +111,14 @@ class SentryOptions { List get inAppIncludes => List.unmodifiable(_inAppIncludes); - // TODO: transport, transportGate, connectionTimeoutMillis, readTimeoutMillis, hostnameVerifier, sslSocketFactory, proxy + Transport _transport = NoOpTransport(); + + Transport get transport => _transport; + + 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; @@ -126,8 +126,8 @@ class SentryOptions { /// The server name used in the Sentry messages. String serverName; - /// SdkVersion object that contains the Sentry Client Name and its version - Sdk sdkVersion; + /// Sdk object that contains the Sentry Client Name and its version + Sdk sdk; // TODO: Scope observers, enableScopeSync @@ -136,7 +136,6 @@ class SentryOptions { // TODO: those ctor params could be set on Sentry._setDefaultConfiguration or instantiate by default here SentryOptions({ this.dsn, - this.environmentAttributes, this.compressPayload, this.httpClient, ClockProvider clock = getUtcDateTime, diff --git a/dart/lib/src/transport/body_encoder.dart b/dart/lib/src/transport/body_encoder.dart new file mode 100644 index 0000000000..eb46bfc16d --- /dev/null +++ b/dart/lib/src/transport/body_encoder.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; +import 'dart:io'; + +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) { + headers['Content-Encoding'] = 'gzip'; + body = gzip.encode(body); + } + return body; +} diff --git a/dart/lib/src/transport/body_encoder_browser.dart b/dart/lib/src/transport/body_encoder_browser.dart new file mode 100644 index 0000000000..5e587c8bf6 --- /dev/null +++ b/dart/lib/src/transport/body_encoder_browser.dart @@ -0,0 +1,12 @@ +import 'dart:convert'; + +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)); + return body; +} diff --git a/dart/lib/src/transport/noop_transport.dart b/dart/lib/src/transport/noop_transport.dart new file mode 100644 index 0000000000..1488b89258 --- /dev/null +++ b/dart/lib/src/transport/noop_transport.dart @@ -0,0 +1,11 @@ +import 'dart:async'; + +import 'package:sentry/sentry.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 new file mode 100644 index 0000000000..3fcb51f62b --- /dev/null +++ b/dart/lib/src/transport/transport.dart @@ -0,0 +1,114 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:meta/meta.dart'; +import 'package:sentry/src/utils.dart'; + +import '../protocol.dart'; +import '../sentry_options.dart'; +import 'body_encoder_browser.dart' if (dart.library.io) 'body_encoder.dart'; + +typedef BodyEncoder = List Function( + Map data, + Map headers, { + bool compressPayload, +}); + +/// 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(); + } +} + +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/utils.dart b/dart/lib/src/utils.dart index d8ce547bbe..9cc4952dda 100644 --- a/dart/lib/src/utils.dart +++ b/dart/lib/src/utils.dart @@ -39,3 +39,6 @@ String formatDateAsIso8601WithSecondPrecision(DateTime date) { } return iso; } + +/// helper to detect a browser context +const isWeb = identical(1.0, 1); diff --git a/dart/lib/src/version.dart b/dart/lib/src/version.dart index a495d59cb1..820cd11bdf 100644 --- a/dart/lib/src/version.dart +++ b/dart/lib/src/version.dart @@ -8,19 +8,28 @@ /// This library contains Sentry.io SDK constants used by this package. library version; +import 'utils.dart'; + /// The SDK version reported to Sentry.io in the submitted events. const String sdkVersion = '4.0.0'; +String get sdkName => isWeb ? _browserSdkName : _ioSdkName; + /// The default SDK name reported to Sentry.io in the submitted events. -const String sdkName = 'sentry.dart'; +const String _ioSdkName = 'sentry.dart'; /// The SDK name for web projects reported to Sentry.io in the submitted events. -const String browserSdkName = 'sentry.dart.browser'; +const String _browserSdkName = 'sentry.dart.browser'; + +/// The name of the SDK platform reported to Sentry.io in the submitted events. +/// +/// Used for IO version. +String get sdkPlatform => isWeb ? _browserPlatform : _ioSdkPlatform; /// The name of the SDK platform reported to Sentry.io in the submitted events. /// /// Used for IO version. -const String sdkPlatform = 'dart'; +const String _ioSdkPlatform = 'dart'; /// Used to report browser Stacktrace to sentry. -const String browserPlatform = 'javascript'; +const String _browserPlatform = 'javascript'; diff --git a/dart/test/event_test.dart b/dart/test/event_test.dart index 81abef84c8..9d4d76c804 100644 --- a/dart/test/event_test.dart +++ b/dart/test/event_test.dart @@ -4,6 +4,7 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/stack_trace.dart'; +import 'package:sentry/src/utils.dart'; import 'package:test/test.dart'; void main() { @@ -26,15 +27,20 @@ void main() { }); test('$Sdk serializes', () { final event = SentryEvent( - eventId: SentryId.empty(), - timestamp: DateTime.utc(2019), - sdk: Sdk( - name: 'sentry.dart.flutter', - version: '4.3.2', - integrations: ['integration'], - packages: [Package('npm:@sentry/javascript', '1.3.4')])); + eventId: SentryId.empty(), + timestamp: DateTime.utc(2019), + platform: sdkPlatform, + sdk: Sdk( + name: 'sentry.dart.flutter', + version: '4.3.2', + integrations: ['integration'], + packages: [ + Package('npm:@sentry/javascript', '1.3.4'), + ], + ), + ); expect(event.toJson(), { - 'platform': 'dart', + 'platform': isWeb ? 'javascript' : 'dart', 'event_id': '00000000000000000000000000000000', 'timestamp': '2019-01-01T00:00:00', 'sdk': { @@ -67,6 +73,7 @@ void main() { SentryEvent( eventId: SentryId.empty(), timestamp: timestamp, + platform: sdkPlatform, message: Message( 'test-message 1 2', template: 'test-message %d %d', @@ -89,10 +96,10 @@ void main() { breadcrumbs: breadcrumbs, ).toJson(), { - 'platform': 'dart', + 'platform': isWeb ? 'javascript' : 'dart', 'event_id': '00000000000000000000000000000000', 'timestamp': '2019-01-01T00:00:00', - 'sdk': {'version': sdkVersion, 'name': 'sentry.dart'}, + 'sdk': {'version': sdkVersion, 'name': sdkName}, 'message': { 'formatted': 'test-message 1 2', 'message': 'test-message %d %d', diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index e50a8f0abf..57ca1da64d 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -4,6 +4,8 @@ import 'package:sentry/src/protocol.dart'; class MockSentryClient extends Mock implements SentryClient {} +class MockTransport extends Mock implements Transport {} + final fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; final fakeException = Exception('Error'); diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart new file mode 100644 index 0000000000..80a7843855 --- /dev/null +++ b/dart/test/sentry_client_test.dart @@ -0,0 +1,44 @@ +import 'package:mockito/mockito.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; + +void main() { + group('SentryClient sampling', () { + SentryOptions options; + Transport transport; + + setUp(() { + options = SentryOptions(dsn: fakeDsn); + transport = MockTransport(); + }); + + test('captures event, sample rate is 100% enabled', () { + options.sampleRate = 1.0; + final client = SentryClient(options); + options.transport = transport; + client.captureEvent(fakeEvent); + + verify(transport.send(any)).called(1); + }); + + test('do not capture event, sample rate is 0% disabled', () { + options.sampleRate = 0.0; + final client = SentryClient(options); + options.transport = transport; + client.captureEvent(fakeEvent); + + verifyNever(transport.send(any)); + }); + + test('captures event, sample rate is null, disabled', () { + options.sampleRate = null; + final client = SentryClient(options); + options.transport = transport; + client.captureEvent(fakeEvent); + + verify(transport.send(any)).called(1); + }); + }); +} diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index 1ffd29a6cc..f438ca1a8e 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -29,15 +29,16 @@ void testHeaders( 'Content-Type': 'application/json', 'X-Sentry-Auth': 'Sentry sentry_version=6, ' 'sentry_client=$sdkName/$sdkVersion, ' - 'sentry_timestamp=${fakeClockProvider().millisecondsSinceEpoch}, ' - 'sentry_key=public' + 'sentry_key=public, ' }; if (withSecret) { - expectedHeaders['X-Sentry-Auth'] += ', ' - 'sentry_secret=secret'; + expectedHeaders['X-Sentry-Auth'] += 'sentry_secret=secret, '; } + expectedHeaders['X-Sentry-Auth'] += + 'sentry_timestamp=${fakeClockProvider().millisecondsSinceEpoch}'; + if (withUserAgent) { expectedHeaders['User-Agent'] = '$sdkName/$sdkVersion'; } @@ -69,20 +70,18 @@ Future testCaptureException( fail('Unexpected request on ${request.method} ${request.url} in HttpMock'); }); + final options = SentryOptions( + dsn: testDsn, + httpClient: httpMock, + clock: fakeClockProvider, + compressPayload: compressPayload, + ) + ..serverName = 'test.server.com' + ..release = '1.2.3' + ..environment = 'staging'; + var sentryId = SentryId.empty(); - final client = SentryClient( - SentryOptions( - dsn: testDsn, - httpClient: httpMock, - clock: fakeClockProvider, - compressPayload: compressPayload, - environmentAttributes: SentryEvent( - serverName: 'test.server.com', - release: '1.2.3', - environment: 'staging', - ), - ), - ); + final client = SentryClient(options); try { throw ArgumentError('Test error'); @@ -91,14 +90,14 @@ Future testCaptureException( expect('$sentryId', 'testeventid'); } - expect(postUri, client.postUri); + expect(postUri, options.transport.dsn.postUri); testHeaders( headers, fakeClockProvider, compressPayload: compressPayload, withUserAgent: !isWeb, - sdkName: isWeb ? browserSdkName : sdkName, + sdkName: sdkName, ); Map data; @@ -141,11 +140,10 @@ Future testCaptureException( expect(topFrame['function'], 'Object.wrapException'); expect(data, { - 'project': '1', 'event_id': sentryId.toString(), 'timestamp': '2017-01-02T00:00:00', 'platform': 'javascript', - 'sdk': {'version': sdkVersion, 'name': 'sentry.dart'}, + 'sdk': {'version': sdkVersion, 'name': sdkName}, 'server_name': 'test.server.com', 'release': '1.2.3', 'environment': 'staging', @@ -159,7 +157,6 @@ Future testCaptureException( expect(topFrame['function'], 'testCaptureException'); expect(data, { - 'project': '1', 'event_id': sentryId.toString(), 'timestamp': '2017-01-02T00:00:00', 'platform': 'dart', @@ -181,41 +178,53 @@ Future testCaptureException( void runTest({Codec, List> gzip, bool isWeb = false}) { test('can parse DSN', () async { - final client = SentryClient(SentryOptions(dsn: testDsn)); - expect(client.dsnUri, Uri.parse(testDsn)); - expect(client.postUri, 'https://sentry.example.com/api/1/store/'); - expect(client.publicKey, 'public'); - expect(client.secretKey, 'secret'); - expect(client.projectId, '1'); + final options = SentryOptions(dsn: testDsn); + final client = SentryClient(options); + expect(options.transport.dsn.uri, Uri.parse(testDsn)); + expect(options.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'); await client.close(); }); test('can parse DSN without secret', () async { - final client = SentryClient(SentryOptions(dsn: _testDsnWithoutSecret)); - expect(client.dsnUri, Uri.parse(_testDsnWithoutSecret)); - expect(client.postUri, 'https://sentry.example.com/api/1/store/'); - expect(client.publicKey, 'public'); - expect(client.secretKey, null); - expect(client.projectId, '1'); + final options = SentryOptions(dsn: _testDsnWithoutSecret); + final client = SentryClient(options); + expect(options.transport.dsn.uri, Uri.parse(_testDsnWithoutSecret)); + expect(options.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'); await client.close(); }); test('can parse DSN with path', () async { - final client = SentryClient(SentryOptions(dsn: _testDsnWithPath)); - expect(client.dsnUri, Uri.parse(_testDsnWithPath)); - expect(client.postUri, 'https://sentry.example.com/path/api/1/store/'); - expect(client.publicKey, 'public'); - expect(client.secretKey, 'secret'); - expect(client.projectId, '1'); + final options = SentryOptions(dsn: _testDsnWithPath); + final client = SentryClient(options); + expect(options.transport.dsn.uri, Uri.parse(_testDsnWithPath)); + expect( + options.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'); await client.close(); }); test('can parse DSN with port', () async { - final client = SentryClient(SentryOptions(dsn: _testDsnWithPort)); - expect(client.dsnUri, Uri.parse(_testDsnWithPort)); - expect(client.postUri, 'https://sentry.example.com:8888/api/1/store/'); - expect(client.publicKey, 'public'); - expect(client.secretKey, 'secret'); - expect(client.projectId, '1'); + final options = SentryOptions(dsn: _testDsnWithPort); + final client = SentryClient(options); + expect(options.transport.dsn.uri, Uri.parse(_testDsnWithPort)); + expect( + options.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'); await client.close(); }); test('sends client auth header without secret', () async { @@ -238,12 +247,10 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { httpClient: httpMock, clock: fakeClockProvider, compressPayload: false, - environmentAttributes: SentryEvent( - serverName: 'test.server.com', - release: '1.2.3', - environment: 'staging', - ), - ), + ) + ..serverName = 'test.server.com' + ..release = '1.2.3' + ..environment = 'staging', ); try { @@ -260,7 +267,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { withUserAgent: !isWeb, compressPayload: false, withSecret: false, - sdkName: isWeb ? browserSdkName : sdkName, + sdkName: sdkName, ); await client.close(); @@ -295,12 +302,10 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { httpClient: httpMock, clock: fakeClockProvider, compressPayload: false, - environmentAttributes: SentryEvent( - serverName: 'test.server.com', - release: '1.2.3', - environment: 'staging', - ), - ), + ) + ..serverName = 'test.server.com' + ..release = '1.2.3' + ..environment = 'staging', ); try { @@ -333,31 +338,30 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { }); const clientUserContext = User( - id: 'client_user', - username: 'username', - email: 'email@email.com', - ipAddress: '127.0.0.1'); + id: 'client_user', + username: 'username', + email: 'email@email.com', + ipAddress: '127.0.0.1', + ); const eventUserContext = User( - id: 'event_user', - username: 'username', - email: 'email@email.com', - ipAddress: '127.0.0.1', - extras: {'foo': 'bar'}); - - final client = SentryClient( - SentryOptions( - dsn: testDsn, - httpClient: httpMock, - clock: fakeClockProvider, - compressPayload: false, - environmentAttributes: SentryEvent( - serverName: 'test.server.com', - release: '1.2.3', - environment: 'staging', - ), - ), + id: 'event_user', + username: 'username', + email: 'email@email.com', + ipAddress: '127.0.0.1', + extras: {'foo': 'bar'}, ); - client.userContext = clientUserContext; + + final options = SentryOptions( + dsn: testDsn, + httpClient: httpMock, + clock: fakeClockProvider, + compressPayload: false, + ) + ..serverName = 'test.server.com' + ..release = '1.2.3' + ..environment = 'staging'; + + final client = SentryClient(options); try { throw ArgumentError('Test error'); @@ -373,9 +377,13 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { stackTrace: stackTrace, userContext: eventUserContext, ); - await client.captureEvent(eventWithoutContext); + await client.captureEvent(eventWithoutContext, + scope: Scope(options)..user = clientUserContext); expect(loggedUserId, clientUserContext.id); - await client.captureEvent(eventWithContext); + await client.captureEvent( + eventWithContext, + scope: Scope(options)..user = clientUserContext, + ); expect(loggedUserId, eventUserContext.id); } diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 4b12dd8ca7..7c9dc8f024 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -40,7 +40,8 @@ Future main() async { stackTrace: stackTrace, // release is required on Web to match the source maps release: _release, - sdk: _sentry.sdk, + + // sdk: const Sdk(name: sdkName, version: sdkVersion), ); await _sentry.captureEvent(event); }); From c301ec696a1b66e6cfdb46c7bd9a236255a788b2 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Sat, 24 Oct 2020 09:05:10 +0200 Subject: [PATCH 13/34] Ref: execute before send callback (#128) * ref: execute before send * changelog * fix test name --- CHANGELOG.md | 1 + dart/lib/src/client.dart | 26 ++++++++++++++++++- dart/test/sentry_client_test.dart | 42 +++++++++++++++++++++++++------ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 441bb07705..e1176fd873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Ref: Remove stackFrameFilter in favor of beforeSendCallback #125 - Ref: added Transport #123 - Feat: apply sample rate +- Ref: execute before send callback # `package:sentry` changelog diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index 9f100fda14..c10b666928 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -35,15 +35,22 @@ abstract class SentryClient { 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 Future.value(SentryId.empty()); + return emptyFuture; } event = _applyScope(event: event, scope: scope); + // dropped by scope event processors + if (event == null) { + return emptyFuture; + } + // TODO create eventProcessors ? event = event.copyWith( serverName: _options.serverName, @@ -52,6 +59,21 @@ abstract class SentryClient { platform: event.platform ?? sdkPlatform, ); + if (_options.beforeSendCallback != null) { + try { + event = _options.beforeSendCallback(event, hint); + } catch (err) { + _options.logger( + SentryLevel.error, + 'The BeforeSend callback threw an exception', + ); + } + if (event == null) { + _options.logger(SentryLevel.debug, 'Event was dropped by a processor'); + return emptyFuture; + } + } + return _options.transport.send(event); } @@ -160,6 +182,8 @@ abstract class SentryClient { if (scope.level != null) { event = event.copyWith(level: scope.level); } + + // TODO: execute scope event processors } return event; } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 80a7843855..d6801cc753 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -7,38 +7,64 @@ import 'mocks.dart'; void main() { group('SentryClient sampling', () { SentryOptions options; - Transport transport; setUp(() { options = SentryOptions(dsn: fakeDsn); - transport = MockTransport(); + options.transport = MockTransport(); }); test('captures event, sample rate is 100% enabled', () { options.sampleRate = 1.0; final client = SentryClient(options); - options.transport = transport; client.captureEvent(fakeEvent); - verify(transport.send(any)).called(1); + verify(options.transport.send(any)).called(1); }); test('do not capture event, sample rate is 0% disabled', () { options.sampleRate = 0.0; final client = SentryClient(options); - options.transport = transport; client.captureEvent(fakeEvent); - verifyNever(transport.send(any)); + verifyNever(options.transport.send(any)); }); test('captures event, sample rate is null, disabled', () { options.sampleRate = null; final client = SentryClient(options); - options.transport = transport; client.captureEvent(fakeEvent); - verify(transport.send(any)).called(1); + verify(options.transport.send(any)).called(1); + }); + }); + + group('SentryClient before send', () { + SentryOptions options; + + setUp(() { + options = SentryOptions(dsn: fakeDsn); + options.transport = MockTransport(); + }); + + test('before send drops event', () { + options.beforeSendCallback = beforeSendCallbackDropEvent; + final client = SentryClient(options); + client.captureEvent(fakeEvent); + + verifyNever(options.transport.send(any)); + }); + + test('before send returns an event and event is captured', () { + options.beforeSendCallback = beforeSendCallback; + final client = SentryClient(options); + client.captureEvent(fakeEvent); + + verify(options.transport.send(any)).called(1); }); }); } + +SentryEvent beforeSendCallbackDropEvent(SentryEvent event, dynamic hint) => + null; + +SentryEvent beforeSendCallback(SentryEvent event, dynamic hint) => event; From 1794f048a02099096eae0b69c2fca6f3ae726803 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Mon, 26 Oct 2020 10:06:44 +0100 Subject: [PATCH 14/34] feat: addBreadcrumb to the Sentry static API (#133) --- CHANGELOG.md | 1 + dart/example/event_example.dart | 4 +- dart/lib/src/hub.dart | 25 ++++++++++++ dart/lib/src/noop_hub.dart | 5 +++ dart/lib/src/protocol/breadcrumb.dart | 6 +-- dart/lib/src/sentry.dart | 5 +++ dart/test/event_test.dart | 11 ++++-- dart/test/hub_test.dart | 11 ++++++ dart/test/mocks.dart | 4 +- dart/test/scope_test.dart | 55 +++++++++++++++++++++------ 10 files changed, 107 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1176fd873..bcf0a567b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Ref: added Transport #123 - Feat: apply sample rate - Ref: execute before send callback +- Feat: addBreadcrumb on Static API # `package:sentry` changelog diff --git a/dart/example/event_example.dart b/dart/example/event_example.dart index a02c92a9b3..8848782ed3 100644 --- a/dart/example/event_example.dart +++ b/dart/example/event_example.dart @@ -18,7 +18,9 @@ final event = SentryEvent( ipAddress: '127.0.0.1', extras: {'first-sign-in': '2020-01-01'}), breadcrumbs: [ - Breadcrumb('UI Lifecycle', DateTime.now().toUtc(), + Breadcrumb( + message: 'UI Lifecycle', + timestamp: DateTime.now().toUtc(), category: 'ui.lifecycle', type: 'navigation', data: {'screen': 'MainActivity', 'state': 'created'}, diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 2244149fe3..ee6b5cf440 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -191,6 +191,31 @@ class Hub { return sentryId; } + /// Adds a breacrumb to the current Scope + void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) { + if (!_isEnabled) { + _options.logger( + SentryLevel.warning, + "Instance is disabled and this 'addBreadcrumb' call is a no-op.", + ); + } else if (crumb == null) { + _options.logger( + SentryLevel.warning, + 'addBreadcrumb called with null parameter.', + ); + } else { + final item = _peek(); + if (item != null) { + item.scope.addBreadcrumb(crumb, hint: hint); + } else { + _options.logger( + SentryLevel.fatal, + 'Stack peek was null when addBreadcrumb', + ); + } + } + } + /// Binds a different client to the hub void bindClient(SentryClient client) { if (!_isEnabled) { diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index f8e528430e..3a79f71211 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:sentry/src/protocol/breadcrumb.dart'; + import 'client.dart'; import 'hub.dart'; import 'protocol/sentry_event.dart'; @@ -54,4 +56,7 @@ class NoOpHub implements Hub { @override SentryId get lastEventId => SentryId.empty(); + + @override + void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) {} } diff --git a/dart/lib/src/protocol/breadcrumb.dart b/dart/lib/src/protocol/breadcrumb.dart index a458ba7c69..7769cf33cb 100644 --- a/dart/lib/src/protocol/breadcrumb.dart +++ b/dart/lib/src/protocol/breadcrumb.dart @@ -19,14 +19,14 @@ import 'sentry_level.dart'; /// * https://docs.sentry.io/development/sdk-dev/event-payloads/breadcrumbs/ class Breadcrumb { /// Creates a breadcrumb that can be attached to an [Event]. - const Breadcrumb( + Breadcrumb({ this.message, - this.timestamp, { + DateTime timestamp, this.category, this.data, this.level = SentryLevel.info, this.type, - }) : assert(timestamp != null); + }) : timestamp = timestamp ?? getUtcDateTime(); /// Describes the breadcrumb. /// diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 18856f6183..495c9d1db3 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -94,6 +94,11 @@ class Sentry { /// Check if the current Hub is enabled/active. static bool get isEnabled => currentHub.isEnabled; + /// Adds a breacrumb to the current Scope + static void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) { + currentHub.addBreadcrumb(crumb, hint: hint); + } + static bool _setDefaultConfiguration(SentryOptions options) { if (options.dsn == null) { throw ArgumentError.notNull( diff --git a/dart/test/event_test.dart b/dart/test/event_test.dart index 9d4d76c804..e5aa283587 100644 --- a/dart/test/event_test.dart +++ b/dart/test/event_test.dart @@ -12,8 +12,8 @@ void main() { test('$Breadcrumb serializes', () { expect( Breadcrumb( - 'example log', - DateTime.utc(2019), + message: 'example log', + timestamp: DateTime.utc(2019), level: SentryLevel.debug, category: 'test', ).toJson(), @@ -63,8 +63,11 @@ void main() { extras: {'foo': 'bar'}); final breadcrumbs = [ - Breadcrumb('test log', timestamp, - level: SentryLevel.debug, category: 'test'), + Breadcrumb( + message: 'test log', + timestamp: timestamp, + level: SentryLevel.debug, + category: 'test'), ]; final error = StateError('test-error'); diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 24527b7364..5c164f1d52 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -121,6 +121,17 @@ void main() { true, ); }); + + test('should add breadcrumb to current Scope', () { + hub.configureScope((Scope scope) { + expect(0, scope..breadcrumbs.length); + }); + hub.addBreadcrumb(Breadcrumb(message: 'test')); + hub.configureScope((Scope scope) { + expect(1, scope..breadcrumbs.length); + expect('test', scope..breadcrumbs.first.message); + }); + }); }); group('Hub Client', () { diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index 57ca1da64d..6d50a7ecb9 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -32,7 +32,9 @@ final fakeEvent = SentryEvent( ipAddress: '127.0.0.1', extras: {'first-sign-in': '2020-01-01'}), breadcrumbs: [ - Breadcrumb('UI Lifecycle', DateTime.now().toUtc(), + Breadcrumb( + message: 'UI Lifecycle', + timestamp: DateTime.now().toUtc(), category: 'ui.lifecycle', type: 'navigation', data: {'screen': 'MainActivity', 'state': 'created'}, diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index f539930405..f48152d07d 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -42,7 +42,10 @@ void main() { test('adds $Breadcrumb', () { final sut = fixture.getSut(); - final breadcrumb = Breadcrumb('test log', DateTime.utc(2019)); + final breadcrumb = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); sut.addBreadcrumb(breadcrumb); expect(sut.breadcrumbs.last, breadcrumb); @@ -53,7 +56,10 @@ void main() { beforeBreadcrumbCallback: fixture.beforeBreadcrumbCallback, ); - final breadcrumb = Breadcrumb('test log', DateTime.utc(2019)); + final breadcrumb = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); sut.addBreadcrumb(breadcrumb); expect(sut.breadcrumbs.length, 0); @@ -71,9 +77,18 @@ void main() { 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)); + final breadcrumb1 = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); + final breadcrumb2 = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); + final breadcrumb3 = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); sut.addBreadcrumb(breadcrumb1); sut.addBreadcrumb(breadcrumb2); sut.addBreadcrumb(breadcrumb3); @@ -84,9 +99,18 @@ void main() { 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)); + final breadcrumb1 = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); + final breadcrumb2 = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); + final breadcrumb3 = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); sut.addBreadcrumb(breadcrumb1); sut.addBreadcrumb(breadcrumb2); sut.addBreadcrumb(breadcrumb3); @@ -100,7 +124,10 @@ void main() { final maxBreadcrumbs = 0; final sut = fixture.getSut(maxBreadcrumbs: maxBreadcrumbs); - final breadcrumb1 = Breadcrumb('test log', DateTime.utc(2019)); + final breadcrumb1 = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); sut.addBreadcrumb(breadcrumb1); expect(sut.breadcrumbs.length, maxBreadcrumbs); @@ -109,7 +136,10 @@ void main() { test('clears $Breadcrumb list', () { final sut = fixture.getSut(); - final breadcrumb1 = Breadcrumb('test log', DateTime.utc(2019)); + final breadcrumb1 = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); sut.addBreadcrumb(breadcrumb1); sut.clear(); @@ -153,7 +183,10 @@ void main() { test('clears $Scope', () { final sut = fixture.getSut(); - final breadcrumb1 = Breadcrumb('test log', DateTime.utc(2019)); + final breadcrumb1 = Breadcrumb( + message: 'test log', + timestamp: DateTime.utc(2019), + ); sut.addBreadcrumb(breadcrumb1); sut.level = SentryLevel.debug; From e7db5d1223113c70b681301889824db35f0709f3 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Mon, 26 Oct 2020 10:13:34 +0100 Subject: [PATCH 15/34] Feat: add lastEventId to the Sentry static API (#134) --- CHANGELOG.md | 1 + dart/lib/src/hub.dart | 2 +- dart/lib/src/sentry.dart | 3 +++ dart/test/hub_test.dart | 12 ++++++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf0a567b7..3b35b89037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Ref: added Transport #123 - Feat: apply sample rate - Ref: execute before send callback +- Feat: add lastEventId to the Sentry static API - Feat: addBreadcrumb on Static API # `package:sentry` changelog diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index ee6b5cf440..861dfd918b 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -50,7 +50,7 @@ class Hub { SentryId _lastEventId = SentryId.empty(); - /// Last event id recorded in the current scope + /// Last event id recorded by the Hub SentryId get lastEventId => _lastEventId; /// Captures the event. diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 495c9d1db3..fb1e54e952 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -94,6 +94,9 @@ class Sentry { /// Check if the current Hub is enabled/active. static bool get isEnabled => currentHub.isEnabled; + /// Last event id recorded by the current Hub + static SentryId get lastEventId => currentHub.lastEventId; + /// Adds a breacrumb to the current Scope static void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) { currentHub.addBreadcrumb(crumb, hint: hint); diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 5c164f1d52..42ee5057b1 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -83,6 +83,18 @@ void main() { ), ).called(1); }); + + test('should save the lastEventId', () async { + final event = SentryEvent(); + final eventId = event.eventId; + when(client.captureEvent( + event, + scope: anyNamed('scope'), + hint: anyNamed('hint'), + )).thenAnswer((_) => Future.value(event.eventId)); + final returnedId = await hub.captureEvent(event); + expect(eventId.toString(), returnedId.toString()); + }); }); group('Hub scope', () { From dc962ae7d4ad586133f4f5b185621e05b4eadf10 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Mon, 26 Oct 2020 12:52:54 +0100 Subject: [PATCH 16/34] Fix: Breadcrumb data should accept serializable types and not only String values (#139) --- CHANGELOG.md | 1 + dart/lib/src/protocol/breadcrumb.dart | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b35b89037..4e8e5c3587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Ref: execute before send callback - Feat: add lastEventId to the Sentry static API - Feat: addBreadcrumb on Static API +- Fix: Breadcrumb data should accept serializable types and not only String values # `package:sentry` changelog diff --git a/dart/lib/src/protocol/breadcrumb.dart b/dart/lib/src/protocol/breadcrumb.dart index 7769cf33cb..03c41dfb44 100644 --- a/dart/lib/src/protocol/breadcrumb.dart +++ b/dart/lib/src/protocol/breadcrumb.dart @@ -47,7 +47,7 @@ class Breadcrumb { /// See also: /// /// * https://docs.sentry.io/development/sdk-dev/event-payloads/breadcrumbs/#breadcrumb-types - final Map data; + final Map data; /// Severity of the breadcrumb. /// @@ -85,7 +85,7 @@ class Breadcrumb { json['category'] = category; } if (data != null && data.isNotEmpty) { - json['data'] = Map.of(data); + json['data'] = data; } if (level != null) { json['level'] = level.name; From b9cd68bfc1c5f205d78ce01a53c6d1d948affae1 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Mon, 26 Oct 2020 12:53:39 +0100 Subject: [PATCH 17/34] Fix: NoOp encode for Web (#138) --- CHANGELOG.md | 1 + dart/lib/src/transport/body_encoder.dart | 17 ------------- .../src/transport/body_encoder_browser.dart | 12 ---------- dart/lib/src/transport/encode.dart | 7 ++++++ dart/lib/src/transport/noop_encode.dart | 2 ++ dart/lib/src/transport/transport.dart | 24 ++++++++++++------- 6 files changed, 26 insertions(+), 37 deletions(-) delete mode 100644 dart/lib/src/transport/body_encoder.dart delete mode 100644 dart/lib/src/transport/body_encoder_browser.dart create mode 100644 dart/lib/src/transport/encode.dart create mode 100644 dart/lib/src/transport/noop_encode.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e8e5c3587..77d5e01f67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Ref: execute before send callback - Feat: add lastEventId to the Sentry static API - Feat: addBreadcrumb on Static API +- Fix: NoOp encode for Web - Fix: Breadcrumb data should accept serializable types and not only String values # `package:sentry` changelog diff --git a/dart/lib/src/transport/body_encoder.dart b/dart/lib/src/transport/body_encoder.dart deleted file mode 100644 index eb46bfc16d..0000000000 --- a/dart/lib/src/transport/body_encoder.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -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) { - headers['Content-Encoding'] = 'gzip'; - body = gzip.encode(body); - } - return body; -} diff --git a/dart/lib/src/transport/body_encoder_browser.dart b/dart/lib/src/transport/body_encoder_browser.dart deleted file mode 100644 index 5e587c8bf6..0000000000 --- a/dart/lib/src/transport/body_encoder_browser.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:convert'; - -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)); - return body; -} diff --git a/dart/lib/src/transport/encode.dart b/dart/lib/src/transport/encode.dart new file mode 100644 index 0000000000..03256130f9 --- /dev/null +++ b/dart/lib/src/transport/encode.dart @@ -0,0 +1,7 @@ +import 'dart:io'; + +/// Encodes the body using Gzip compression +List compressBody(List body, Map headers) { + headers['Content-Encoding'] = 'gzip'; + return gzip.encode(body); +} diff --git a/dart/lib/src/transport/noop_encode.dart b/dart/lib/src/transport/noop_encode.dart new file mode 100644 index 0000000000..73f92b2fa0 --- /dev/null +++ b/dart/lib/src/transport/noop_encode.dart @@ -0,0 +1,2 @@ +/// gzip compression is not available on browser +List compressBody(List body, Map headers) => body; diff --git a/dart/lib/src/transport/transport.dart b/dart/lib/src/transport/transport.dart index 3fcb51f62b..c0bf81641a 100644 --- a/dart/lib/src/transport/transport.dart +++ b/dart/lib/src/transport/transport.dart @@ -6,13 +6,7 @@ import 'package:sentry/src/utils.dart'; import '../protocol.dart'; import '../sentry_options.dart'; -import 'body_encoder_browser.dart' if (dart.library.io) 'body_encoder.dart'; - -typedef BodyEncoder = List Function( - Map data, - Map headers, { - bool compressPayload, -}); +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 { @@ -43,7 +37,7 @@ class Transport { Future send(SentryEvent event) async { final data = event.toJson(origin: _origin); - final body = bodyEncoder( + final body = _bodyEncoder( data, _headers, compressPayload: _options.compressPayload, @@ -62,6 +56,20 @@ class Transport { 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 { From 7b818907946bcfa3c1f3d8e373777477b7ce93f5 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Mon, 26 Oct 2020 14:26:55 +0100 Subject: [PATCH 18/34] Fix: execute Integrations on Hub creation (#136) --- CHANGELOG.md | 1 + dart/lib/src/hub.dart | 7 ++++ dart/lib/src/hub_adapter.dart | 74 +++++++++++++++++++++++++++++++++++ dart/lib/src/sentry.dart | 57 +++++++++++++++------------ dart/test/hub_test.dart | 10 +++++ 5 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 dart/lib/src/hub_adapter.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 77d5e01f67..be2abfe689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Ref: execute before send callback - Feat: add lastEventId to the Sentry static API - Feat: addBreadcrumb on Static API +- Fix: Integrations are executed on Hub creation - Fix: NoOp encode for Web - Fix: Breadcrumb data should accept serializable types and not only String values diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 861dfd918b..cda6add859 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:collection'; +import 'package:sentry/src/hub_adapter.dart'; + import 'client.dart'; import 'noop_client.dart'; import 'protocol.dart'; @@ -31,6 +33,11 @@ class Hub { Hub._(SentryOptions options) : _options = options { _stack.add(_StackItem(_getClient(_options), Scope(_options))); _isEnabled = true; + + // execute integrations + options.integrations.forEach((integration) { + integration(HubAdapter(), _options); + }); } static void _validateOptions(SentryOptions options) { diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart new file mode 100644 index 0000000000..9f5fc1b25e --- /dev/null +++ b/dart/lib/src/hub_adapter.dart @@ -0,0 +1,74 @@ +import 'package:sentry/src/protocol/sentry_level.dart'; +import 'package:sentry/src/protocol/sentry_id.dart'; +import 'package:sentry/src/protocol/sentry_event.dart'; +import 'package:sentry/src/protocol/breadcrumb.dart'; +import 'package:sentry/src/client.dart'; +import 'package:sentry/src/sentry.dart'; +import 'hub.dart'; +import 'dart:async'; + +/// Hub adapter to make Integrations testable +class HubAdapter implements Hub { + HubAdapter._(); + + static final HubAdapter _instance = HubAdapter._(); + + factory HubAdapter() { + return _instance; + } + + @override + void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) => + Sentry.addBreadcrumb(crumb, hint: hint); + + @override + void bindClient(SentryClient client) => Sentry.bindClient(client); + + @override + Future captureEvent(SentryEvent event, {dynamic hint}) => + Sentry.captureEvent(event, hint: hint); + + @override + Future captureException( + dynamic throwable, { + dynamic stackTrace, + dynamic hint, + }) => + Sentry.captureException( + throwable, + stackTrace: stackTrace, + hint: hint, + ); + + @override + Future captureMessage( + String message, { + SentryLevel level = SentryLevel.info, + String template, + List params, + dynamic hint, + }) => + Sentry.captureMessage( + message, + level: level, + template: template, + params: params, + hint: hint, + ); + + @override + Hub clone() => Sentry.clone(); + + @override + void close() => Sentry.close(); + + @override + void configureScope(ScopeCallback callback) => + Sentry.configureScope(callback); + + @override + bool get isEnabled => Sentry.isEnabled; + + @override + SentryId get lastEventId => Sentry.lastEventId; +} diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index fb1e54e952..d44d80fbc1 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -51,38 +51,35 @@ class Sentry { static Future captureEvent( SentryEvent event, { dynamic hint, - }) async { - return currentHub.captureEvent(event, hint: hint); - } + }) async => + currentHub.captureEvent(event, hint: hint); /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. static Future captureException( - dynamic error, { + dynamic throwable, { dynamic stackTrace, dynamic hint, - }) async { - return currentHub.captureException( - error, - stackTrace: stackTrace, - hint: hint, - ); - } + }) async => + currentHub.captureException( + throwable, + stackTrace: stackTrace, + hint: hint, + ); - Future captureMessage( + static Future captureMessage( String message, { SentryLevel level, String template, List params, dynamic hint, - }) async { - return currentHub.captureMessage( - message, - level: level, - template: template, - params: params, - hint: hint, - ); - } + }) async => + currentHub.captureMessage( + message, + level: level, + template: template, + params: params, + hint: hint, + ); /// Close the client SDK static void close() { @@ -98,20 +95,32 @@ class Sentry { static SentryId get lastEventId => currentHub.lastEventId; /// Adds a breacrumb to the current Scope - static void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) { - currentHub.addBreadcrumb(crumb, hint: hint); - } + static void addBreadcrumb(Breadcrumb crumb, {dynamic hint}) => + currentHub.addBreadcrumb(crumb, hint: hint); + + /// Configures the scope through the callback. + static void configureScope(ScopeCallback callback) => + currentHub.configureScope(callback); + + /// Clones the current Hub + static Hub clone() => currentHub.clone(); + + /// Binds a different client to the current hub + static void bindClient(SentryClient client) => currentHub.bindClient(client); static bool _setDefaultConfiguration(SentryOptions options) { + // if DSN is null, let's crash the App. if (options.dsn == null) { throw ArgumentError.notNull( 'DSN is required. Use empty string to disable SDK.'); } + // if the DSN is empty, let's disable the SDK if (options.dsn.isEmpty) { close(); return false; } + // if logger os NoOp, let's set a logger that prints on the console if (options.debug && options.logger == noOpLogger) { options.logger = dartLogger; } diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 42ee5057b1..409e69a584 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -3,6 +3,7 @@ import 'package:mockito/mockito.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/hub.dart'; import 'package:test/test.dart'; +import 'dart:async'; import 'mocks.dart'; @@ -95,6 +96,15 @@ void main() { final returnedId = await hub.captureEvent(event); expect(eventId.toString(), returnedId.toString()); }); + + test('should install integrations', () { + var called = false; + void integration(Hub hub, SentryOptions options) => called = true; + options.addIntegration(integration); + Hub(options); + + expect(called, true); + }); }); group('Hub scope', () { From b1761f4615658b4cb17708f3acbe2e6ee55fd9e9 Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Tue, 27 Oct 2020 13:13:25 +0100 Subject: [PATCH 19/34] unified api fixes and add web example (#137) --- .gitignore | 2 + CHANGELOG.md | 1 + dart/example/event_example.dart | 56 +++++----- dart/example/main.dart | 74 ++++++++++--- dart/example_web/.gitignore | 9 ++ dart/example_web/README.md | 9 ++ dart/example_web/pubspec.yaml | 16 +++ dart/example_web/web/event.dart | 77 ++++++++++++++ dart/example_web/web/favicon.ico | Bin 0 -> 3559 bytes dart/example_web/web/index.html | 49 +++++++++ dart/example_web/web/main.dart | 114 ++++++++++++++++++++ dart/example_web/web/styles.css | 14 +++ dart/lib/src/browser_client.dart | 4 - dart/lib/src/client.dart | 1 - dart/lib/src/sentry.dart | 2 +- dart/lib/src/sentry_options.dart | 26 +++-- dart/test/hub_test.dart | 3 +- dart/test/scope_test.dart | 6 +- dart/test/sentry_client_test.dart | 167 ++++++++++++++++++++++++++++++ dart/test/test_utils.dart | 44 ++++---- 20 files changed, 588 insertions(+), 86 deletions(-) create mode 100644 dart/example_web/.gitignore create mode 100644 dart/example_web/README.md create mode 100644 dart/example_web/pubspec.yaml create mode 100644 dart/example_web/web/event.dart create mode 100644 dart/example_web/web/favicon.ico create mode 100644 dart/example_web/web/index.html create mode 100644 dart/example_web/web/main.dart create mode 100644 dart/example_web/web/styles.css diff --git a/.gitignore b/.gitignore index 722f819f1a..00f642ea82 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,6 @@ ios/ build/ .cxx/ + .test_coverage.dart +dart/coverage/* diff --git a/CHANGELOG.md b/CHANGELOG.md index be2abfe689..55f26e7c95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Ref: execute before send callback - Feat: add lastEventId to the Sentry static API - Feat: addBreadcrumb on Static API +- Add a Dart web example - Fix: Integrations are executed on Hub creation - Fix: NoOp encode for Web - Fix: Breadcrumb data should accept serializable types and not only String values diff --git a/dart/example/event_example.dart b/dart/example/event_example.dart index 8848782ed3..d4559014ff 100644 --- a/dart/example/event_example.dart +++ b/dart/example/event_example.dart @@ -6,43 +6,45 @@ final event = SentryEvent( release: '1.4.0-preview.1', environment: 'Test', message: Message('This is an example Dart event.'), - transaction: '/example/app', - level: SentryLevel.warning, tags: const {'project-id': '7371'}, - extra: const {'company-name': 'Dart Inc'}, + extra: const {'section': '1'}, 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'}), + id: '800', + username: 'first-user', + email: 'first@user.lan', + ipAddress: '127.0.0.1', + extras: {'first-sign-in': '2020-01-01'}, + ), breadcrumbs: [ Breadcrumb( - message: 'UI Lifecycle', - timestamp: DateTime.now().toUtc(), - category: 'ui.lifecycle', - type: 'navigation', - data: {'screen': 'MainActivity', 'state': 'created'}, - level: SentryLevel.info) + message: 'UI Lifecycle', + timestamp: DateTime.now().toUtc(), + category: 'ui.lifecycle', + type: 'navigation', + data: {'screen': 'MainActivity', 'state': 'created'}, + level: SentryLevel.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), + 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()), + 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', diff --git a/dart/example/main.dart b/dart/example/main.dart index 777407967e..9559110536 100644 --- a/dart/example/main.dart +++ b/dart/example/main.dart @@ -3,32 +3,72 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'package:sentry/sentry.dart'; import 'event_example.dart'; /// Sends a test exception report to Sentry.io using this Dart client. -Future main(List rawArgs) async { - if (rawArgs.length != 1) { - stderr.writeln( - 'Expected exactly one argument, which is the DSN issued by Sentry.io to your project.'); - exit(1); - } +Future main() async { + const dsn = + 'https://cb0fad6f5d4e42ebb9c956cb0463edc9@o447951.ingest.sentry.io/5428562'; + + SentryEvent processTagEvent(SentryEvent event, Object hint) => + event..tags.addAll({'page-locale': 'en-us'}); + + Sentry.init((options) => options + ..dsn = dsn + ..addEventProcessor(processTagEvent)); + + Sentry.addBreadcrumb( + Breadcrumb( + message: 'Authenticated user', + category: 'auth', + type: 'debug', + data: { + 'admin': true, + 'permissions': [1, 2, 3] + }, + ), + ); - final dsn = rawArgs.single; - Sentry.init((options) => options.dsn = dsn); + Sentry.configureScope((scope) { + scope + ..user = User( + id: '800', + username: 'first-user', + email: 'first@user.lan', + ipAddress: '127.0.0.1', + extras: {'first-sign-in': '2020-01-01'}, + ) + ..fingerprint = ['example-dart'] + ..transaction = '/example/app' + ..level = SentryLevel.warning + ..setTag('build', '579') + ..setExtra('company-name', 'Dart Inc'); + }); print('\nReporting a complete event example: '); // Sends a full Sentry event payload to show the different parts of the UI. final sentryId = await Sentry.captureEvent(event); - print('SentryId : ${sentryId}'); + print('Capture event result : SentryId : ${sentryId}'); + + print('\nCapture message: '); + + // Sends a full Sentry event payload to show the different parts of the UI. + final messageSentryId = await Sentry.captureMessage( + 'Message 1', + level: SentryLevel.warning, + template: 'Message %s', + params: ['1'], + ); + + print('Capture message result : SentryId : ${messageSentryId}'); try { - await foo(); + await loadConfig(); } catch (error, stackTrace) { print('\nReporting the following stack trace: '); print(stackTrace); @@ -37,7 +77,7 @@ Future main(List rawArgs) async { stackTrace: stackTrace, ); - print('SentryId : ${sentryId}'); + print('Capture exception result : SentryId : ${sentryId}'); } finally { await Sentry.close(); } @@ -45,14 +85,14 @@ Future main(List rawArgs) async { /* TODO(rxlabz) Sentry CaptureMessage(message, level) */ } -Future foo() async { - await bar(); +Future loadConfig() async { + await parseConfig(); } -Future bar() async { - await baz(); +Future parseConfig() async { + await decode(); } -Future baz() async { +Future decode() async { throw StateError('This is a test error'); } diff --git a/dart/example_web/.gitignore b/dart/example_web/.gitignore new file mode 100644 index 0000000000..3d64647b50 --- /dev/null +++ b/dart/example_web/.gitignore @@ -0,0 +1,9 @@ +# Files and directories created by pub +.dart_tool/ +.packages + +# Conventional directory for build outputs +build/ + +# Directory created by dartdoc +doc/api/ diff --git a/dart/example_web/README.md b/dart/example_web/README.md new file mode 100644 index 0000000000..73e0886e79 --- /dev/null +++ b/dart/example_web/README.md @@ -0,0 +1,9 @@ +# Sentry Dart : web example + +```dart +pub get + +# run the project ( see https://dart.dev/tools/webdev#serve ) +webdev serve --release + +``` diff --git a/dart/example_web/pubspec.yaml b/dart/example_web/pubspec.yaml new file mode 100644 index 0000000000..2b2d21817f --- /dev/null +++ b/dart/example_web/pubspec.yaml @@ -0,0 +1,16 @@ +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 + +dependencies: + sentry: + path: ../.. + +dev_dependencies: + build_runner: ^1.10.0 + build_web_compilers: ^2.13.0 + pedantic: ^1.9.0 diff --git a/dart/example_web/web/event.dart b/dart/example_web/web/event.dart new file mode 100644 index 0000000000..d4559014ff --- /dev/null +++ b/dart/example_web/web/event.dart @@ -0,0 +1,77 @@ +import 'package:sentry/src/protocol.dart'; + +final event = SentryEvent( + logger: 'main', + serverName: 'server.dart', + release: '1.4.0-preview.1', + environment: 'Test', + message: Message('This is an example Dart event.'), + tags: const {'project-id': '7371'}, + extra: const {'section': '1'}, + 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( + message: 'UI Lifecycle', + timestamp: DateTime.now().toUtc(), + category: 'ui.lifecycle', + type: 'navigation', + data: {'screen': 'MainActivity', 'state': 'created'}, + level: SentryLevel.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/example_web/web/favicon.ico b/dart/example_web/web/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7ba349b3e628d2423d4a2ed217422a4722f73739 GIT binary patch literal 3559 zcmV|z)fLATAcZDKyK$JdGY~s=NSr`PnS}BvP$+3A z8CpoogqBhg+p;Cg51fS9@izOF7~1r6zw|?g zDQ!X_8B4l7_wKH=QY>4NwW55uUP;#D-rxP7bI-kdjtU{9Dpi9&%XV3<*GkWK^P@NG zgWRw6Vb?`n$T_Evx_k{$?y0Rh-E#bYD?-UGV3Tc>$SdfYhb2dG)#K`(KPKx z4IwA0_p^z5A4{(AI%=BqUe-mpgFoo&TY*3Gu!0a29lR)aGV2dpEZ4z|Kc)+FUc-bN zHIDPB&TC8HnJ0tyG0*^nmzmQ?TnN+!QqapY^N|7@`F5AqbYw-`02pC0LNbv4yz60?w^9K&j_>533B&I%i9tFNIn5p2kb+@G0y43>@$)ns6>BLG63+2Wpepx zJ&v#ILasL(C%pe{n)2h>g2u-1wVpgKUaNE4V$J76NI&82+j&+}!O~12Z$~FRKK$`9 zx^J3f|L@(w z@^0VL;CU-=w^+ZF9FR4?4ODJ#62DZXnxe`qk)!2S9)0Z%YeH3TkE!aMNY!YE_0LhF z2ESF$qU+kcNYfp>Oq;_Knx0_qs&4=0WPdHW`-Qyher0=jx5gB?QhDMW+Qc1=t$k|< zt=eZtRI`&@>AfXtZFZz?wIfZ37txkUL?4_$0OBvSIr99C2j2UN)Ni@j77k#SApKPq z|7OZGK1&}QM-|70VjJzpQ8hDwD&8DI6m)83lM`v+s(Btdr*I>`(aIvtK1ZDD;A51L zClILKDAJgMZ)-X|x8@2VC+X9BJv40&^lN&j5M^{HDvl4q-~qts09^Y4!n4Ma6_Lw34kz1b@>qe;tZn9VPT9z@k+{b=Lo2to6L3;F~QIz4!D1T|P-qRdf7Z303(CYKm}t10))3j2!;|tzyS7gc;G1rFhS73B&NU|LN;}mYr{eivPfUF zdm~5DreHsX?W>bdsM|qmnE=2HBnZ`V2&GU0HiPHE4BB~d@G=O*FMxyW35}^c+*y^d zu=LHL8rmGaLUn`myIgTKc-?scBq8(@2<4?z0#?C(P6j}(1UFeFC{V&pSs-Nh`dIqC zkq_zKagZ2z+AcRzw=V!dgs?$W0)eov1WLdv*y|LWVW)c@2!awQQ^c0$7^MT+`37Is z%4jsE07!ol4_@%H1b}B@02vS}j=YN~fUrVwC4dzE;VS8yeRqJ(To9x$c>TNqWIDzpRz&Sr zPzjP57~P9Na0}*O4%=_+^52#;fi&rNW3NA+l7688GL>)?AiTgTsszmeR~7(L6O~|@ zzz|qG+3C{n4%C4}E>qpUB(Ws{kV9bm(b{8HL<58sjR2ud0W;XQkP4(=2|ILf=2+pq z(O1(09&`AwG{n*Q)qw$JVxnF zMFb%C2^hk0fN(%m0*265LNmZ)!wN7*KLbbq8UaA{1auJa2wp!^`o#huDPc4NLNR?p zE@mJB=mh`=BfnEomf&3wBwPRh_zkhFA1nrdt00_4bi2$P+KLn!cjN=0CupO3Leg$3 zp*Vm{2>k+tq!Nk%A+NXX^~lmZ}E0)ru(A`q6O1aeT4#SAh5kY%uwe*{*64`?9{h|TK{lms9t zVMO!^gQrlLafwQR&uH5D+yIa;xWn}w$_&dP-ZmCH63kNx)pmez0+e9HK7lI?Lbe@Z zCIIH03!8~Gbn zf+p*Bct|+_8A_;n`y?vsWCSI&<*x)yyDR;;ESm|WDWSu=9V-Fv4K$Kt?D8OWhX~-< z8M4JKx(QsRgh2tq34qYWSpHUUkm|e@h>8u?io3kMt+jNkPo$fU+`TO^E$=_ zAV@2L(Nh=zdBX|I7zlv)vLWhvxn(AR^nQB+a(@#wUK`rQ52NkQchOw{V?Bles;Gnx zuO~1Di)SVo=CHckmenU{((WCK0PvY$@A#*1=j-)CbAeSgo{@WXVb|Yr24@501Of;Q zgQUdn@s6RV_;ctHhZSwHy^XM+5McC+FpA(acq zkST#cFbNRUG6bnF(C#1)tpLs{oldkvBx7pL^j%9 z^aQ|o(0&Tt4lvfjK-P*ds`G^*Gl%u3PGSg&Ms9I z*zZ)`R3{W-EGbbsnIz4z4?~&D2QBA=kRHntC1hrXOE4OI7(xn09lZ7ozLsW{b=7 zbnCtL2cfv(eDh3zWQflPAv+AgOlsk^pSVZR4(AZM7hvEebZwgR987~DJRT$~4t`JN z@IV4P-6z6hXeZ}5TxI0SRjTv?3$ouKS*60hr&tvtLe{uv^Z_W4m}z-GL@GnHGIPk* zw6ctFod^P(OD!y`KXwnJ@4>QqH;FL@i7G0^fC~dyCpy$y;qkr9N%VyCOuRPafGQLB zzxU5Nx5-m}$bfT6kttLODx@M`to1wZ2XmNi7JNd^g%aAUV6e$$mBbisA;#D$#u!)` zw}J0?$bOnExiyeYuJhSrI5vUQ{Xnh5v4#|I^i3@pb{W7_{P2k5GK==kbAYr zd@D&R#;~Cu!m^6Z1Sv9BK^_RF-@KuRkuuEQ=LX6u&}L20<6F-P1JfjkL^$kk*d@$ZG_p zlDS-4dId>x;8Ix))Ft8KEW?C11O-;*xfWL`Qzk1{Ldf+^h!aB1=lxg-30(gpl+6{; zlAp7sn($go>tSNJPRTIkIh2%t4%H;e)d~Xy$^IHbwmS{eULGp}7eC>K>x%RdXHl9i z=pa>P`f>La2+w!sQ%|I9!8C>-&H_}9-U;=8E{GN8praR|_~}w{8h=S2<}S6&1}__C z{K0ykqcUgtgVR>NYFus(0ow+ctv$LRyQjfxf3DtV-(8H>5U@W7MVi`%u=AlE% + + + + + + + + dart_web + + + + + + + + + + +
+ +
+ +
Captured
+
+ +
+ +
Captured
+
+ +
+ +
Captured
+
+ + + diff --git a/dart/example_web/web/main.dart b/dart/example_web/web/main.dart new file mode 100644 index 0000000000..edbf86c9c3 --- /dev/null +++ b/dart/example_web/web/main.dart @@ -0,0 +1,114 @@ +import 'dart:async'; +import 'dart:html'; + +import 'package:sentry/sentry.dart'; + +import 'event.dart'; + +const dsn = + 'https://cb0fad6f5d4e42ebb9c956cb0463edc9@o447951.ingest.sentry.io/5428562'; + +void main() { + querySelector('#output').text = 'Your Dart app is running.'; + + querySelector('#btEvent') + .onClick + .listen((event) => captureCompleteExampleEvent()); + querySelector('#btMessage').onClick.listen((event) => captureMessage()); + querySelector('#btException').onClick.listen((event) => captureException()); + + initSentry(); +} + +void initSentry() { + SentryEvent processTagEvent(SentryEvent event, Object hint) => + event..tags.addAll({'page-locale': 'en-us'}); + + Sentry.init((options) => options + ..dsn = dsn + ..addEventProcessor(processTagEvent)); + + Sentry.addBreadcrumb( + Breadcrumb( + message: 'Authenticated user', + category: 'auth', + type: 'debug', + data: { + 'admin': true, + 'permissions': [1, 2, 3] + }, + ), + ); + + Sentry.configureScope((scope) { + scope + ..user = User( + id: '800', + username: 'first-user', + email: 'first@user.lan', + ipAddress: '127.0.0.1', + extras: {'first-sign-in': '2020-01-01'}, + ) + ..fingerprint = ['example-dart'] + ..transaction = '/example/app' + ..level = SentryLevel.warning + ..setTag('build', '579') + ..setExtra('company-name', 'Dart Inc'); + }); +} + +void captureMessage() async { + print('Capturing Message : '); + final sentryId = await Sentry.captureMessage( + 'Message 2', + template: 'Message %s', + params: ['2'], + ); + print('capture message result : $sentryId'); + if (sentryId != SentryId.empty()) { + querySelector('#messageResult').style.display = 'block'; + } + await Sentry.close(); +} + +void captureException() async { + try { + await buildCard(); + } catch (error, stackTrace) { + print('\nReporting the following stack trace: '); + print(stackTrace); + final sentryId = await Sentry.captureException( + error, + stackTrace: stackTrace, + ); + + print('Capture exception : SentryId: ${sentryId}'); + + if (sentryId != SentryId.empty()) { + querySelector('#exceptionResult').style.display = 'block'; + } + } +} + +Future captureCompleteExampleEvent() async { + final sentryId = await Sentry.captureEvent(event); + + print('\nReporting a complete event example: ${sdkName}'); + print('Response SentryId: ${sentryId}'); + + if (sentryId != SentryId.empty()) { + querySelector('#eventResult').style.display = 'block'; + } +} + +Future buildCard() async { + await loadData(); +} + +Future loadData() async { + await parseData(); +} + +Future parseData() async { + throw StateError('This is a test error'); +} diff --git a/dart/example_web/web/styles.css b/dart/example_web/web/styles.css new file mode 100644 index 0000000000..cc035c95c9 --- /dev/null +++ b/dart/example_web/web/styles.css @@ -0,0 +1,14 @@ +@import url(https://fonts.googleapis.com/css?family=Roboto); + +html, body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: 'Roboto', sans-serif; +} + +#output { + padding: 20px; + text-align: center; +} diff --git a/dart/lib/src/browser_client.dart b/dart/lib/src/browser_client.dart index 26a8240d50..321d723ef1 100644 --- a/dart/lib/src/browser_client.dart +++ b/dart/lib/src/browser_client.dart @@ -5,8 +5,6 @@ /// A pure Dart client for Sentry.io crash reporting. import 'dart:html' show window; -import 'package:http/browser_client.dart'; - import 'client.dart'; import 'protocol.dart'; import 'sentry_options.dart'; @@ -28,8 +26,6 @@ class SentryBrowserClient extends SentryClient { /// If [httpClient] is provided, it is used instead of the default client to /// make HTTP calls to Sentry.io. This is useful in tests. factory SentryBrowserClient(SentryOptions options) { - options.httpClient ??= BrowserClient(); - options.sdk ??= Sdk(name: sdkName, version: sdkVersion); // origin is necessary for sentry to resolve stacktrace diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index c10b666928..f861a07c4e 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -167,7 +167,6 @@ abstract class SentryClient { event = event.copyWith(breadcrumbs: scope.breadcrumbs); } - // TODO add tests // Merge the scope tags. event = event.copyWith( tags: scope.tags.map((key, value) => MapEntry(key, value)) diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index d44d80fbc1..ff3c699a48 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -4,9 +4,9 @@ import 'package:meta/meta.dart'; import 'client.dart'; import 'hub.dart'; +import 'noop_hub.dart'; import 'protocol.dart'; import 'sentry_options.dart'; -import 'noop_hub.dart'; /// Configuration options callback typedef OptionsConfiguration = void Function(SentryOptions); diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 8adadbe9d7..1a38bfc126 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -16,21 +16,32 @@ class SentryOptions { /// just not send any events. String dsn; + bool _compressPayload = true; + /// 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 = false; + bool get compressPayload => _compressPayload; + + set compressPayload(bool compressPayload) => + _compressPayload = compressPayload ?? _compressPayload; + + Client _httpClient = Client(); /// If [httpClient] is provided, it is used instead of the default client to /// make HTTP calls to Sentry.io. This is useful in tests. - Client httpClient; + Client get httpClient => _httpClient; + + set httpClient(Client httpClient) => _httpClient = httpClient ?? _httpClient; /// If [clock] is provided, it is used to get time instead of the system /// clock. This is useful in tests. Should be an implementation of [ClockProvider]. - ClockProvider _clock; + ClockProvider _clock = getUtcDateTime; ClockProvider get clock => _clock; + set clock(ClockProvider clock) => _clock = clock ?? _clock; + /// This variable controls the total amount of breadcrumbs that should be captured Default is 100 int maxBreadcrumbs = 100; @@ -134,14 +145,7 @@ class SentryOptions { // TODO: sendDefaultPii // TODO: those ctor params could be set on Sentry._setDefaultConfiguration or instantiate by default here - SentryOptions({ - this.dsn, - this.compressPayload, - this.httpClient, - ClockProvider clock = getUtcDateTime, - }) { - _clock = clock; - } + SentryOptions({this.dsn}); /// Adds an event processor void addEventProcessor(EventProcessor eventProcessor) { diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 409e69a584..e253929c49 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -1,9 +1,10 @@ +import 'dart:async'; + 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 'dart:async'; import 'mocks.dart'; diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index f48152d07d..afb2b57700 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -231,8 +231,10 @@ void main() { expect(sut.tags, clone.tags); expect(sut.breadcrumbs, clone.breadcrumbs); expect(ListEquality().equals(sut.fingerprint, clone.fingerprint), true); - expect(ListEquality().equals(sut.eventProcessors, clone.eventProcessors), - true); + expect( + ListEquality().equals(sut.eventProcessors, clone.eventProcessors), + true, + ); }); } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index d6801cc753..ec317afb29 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -5,6 +5,173 @@ import 'package:test/test.dart'; import 'mocks.dart'; void main() { + group('SentryClient captures message', () { + SentryOptions options; + + setUp(() { + options = SentryOptions(dsn: fakeDsn); + options.transport = MockTransport(); + }); + + test('should capture message', () async { + final client = SentryClient(options); + await client.captureMessage( + 'simple message 1', + template: 'simple message %d', + params: [1], + level: SentryLevel.error, + ); + + final capturedEvent = (verify( + options.transport.send(captureAny), + ).captured.first) as SentryEvent; + + expect(capturedEvent.message.formatted, 'simple message 1'); + expect(capturedEvent.message.template, 'simple message %d'); + expect(capturedEvent.message.params, [1]); + }); + }); + + group('SentryClient captures exception', () { + SentryOptions options; + + Error error; + StackTrace stackTrace; + + setUp(() { + options = SentryOptions(dsn: fakeDsn); + options.transport = MockTransport(); + }); + + test('should capture exception', () async { + try { + throw StateError('Error'); + } on Error catch (err, stack) { + error = err; + stackTrace = stack; + } + + final client = SentryClient(options); + await client.captureException(error, stackTrace: stackTrace); + + final capturedEvent = (verify( + options.transport.send(captureAny), + ).captured.first) as SentryEvent; + + expect(capturedEvent.exception, error); + expect(capturedEvent.stackTrace, stackTrace); + }); + }); + + group('SentryClient : apply scope to the captured event', () { + SentryOptions options; + Scope scope; + + final level = SentryLevel.error; + final transaction = '/test/scope'; + final fingerprint = ['foo', 'bar', 'baz']; + final user = User(id: '123', username: 'test'); + final crumb = Breadcrumb(message: 'bread'); + final scopeTagKey = 'scope-tag'; + final scopeTagValue = 'scope-tag-value'; + final eventTagKey = 'event-tag'; + final eventTagValue = 'event-tag-value'; + final scopeExtraKey = 'scope-extra'; + final scopeExtraValue = 'scope-extra-value'; + final eventExtraKey = 'event-extra'; + final eventExtraValue = 'event-extra-value'; + + final event = SentryEvent( + tags: {eventTagKey: eventTagValue}, + extra: {eventExtraKey: eventExtraValue}, + level: SentryLevel.warning, + ); + + setUp(() { + options = SentryOptions(dsn: fakeDsn); + options.transport = MockTransport(); + + scope = Scope(options) + ..user = user + ..level = level + ..transaction = transaction + ..fingerprint = fingerprint + ..addBreadcrumb(crumb) + ..setTag(scopeTagKey, scopeTagValue) + ..setExtra(scopeExtraKey, scopeExtraValue); + }); + + test('should apply the scope', () async { + final client = SentryClient(options); + await client.captureEvent(event, scope: scope); + + final capturedEvent = (verify( + options.transport.send(captureAny), + ).captured.first) as SentryEvent; + + expect(capturedEvent.userContext?.id, user.id); + expect(capturedEvent.level.name, SentryLevel.error.name); + expect(capturedEvent.transaction, transaction); + expect(capturedEvent.fingerprint, fingerprint); + expect(capturedEvent.breadcrumbs.first, crumb); + expect(capturedEvent.tags, { + scopeTagKey: scopeTagValue, + eventTagKey: eventTagValue, + }); + expect(capturedEvent.extra, { + scopeExtraKey: scopeExtraValue, + eventExtraKey: eventExtraValue, + }); + }); + }); + + group('SentryClient : apply partial scope to the captured event', () { + SentryOptions options; + Scope scope; + + final transaction = '/test/scope'; + final eventTransaction = '/event/transaction'; + final fingerprint = ['foo', 'bar', 'baz']; + final eventFingerprint = ['123', '456', '798']; + final user = User(id: '123'); + final eventUser = User(id: '987'); + final crumb = Breadcrumb(message: 'bread'); + final eventCrumbs = [Breadcrumb(message: 'bread')]; + + final event = SentryEvent( + level: SentryLevel.warning, + transaction: eventTransaction, + userContext: eventUser, + fingerprint: eventFingerprint, + breadcrumbs: eventCrumbs, + ); + + setUp(() { + options = SentryOptions(dsn: fakeDsn); + options.transport = MockTransport(); + scope = Scope(options) + ..user = user + ..transaction = transaction + ..fingerprint = fingerprint + ..addBreadcrumb(crumb); + }); + + test('should not apply the scope to non null event fields ', () async { + final client = SentryClient(options); + await client.captureEvent(event, scope: scope); + + final capturedEvent = (verify( + options.transport.send(captureAny), + ).captured.first) as SentryEvent; + + expect(capturedEvent.userContext.id, eventUser.id); + expect(capturedEvent.level.name, SentryLevel.warning.name); + expect(capturedEvent.transaction, eventTransaction); + expect(capturedEvent.fingerprint, eventFingerprint); + expect(capturedEvent.breadcrumbs, eventCrumbs); + }); + }); + group('SentryClient sampling', () { SentryOptions options; diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index f438ca1a8e..d540b39fbc 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -70,12 +70,10 @@ Future testCaptureException( fail('Unexpected request on ${request.method} ${request.url} in HttpMock'); }); - final options = SentryOptions( - dsn: testDsn, - httpClient: httpMock, - clock: fakeClockProvider, - compressPayload: compressPayload, - ) + final options = SentryOptions(dsn: testDsn) + ..compressPayload = compressPayload + ..clock = fakeClockProvider + ..httpClient = httpMock ..serverName = 'test.server.com' ..release = '1.2.3' ..environment = 'staging'; @@ -181,8 +179,10 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { final options = SentryOptions(dsn: testDsn); final client = SentryClient(options); expect(options.transport.dsn.uri, Uri.parse(testDsn)); - expect(options.transport.dsn.postUri, - 'https://sentry.example.com/api/1/store/'); + expect( + options.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'); @@ -193,8 +193,10 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { final options = SentryOptions(dsn: _testDsnWithoutSecret); final client = SentryClient(options); expect(options.transport.dsn.uri, Uri.parse(_testDsnWithoutSecret)); - expect(options.transport.dsn.postUri, - 'https://sentry.example.com/api/1/store/'); + expect( + options.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'); @@ -242,12 +244,10 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { }); final client = SentryClient( - SentryOptions( - dsn: _testDsnWithoutSecret, - httpClient: httpMock, - clock: fakeClockProvider, - compressPayload: false, - ) + SentryOptions(dsn: _testDsnWithoutSecret) + ..httpClient = httpMock + ..clock = fakeClockProvider + ..compressPayload = false ..serverName = 'test.server.com' ..release = '1.2.3' ..environment = 'staging', @@ -299,10 +299,10 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { final client = SentryClient( SentryOptions( dsn: testDsn, - httpClient: httpMock, - clock: fakeClockProvider, - compressPayload: false, ) + ..httpClient = httpMock + ..clock = fakeClockProvider + ..compressPayload = false ..serverName = 'test.server.com' ..release = '1.2.3' ..environment = 'staging', @@ -353,10 +353,10 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { final options = SentryOptions( dsn: testDsn, - httpClient: httpMock, - clock: fakeClockProvider, - compressPayload: false, ) + ..httpClient = httpMock + ..clock = fakeClockProvider + ..compressPayload = false ..serverName = 'test.server.com' ..release = '1.2.3' ..environment = 'staging'; From d249c9722eb78014ff992e6a040ada084bb1aad0 Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Tue, 27 Oct 2020 16:19:24 +0100 Subject: [PATCH 20/34] Ref/prepare event & scope.applyToEvent (#140) --- CHANGELOG.md | 1 + dart/lib/src/client.dart | 70 ++++++------------------- dart/lib/src/protocol/sentry_event.dart | 5 +- dart/lib/src/scope.dart | 40 ++++++++++++++ dart/lib/src/sentry_options.dart | 11 +++- 5 files changed, 68 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55f26e7c95..ebf27f42d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - Fix: Integrations are executed on Hub creation - Fix: NoOp encode for Web - Fix: Breadcrumb data should accept serializable types and not only String values +- Ref: added Scope.applyToEvent # `package:sentry` changelog diff --git a/dart/lib/src/client.dart b/dart/lib/src/client.dart index f861a07c4e..3728020004 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/client.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:math'; -import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/transport/noop_transport.dart'; @@ -44,20 +43,18 @@ abstract class SentryClient { return emptyFuture; } - event = _applyScope(event: event, scope: scope); + if (scope != null) { + event = scope.applyToEvent(event, hint); + } else { + _options.logger(SentryLevel.debug, 'No scope is defined'); + } // dropped by scope event processors if (event == null) { return emptyFuture; } - // TODO create eventProcessors ? - event = event.copyWith( - serverName: _options.serverName, - environment: _options.environment, - release: _options.release, - platform: event.platform ?? sdkPlatform, - ); + event = _prepareEvent(event); if (_options.beforeSendCallback != null) { try { @@ -77,6 +74,16 @@ abstract class SentryClient { return _options.transport.send(event); } + SentryEvent _prepareEvent(SentryEvent event) => event.copyWith( + serverName: event.serverName ?? _options.serverName, + dist: event.dist ?? _options.dist, + environment: + event.environment ?? _options.environment ?? defaultEnvironment, + release: event.release ?? _options.release, + sdk: event.sdk ?? _options.sdk, + platform: event.platform ?? sdkPlatform, + ); + /// Reports the [throwable] and optionally its [stackTrace] to Sentry.io. Future captureException( dynamic throwable, { @@ -142,51 +149,6 @@ abstract class SentryClient { return event; } - SentryEvent _applyScope({ - @required SentryEvent event, - @required Scope scope, - }) { - if (scope != null) { - // Merge the scope transaction. - if (event.transaction == null) { - event = event.copyWith(transaction: scope.transaction); - } - - // Merge the user context. - if (event.userContext == null) { - event = event.copyWith(userContext: scope.user); - } - - // Merge the scope fingerprint. - if (event.fingerprint == null) { - event = event.copyWith(fingerprint: scope.fingerprint); - } - - // Merge the scope breadcrumbs. - if (event.breadcrumbs == null) { - event = event.copyWith(breadcrumbs: scope.breadcrumbs); - } - - // Merge the scope tags. - event = event.copyWith( - tags: scope.tags.map((key, value) => MapEntry(key, value)) - ..addAll(event.tags ?? {})); - - // Merge the scope extra. - event = event.copyWith( - extra: scope.extra.map((key, value) => MapEntry(key, value)) - ..addAll(event.extra ?? {})); - - // Merge the scope level. - if (scope.level != null) { - event = event.copyWith(level: scope.level); - } - - // TODO: execute scope event processors - } - return event; - } - bool _sampleRate() { if (_options.sampleRate != null && _random != null) { return (_options.sampleRate < _random.nextDouble()); diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index e721a485fc..76025aaf35 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -12,7 +12,7 @@ class SentryEvent { SentryEvent({ SentryId eventId, DateTime timestamp, - Sdk sdk, + this.sdk, this.platform, this.logger, this.serverName, @@ -33,8 +33,7 @@ class SentryEvent { this.contexts, this.breadcrumbs, }) : eventId = eventId ?? SentryId.newId(), - timestamp = timestamp ?? getUtcDateTime(), - sdk = sdk ?? Sdk(name: sdkName, version: sdkVersion); + timestamp = timestamp ?? getUtcDateTime(); /// Refers to the default fingerprinting algorithm. /// diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 4927fcaa5b..4854955eba 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -142,6 +142,46 @@ class Scope { /// Removes an extra from the Scope void removeExtra(String key) => _extra.remove(key); + SentryEvent applyToEvent(SentryEvent event, dynamic hint) { + event = event.copyWith( + transaction: event.transaction ?? transaction, + userContext: event.userContext ?? user, + fingerprint: event.fingerprint ?? fingerprint, + breadcrumbs: event.breadcrumbs ?? breadcrumbs, + tags: tags.isNotEmpty ? _mergeEventTags(event) : event.tags, + extra: extra.isNotEmpty ? _mergeEventExtra(event) : event.extra, + level: level ?? event.level, + ); + + for (final processor in _eventProcessors) { + try { + event = processor(event, hint); + } catch (err) { + _options.logger( + SentryLevel.error, + 'An exception occurred while processing event by a processor : $err', + ); + } + if (event == null) { + _options.logger(SentryLevel.debug, 'Event was dropped by a processor'); + break; + } + } + + return event; + } + + /// the event tags will be kept + /// if the scope and the event have tag entries with the same key, + Map _mergeEventTags(SentryEvent event) => + tags.map((key, value) => MapEntry(key, value))..addAll(event.tags ?? {}); + + /// if the scope and the event have extra entries with the same key, + /// the event extra will be kept + Map _mergeEventExtra(SentryEvent event) => + extra.map((key, value) => MapEntry(key, value)) + ..addAll(event.extra ?? {}); + /// Clones the current Scope Scope clone() { final clone = Scope(_options) diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 1a38bfc126..fbef8248c6 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -7,6 +7,8 @@ import 'hub.dart'; import 'protocol.dart'; import 'utils.dart'; +const defaultEnvironment = 'production'; + /// Sentry SDK options class SentryOptions { /// Default Log level if not specified Default is DEBUG @@ -98,7 +100,6 @@ class SentryOptions { /// 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. @@ -137,8 +138,14 @@ class SentryOptions { /// The server name used in the Sentry messages. String serverName; + Sdk _sdk = Sdk(name: sdkName, version: sdkVersion); + /// Sdk object that contains the Sentry Client Name and its version - Sdk sdk; + Sdk get sdk => _sdk; + + set sdk(Sdk sdk) { + _sdk = sdk ?? _sdk; + } // TODO: Scope observers, enableScopeSync From dc258d9a0e287d8e7342796c895a9b482843d544 Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Tue, 27 Oct 2020 17:08:27 +0100 Subject: [PATCH 21/34] Ref : rename files accordely to their content (#141) --- CHANGELOG.md | 1 + dart/example/event_example.dart | 2 +- dart/example_web/web/event.dart | 2 +- dart/lib/browser.dart | 2 +- dart/lib/io.dart | 2 +- dart/lib/sentry.dart | 2 +- dart/lib/src/hub.dart | 2 +- dart/lib/src/hub_adapter.dart | 12 +++++++----- dart/lib/src/noop_client.dart | 2 +- dart/lib/src/noop_hub.dart | 4 ++-- dart/lib/src/protocol/sentry_event.dart | 17 ++++++++--------- dart/lib/src/protocol/user.dart | 4 ++-- dart/lib/src/scope.dart | 2 +- dart/lib/src/sentry.dart | 2 +- ...r_client.dart => sentry_browser_client.dart} | 2 +- .../lib/src/{client.dart => sentry_client.dart} | 6 +++--- ...client_stub.dart => sentry_client_stub.dart} | 4 ++-- .../{io_client.dart => sentry_io_client.dart} | 2 +- dart/test/event_test.dart | 2 +- dart/test/mocks.dart | 2 +- dart/test/sentry_client_test.dart | 6 +++--- dart/test/sentry_io_test.dart | 2 +- dart/test/test_utils.dart | 16 ++++++++-------- 23 files changed, 50 insertions(+), 48 deletions(-) rename dart/lib/src/{browser_client.dart => sentry_browser_client.dart} (98%) rename dart/lib/src/{client.dart => sentry_client.dart} (96%) rename dart/lib/src/{client_stub.dart => sentry_client_stub.dart} (76%) rename dart/lib/src/{io_client.dart => sentry_io_client.dart} (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf27f42d0..7ba8c18586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - Fix: NoOp encode for Web - Fix: Breadcrumb data should accept serializable types and not only String values - Ref: added Scope.applyToEvent +- Ref: rename sdk files accordely to their content # `package:sentry` changelog diff --git a/dart/example/event_example.dart b/dart/example/event_example.dart index d4559014ff..3ccc3752a3 100644 --- a/dart/example/event_example.dart +++ b/dart/example/event_example.dart @@ -9,7 +9,7 @@ final event = SentryEvent( tags: const {'project-id': '7371'}, extra: const {'section': '1'}, fingerprint: const ['example-dart'], - userContext: const User( + user: const User( id: '800', username: 'first-user', email: 'first@user.lan', diff --git a/dart/example_web/web/event.dart b/dart/example_web/web/event.dart index d4559014ff..3ccc3752a3 100644 --- a/dart/example_web/web/event.dart +++ b/dart/example_web/web/event.dart @@ -9,7 +9,7 @@ final event = SentryEvent( tags: const {'project-id': '7371'}, extra: const {'section': '1'}, fingerprint: const ['example-dart'], - userContext: const User( + user: const User( id: '800', username: 'first-user', email: 'first@user.lan', diff --git a/dart/lib/browser.dart b/dart/lib/browser.dart index 621a3947e8..982e63a54a 100644 --- a/dart/lib/browser.dart +++ b/dart/lib/browser.dart @@ -2,5 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'src/browser_client.dart'; +export 'src/sentry_browser_client.dart'; export 'src/version.dart'; diff --git a/dart/lib/io.dart b/dart/lib/io.dart index 1768abf5f7..e47a9d626b 100644 --- a/dart/lib/io.dart +++ b/dart/lib/io.dart @@ -2,5 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'src/io_client.dart'; +export 'src/sentry_io_client.dart'; export 'src/version.dart'; diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 85a27fd53e..39bf57d074 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -3,10 +3,10 @@ // found in the LICENSE file. /// A pure Dart client for Sentry.io crash reporting. -export 'src/client.dart'; export 'src/protocol.dart'; export 'src/scope.dart'; export 'src/sentry.dart'; +export 'src/sentry_client.dart'; export 'src/sentry_options.dart'; export 'src/transport/transport.dart'; export 'src/version.dart'; diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index cda6add859..bb9402b138 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -3,10 +3,10 @@ import 'dart:collection'; import 'package:sentry/src/hub_adapter.dart'; -import 'client.dart'; import 'noop_client.dart'; import 'protocol.dart'; import 'scope.dart'; +import 'sentry_client.dart'; import 'sentry_options.dart'; typedef ScopeCallback = void Function(Scope); diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 9f5fc1b25e..074a2c62e1 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -1,11 +1,13 @@ -import 'package:sentry/src/protocol/sentry_level.dart'; -import 'package:sentry/src/protocol/sentry_id.dart'; -import 'package:sentry/src/protocol/sentry_event.dart'; +import 'dart:async'; + import 'package:sentry/src/protocol/breadcrumb.dart'; -import 'package:sentry/src/client.dart'; +import 'package:sentry/src/protocol/sentry_event.dart'; +import 'package:sentry/src/protocol/sentry_id.dart'; +import 'package:sentry/src/protocol/sentry_level.dart'; import 'package:sentry/src/sentry.dart'; +import 'package:sentry/src/sentry_client.dart'; + import 'hub.dart'; -import 'dart:async'; /// Hub adapter to make Integrations testable class HubAdapter implements Hub { diff --git a/dart/lib/src/noop_client.dart b/dart/lib/src/noop_client.dart index 412495fec0..758ac86d37 100644 --- a/dart/lib/src/noop_client.dart +++ b/dart/lib/src/noop_client.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'client.dart'; import 'protocol.dart'; import 'scope.dart'; +import 'sentry_client.dart'; class NoOpSentryClient implements SentryClient { NoOpSentryClient._(); diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 3a79f71211..61e59c24ca 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:sentry/src/protocol/breadcrumb.dart'; -import 'client.dart'; import 'hub.dart'; import 'protocol/sentry_event.dart'; -import 'protocol/sentry_level.dart'; import 'protocol/sentry_id.dart'; +import 'protocol/sentry_level.dart'; +import 'sentry_client.dart'; class NoOpHub implements Hub { NoOpHub._(); diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index 76025aaf35..e049baf392 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -29,7 +29,7 @@ class SentryEvent { this.tags, this.extra, this.fingerprint, - this.userContext, + this.user, this.contexts, this.breadcrumbs, }) : eventId = eventId ?? SentryId.newId(), @@ -112,8 +112,8 @@ class SentryEvent { /// Information about the current user. /// /// The value in this field overrides the user context - /// set in [SentryClient.userContext] for this logged event. - final User userContext; + /// set in [SentryClient.user] for this logged event. + final User user; /// The context interfaces provide additional context data. /// Typically this is data related to the current user, @@ -161,7 +161,7 @@ class SentryEvent { Map tags, Map extra, List fingerprint, - User userContext, + User user, Contexts contexts, List breadcrumbs, Sdk sdk, @@ -185,7 +185,7 @@ class SentryEvent { tags: tags ?? this.tags, extra: extra ?? this.extra, fingerprint: fingerprint ?? this.fingerprint, - userContext: userContext ?? this.userContext, + user: user ?? this.user, contexts: contexts ?? this.contexts, breadcrumbs: breadcrumbs ?? this.breadcrumbs, sdk: sdk ?? this.sdk, @@ -288,10 +288,9 @@ class SentryEvent { json['contexts'] = contextsMap; } - Map userContextMap; - if (userContext != null && - (userContextMap = userContext.toJson()).isNotEmpty) { - json['user'] = userContextMap; + Map userMap; + if (user != null && (userMap = user.toJson()).isNotEmpty) { + json['user'] = userMap; } if (fingerprint != null && fingerprint.isNotEmpty) { diff --git a/dart/lib/src/protocol/user.dart b/dart/lib/src/protocol/user.dart index 1462762980..1dd077b55e 100644 --- a/dart/lib/src/protocol/user.dart +++ b/dart/lib/src/protocol/user.dart @@ -1,8 +1,8 @@ /// Describes the current user associated with the application, such as the /// currently signed in user. /// -/// The user can be specified globally in the [SentryClient.userContext] field, -/// or per event in the [Event.userContext] field. +/// The user can be specified globally in the [SentryClient.user] field, +/// or per event in the [Event.user] field. /// /// You should provide at least either an [id] (a unique identifier for an /// authenticated user) or [ipAddress] (their IP address). diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 4854955eba..ebf8867306 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -145,7 +145,7 @@ class Scope { SentryEvent applyToEvent(SentryEvent event, dynamic hint) { event = event.copyWith( transaction: event.transaction ?? transaction, - userContext: event.userContext ?? user, + user: event.user ?? user, fingerprint: event.fingerprint ?? fingerprint, breadcrumbs: event.breadcrumbs ?? breadcrumbs, tags: tags.isNotEmpty ? _mergeEventTags(event) : event.tags, diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index ff3c699a48..8251e212bd 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'client.dart'; import 'hub.dart'; import 'noop_hub.dart'; import 'protocol.dart'; +import 'sentry_client.dart'; import 'sentry_options.dart'; /// Configuration options callback diff --git a/dart/lib/src/browser_client.dart b/dart/lib/src/sentry_browser_client.dart similarity index 98% rename from dart/lib/src/browser_client.dart rename to dart/lib/src/sentry_browser_client.dart index 321d723ef1..2b43ed7957 100644 --- a/dart/lib/src/browser_client.dart +++ b/dart/lib/src/sentry_browser_client.dart @@ -5,8 +5,8 @@ /// A pure Dart client for Sentry.io crash reporting. import 'dart:html' show window; -import 'client.dart'; import 'protocol.dart'; +import 'sentry_client.dart'; import 'sentry_options.dart'; import 'version.dart'; diff --git a/dart/lib/src/client.dart b/dart/lib/src/sentry_client.dart similarity index 96% rename from dart/lib/src/client.dart rename to dart/lib/src/sentry_client.dart index 3728020004..569b51050b 100644 --- a/dart/lib/src/client.dart +++ b/dart/lib/src/sentry_client.dart @@ -4,10 +4,10 @@ import 'dart:math'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/transport/noop_transport.dart'; -import 'client_stub.dart' - if (dart.library.html) 'browser_client.dart' - if (dart.library.io) 'io_client.dart'; import 'protocol.dart'; +import 'sentry_client_stub.dart' + if (dart.library.html) 'sentry_browser_client.dart' + if (dart.library.io) 'sentry_io_client.dart'; /// Logs crash reports and events to the Sentry.io service. abstract class SentryClient { diff --git a/dart/lib/src/client_stub.dart b/dart/lib/src/sentry_client_stub.dart similarity index 76% rename from dart/lib/src/client_stub.dart rename to dart/lib/src/sentry_client_stub.dart index 67fdfbda69..e212b39a0d 100644 --- a/dart/lib/src/client_stub.dart +++ b/dart/lib/src/sentry_client_stub.dart @@ -2,10 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'client.dart'; +import 'sentry_client.dart'; import 'sentry_options.dart'; -/// Implemented in `browser_client.dart` and `io_client.dart`. +/// Implemented in `sentry_browser_client.dart` and `sentry_io_client.dart`. SentryClient createSentryClient(SentryOptions options) => throw UnsupportedError( 'Cannot create a client without dart:html or dart:io.'); diff --git a/dart/lib/src/io_client.dart b/dart/lib/src/sentry_io_client.dart similarity index 96% rename from dart/lib/src/io_client.dart rename to dart/lib/src/sentry_io_client.dart index 8511553c06..c195aa6f04 100644 --- a/dart/lib/src/io_client.dart +++ b/dart/lib/src/sentry_io_client.dart @@ -5,7 +5,7 @@ import 'package:sentry/sentry.dart'; /// A pure Dart client for Sentry.io crash reporting. -import 'client.dart'; +import 'sentry_client.dart'; import 'sentry_options.dart'; SentryClient createSentryClient(SentryOptions options) => diff --git a/dart/test/event_test.dart b/dart/test/event_test.dart index e5aa283587..7f254b0c10 100644 --- a/dart/test/event_test.dart +++ b/dart/test/event_test.dart @@ -95,7 +95,7 @@ void main() { 'g': 2, }, fingerprint: const [SentryEvent.defaultFingerprint, 'foo'], - userContext: user, + user: user, breadcrumbs: breadcrumbs, ).toJson(), { diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index 6d50a7ecb9..c1ee58059a 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -25,7 +25,7 @@ final fakeEvent = SentryEvent( tags: const {'project-id': '7371'}, extra: const {'company-name': 'Dart Inc'}, fingerprint: const ['example-dart'], - userContext: const User( + user: const User( id: '800', username: 'first-user', email: 'first@user.lan', diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index ec317afb29..33e0329a10 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -109,7 +109,7 @@ void main() { options.transport.send(captureAny), ).captured.first) as SentryEvent; - expect(capturedEvent.userContext?.id, user.id); + expect(capturedEvent.user?.id, user.id); expect(capturedEvent.level.name, SentryLevel.error.name); expect(capturedEvent.transaction, transaction); expect(capturedEvent.fingerprint, fingerprint); @@ -141,7 +141,7 @@ void main() { final event = SentryEvent( level: SentryLevel.warning, transaction: eventTransaction, - userContext: eventUser, + user: eventUser, fingerprint: eventFingerprint, breadcrumbs: eventCrumbs, ); @@ -164,7 +164,7 @@ void main() { options.transport.send(captureAny), ).captured.first) as SentryEvent; - expect(capturedEvent.userContext.id, eventUser.id); + expect(capturedEvent.user.id, eventUser.id); expect(capturedEvent.level.name, SentryLevel.warning.name); expect(capturedEvent.transaction, eventTransaction); expect(capturedEvent.fingerprint, eventFingerprint); diff --git a/dart/test/sentry_io_test.dart b/dart/test/sentry_io_test.dart index 10a5b70e00..6e453c90c5 100644 --- a/dart/test/sentry_io_test.dart +++ b/dart/test/sentry_io_test.dart @@ -5,7 +5,7 @@ import 'dart:io'; import 'package:sentry/sentry.dart'; -import 'package:sentry/src/io_client.dart'; +import 'package:sentry/src/sentry_io_client.dart'; import 'package:test/test.dart'; import 'test_utils.dart'; diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index d540b39fbc..9852b7aed7 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -319,7 +319,7 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { await client.close(); }); - test('$SentryEvent userContext overrides client', () async { + test('$SentryEvent user overrides client', () async { final fakeClockProvider = () => DateTime.utc(2017, 1, 2); String loggedUserId; // used to find out what user context was sent @@ -337,13 +337,13 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { 'Unexpected request on ${request.method} ${request.url} in HttpMock'); }); - const clientUserContext = User( + const clientUser = User( id: 'client_user', username: 'username', email: 'email@email.com', ipAddress: '127.0.0.1', ); - const eventUserContext = User( + const eventUser = User( id: 'event_user', username: 'username', email: 'email@email.com', @@ -375,16 +375,16 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { eventId: SentryId.empty(), exception: error, stackTrace: stackTrace, - userContext: eventUserContext, + user: eventUser, ); await client.captureEvent(eventWithoutContext, - scope: Scope(options)..user = clientUserContext); - expect(loggedUserId, clientUserContext.id); + scope: Scope(options)..user = clientUser); + expect(loggedUserId, clientUser.id); await client.captureEvent( eventWithContext, - scope: Scope(options)..user = clientUserContext, + scope: Scope(options)..user = clientUser, ); - expect(loggedUserId, eventUserContext.id); + expect(loggedUserId, eventUser.id); } await client.close(); From c6669c32023fc9358bff7bc213278560d3f204e3 Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Tue, 27 Oct 2020 20:59:07 +0100 Subject: [PATCH 22/34] rename the `throwable` argument to `exception` (#142) --- CHANGELOG.md | 1 + dart/lib/src/hub.dart | 8 ++++---- dart/lib/src/hub_adapter.dart | 4 ++-- dart/lib/src/noop_client.dart | 2 +- dart/lib/src/noop_hub.dart | 2 +- dart/lib/src/sentry.dart | 4 ++-- dart/lib/src/sentry_client.dart | 6 +++--- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba8c18586..e4a0ec80ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - Fix: Breadcrumb data should accept serializable types and not only String values - Ref: added Scope.applyToEvent - Ref: rename sdk files accordely to their content +- Ref: rename the `throwable` argument name to `exception` in `captureEvents(...)` # `package:sentry` changelog diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index bb9402b138..d24d8b56c9 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -103,7 +103,7 @@ class Hub { /// Captures the exception Future captureException( - dynamic throwable, { + dynamic exception, { dynamic stackTrace, dynamic hint, }) async { @@ -114,7 +114,7 @@ class Hub { SentryLevel.warning, "Instance is disabled and this 'captureException' call is a no-op.", ); - } else if (throwable == null) { + } else if (exception == null) { _options.logger( SentryLevel.warning, 'captureException called with null parameter.', @@ -124,7 +124,7 @@ class Hub { if (item != null) { try { sentryId = await item.client.captureException( - throwable, + exception, stackTrace: stackTrace, scope: item.scope, hint: hint, @@ -132,7 +132,7 @@ class Hub { } catch (err) { _options.logger( SentryLevel.error, - 'Error while capturing exception : ${throwable}', + 'Error while capturing exception : ${exception}', ); } finally { _lastEventId = sentryId; diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 074a2c62e1..be527b7cf3 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -32,12 +32,12 @@ class HubAdapter implements Hub { @override Future captureException( - dynamic throwable, { + dynamic exception, { dynamic stackTrace, dynamic hint, }) => Sentry.captureException( - throwable, + exception, stackTrace: stackTrace, hint: hint, ); diff --git a/dart/lib/src/noop_client.dart b/dart/lib/src/noop_client.dart index 758ac86d37..1b5be04a8c 100644 --- a/dart/lib/src/noop_client.dart +++ b/dart/lib/src/noop_client.dart @@ -23,7 +23,7 @@ class NoOpSentryClient implements SentryClient { @override Future captureException( - dynamic throwable, { + dynamic exception, { dynamic stackTrace, Scope scope, dynamic hint, diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 61e59c24ca..db7e1637d5 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -26,7 +26,7 @@ class NoOpHub implements Hub { @override Future captureException( - dynamic throwable, { + dynamic exception, { dynamic stackTrace, dynamic hint, }) => diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 8251e212bd..0ee4365472 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -56,12 +56,12 @@ class Sentry { /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. static Future captureException( - dynamic throwable, { + dynamic exception, { dynamic stackTrace, dynamic hint, }) async => currentHub.captureException( - throwable, + exception, stackTrace: stackTrace, hint: hint, ); diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 569b51050b..2d19ed7f2e 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -84,15 +84,15 @@ abstract class SentryClient { platform: event.platform ?? sdkPlatform, ); - /// Reports the [throwable] and optionally its [stackTrace] to Sentry.io. + /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. Future captureException( - dynamic throwable, { + dynamic exception, { dynamic stackTrace, Scope scope, dynamic hint, }) { final event = SentryEvent( - exception: throwable, + exception: exception, stackTrace: stackTrace, timestamp: _options.clock(), ); From a0bb93966a8745389b237653e1fdad623eddf25f Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Tue, 27 Oct 2020 21:01:22 +0100 Subject: [PATCH 23/34] lint : prefer relative imports and show error when a @required argument is missing (#143) --- CHANGELOG.md | 1 + dart/analysis_options.yaml | 18 +++++++++++++++++- dart/lib/src/diagnostic_logger.dart | 3 ++- dart/lib/src/hub.dart | 3 +-- dart/lib/src/hub_adapter.dart | 10 +++------- dart/lib/src/noop_hub.dart | 3 +-- dart/lib/src/sentry_client.dart | 8 +++++--- dart/lib/src/sentry_io_client.dart | 3 ++- dart/lib/src/sentry_options.dart | 5 +++-- dart/lib/src/transport/noop_transport.dart | 3 ++- dart/lib/src/transport/transport.dart | 2 +- 11 files changed, 38 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4a0ec80ec..6d9342b213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - Fix: Breadcrumb data should accept serializable types and not only String values - Ref: added Scope.applyToEvent - Ref: rename sdk files accordely to their content +- chore: new analysis options rules - Ref: rename the `throwable` argument name to `exception` in `captureEvents(...)` # `package:sentry` changelog diff --git a/dart/analysis_options.yaml b/dart/analysis_options.yaml index d4fcc1ad82..b765dd0459 100644 --- a/dart/analysis_options.yaml +++ b/dart/analysis_options.yaml @@ -1 +1,17 @@ -include: package:pedantic/analysis_options.yaml \ No newline at end of file +include: package:pedantic/analysis_options.yaml + +analyzer: + errors: + # treat missing required parameters as a warning (not a hint) + missing_required_param: error + # treat missing returns as a warning (not a hint) + missing_return: error + # allow having TODOs in the code + todo: ignore + # allow self-reference to deprecated members (we do this because otherwise we have + # to annotate every member in every test, assert, etc, when we deprecate something) + deprecated_member_use_from_same_package: warning + +linter: + rules: + - prefer_relative_imports \ No newline at end of file diff --git a/dart/lib/src/diagnostic_logger.dart b/dart/lib/src/diagnostic_logger.dart index bc2737923f..77148b18be 100644 --- a/dart/lib/src/diagnostic_logger.dart +++ b/dart/lib/src/diagnostic_logger.dart @@ -1,4 +1,5 @@ -import 'package:sentry/sentry.dart'; +import 'protocol.dart'; +import 'sentry_options.dart'; class DiagnosticLogger { final Logger _logger; diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index d24d8b56c9..63eaf3faaa 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'dart:collection'; -import 'package:sentry/src/hub_adapter.dart'; - +import 'hub_adapter.dart'; import 'noop_client.dart'; import 'protocol.dart'; import 'scope.dart'; diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index be527b7cf3..36d0e85af6 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -1,13 +1,9 @@ import 'dart:async'; -import 'package:sentry/src/protocol/breadcrumb.dart'; -import 'package:sentry/src/protocol/sentry_event.dart'; -import 'package:sentry/src/protocol/sentry_id.dart'; -import 'package:sentry/src/protocol/sentry_level.dart'; -import 'package:sentry/src/sentry.dart'; -import 'package:sentry/src/sentry_client.dart'; - import 'hub.dart'; +import 'protocol.dart'; +import 'sentry.dart'; +import 'sentry_client.dart'; /// Hub adapter to make Integrations testable class HubAdapter implements Hub { diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index db7e1637d5..caebabe214 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -1,11 +1,10 @@ import 'dart:async'; -import 'package:sentry/src/protocol/breadcrumb.dart'; - import 'hub.dart'; import 'protocol/sentry_event.dart'; import 'protocol/sentry_id.dart'; import 'protocol/sentry_level.dart'; +import 'protocol.dart'; import 'sentry_client.dart'; class NoOpHub implements Hub { diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 2d19ed7f2e..a889367f8f 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -1,13 +1,15 @@ import 'dart:async'; import 'dart:math'; -import 'package:sentry/sentry.dart'; -import 'package:sentry/src/transport/noop_transport.dart'; - import 'protocol.dart'; +import 'scope.dart'; 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/noop_transport.dart'; +import 'transport/transport.dart'; +import 'version.dart'; /// Logs crash reports and events to the Sentry.io service. abstract class SentryClient { diff --git a/dart/lib/src/sentry_io_client.dart b/dart/lib/src/sentry_io_client.dart index c195aa6f04..ae7975af81 100644 --- a/dart/lib/src/sentry_io_client.dart +++ b/dart/lib/src/sentry_io_client.dart @@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:sentry/sentry.dart'; +import 'protocol.dart'; /// A pure Dart client for Sentry.io crash reporting. import 'sentry_client.dart'; import 'sentry_options.dart'; +import 'version.dart'; SentryClient createSentryClient(SentryOptions options) => SentryIOClient(options); diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index fbef8248c6..384bfffa32 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -1,11 +1,12 @@ import 'package:http/http.dart'; -import 'package:sentry/sentry.dart'; -import 'package:sentry/src/transport/noop_transport.dart'; import 'diagnostic_logger.dart'; import 'hub.dart'; import 'protocol.dart'; +import 'transport/noop_transport.dart'; +import 'transport/transport.dart'; import 'utils.dart'; +import 'version.dart'; const defaultEnvironment = 'production'; diff --git a/dart/lib/src/transport/noop_transport.dart b/dart/lib/src/transport/noop_transport.dart index 1488b89258..5c14599bf5 100644 --- a/dart/lib/src/transport/noop_transport.dart +++ b/dart/lib/src/transport/noop_transport.dart @@ -1,6 +1,7 @@ import 'dart:async'; -import 'package:sentry/sentry.dart'; +import '../protocol.dart'; +import 'transport.dart'; class NoOpTransport implements Transport { @override diff --git a/dart/lib/src/transport/transport.dart b/dart/lib/src/transport/transport.dart index c0bf81641a..da7c637b8c 100644 --- a/dart/lib/src/transport/transport.dart +++ b/dart/lib/src/transport/transport.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'dart:convert'; import 'package:meta/meta.dart'; -import 'package:sentry/src/utils.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. From a7ca364a4a1e87bced6e05ec710c9da97deeb6d0 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Wed, 28 Oct 2020 08:59:27 +0100 Subject: [PATCH 24/34] fix: Unified API code review (#144) --- dart/example_web/pubspec.yaml | 2 - dart/lib/src/hub.dart | 2 +- dart/lib/src/scope.dart | 4 +- dart/lib/src/sentry_client.dart | 18 +-- dart/lib/src/sentry_options.dart | 6 +- dart/lib/src/transport/http_transport.dart | 138 ++++++++++++++++++ dart/lib/src/transport/noop_transport.dart | 3 - dart/lib/src/transport/transport.dart | 121 +-------------- dart/test/scope_test.dart | 2 +- dart/test/sentry_client_test.dart | 4 +- ...event_test.dart => sentry_event_test.dart} | 0 ...o_test.dart => sentry_io_client_test.dart} | 0 dart/test/stack_trace_test.dart | 21 --- dart/test/test_utils.dart | 56 ++++--- 14 files changed, 194 insertions(+), 183 deletions(-) create mode 100644 dart/lib/src/transport/http_transport.dart rename dart/test/{event_test.dart => sentry_event_test.dart} (100%) rename dart/test/{sentry_io_test.dart => sentry_io_client_test.dart} (100%) 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 { From 56ac62a5d6379d4d7826770abcd91ebe18b23bdf Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Wed, 28 Oct 2020 09:33:59 +0100 Subject: [PATCH 25/34] Ref : remove dsn from Transport public API (#145) --- CHANGELOG.md | 1 + dart/lib/src/transport/http_transport.dart | 7 ++- dart/test/test_utils.dart | 53 +++++++++++----------- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d9342b213..0ac46028e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ - Ref: rename sdk files accordely to their content - chore: new analysis options rules - Ref: rename the `throwable` argument name to `exception` in `captureEvents(...)` +- Ref : remove dsn from Transport public Api # `package:sentry` changelog diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 2bf0c8a52e..0330d5ecc3 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -13,8 +13,7 @@ import 'transport.dart'; class HttpTransport implements Transport { final SentryOptions _options; - @visibleForTesting - final Dsn dsn; + final Dsn _dsn; /// Use for browser stacktrace final String _origin; @@ -26,7 +25,7 @@ class HttpTransport implements Transport { HttpTransport({@required SentryOptions options, String origin}) : _options = options, _origin = origin, - dsn = Dsn.parse(options.dsn), + _dsn = Dsn.parse(options.dsn), _headers = _buildHeaders(sdkIdentifier: options.sdk.identifier) { _credentialBuilder = CredentialBuilder( dsn: Dsn.parse(options.dsn), @@ -46,7 +45,7 @@ class HttpTransport implements Transport { ); final response = await _options.httpClient.post( - dsn.postUri, + _dsn.postUri, headers: _credentialBuilder.configure(_headers), body: body, ); diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index e2ae805539..c338228c6b 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -8,7 +8,6 @@ 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'; @@ -89,8 +88,8 @@ Future testCaptureException( expect('$sentryId', 'testeventid'); } - final transport = options.transport as HttpTransport; - expect(postUri, transport.dsn.postUri); + final dsn = Dsn.parse(options.dsn); + expect(postUri, dsn.postUri); testHeaders( headers, @@ -181,16 +180,16 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { final options = SentryOptions(dsn: testDsn); final client = SentryClient(options); - final transport = options.transport as HttpTransport; + final dsn = Dsn.parse(options.dsn); - expect(transport.dsn.uri, Uri.parse(testDsn)); + expect(dsn.uri, Uri.parse(testDsn)); expect( - transport.dsn.postUri, + dsn.postUri, 'https://sentry.example.com/api/1/store/', ); - expect(transport.dsn.publicKey, 'public'); - expect(transport.dsn.secretKey, 'secret'); - expect(transport.dsn.projectId, '1'); + expect(dsn.publicKey, 'public'); + expect(dsn.secretKey, 'secret'); + expect(dsn.projectId, '1'); await client.close(); }); @@ -198,16 +197,16 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { final options = SentryOptions(dsn: _testDsnWithoutSecret); final client = SentryClient(options); - final transport = options.transport as HttpTransport; + final dsn = Dsn.parse(options.dsn); - expect(transport.dsn.uri, Uri.parse(_testDsnWithoutSecret)); + expect(dsn.uri, Uri.parse(_testDsnWithoutSecret)); expect( - transport.dsn.postUri, + dsn.postUri, 'https://sentry.example.com/api/1/store/', ); - expect(transport.dsn.publicKey, 'public'); - expect(transport.dsn.secretKey, null); - expect(transport.dsn.projectId, '1'); + expect(dsn.publicKey, 'public'); + expect(dsn.secretKey, null); + expect(dsn.projectId, '1'); await client.close(); }); @@ -215,32 +214,32 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { final options = SentryOptions(dsn: _testDsnWithPath); final client = SentryClient(options); - final transport = options.transport as HttpTransport; + final dsn = Dsn.parse(options.dsn); - expect(transport.dsn.uri, Uri.parse(_testDsnWithPath)); + expect(dsn.uri, Uri.parse(_testDsnWithPath)); expect( - transport.dsn.postUri, + dsn.postUri, 'https://sentry.example.com/path/api/1/store/', ); - expect(transport.dsn.publicKey, 'public'); - expect(transport.dsn.secretKey, 'secret'); - expect(transport.dsn.projectId, '1'); + expect(dsn.publicKey, 'public'); + expect(dsn.secretKey, 'secret'); + expect(dsn.projectId, '1'); await client.close(); }); test('can parse DSN with port', () async { final options = SentryOptions(dsn: _testDsnWithPort); final client = SentryClient(options); - final transport = options.transport as HttpTransport; + final dsn = Dsn.parse(options.dsn); - expect(transport.dsn.uri, Uri.parse(_testDsnWithPort)); + expect(dsn.uri, Uri.parse(_testDsnWithPort)); expect( - transport.dsn.postUri, + dsn.postUri, 'https://sentry.example.com:8888/api/1/store/', ); - expect(transport.dsn.publicKey, 'public'); - expect(transport.dsn.secretKey, 'secret'); - expect(transport.dsn.projectId, '1'); + expect(dsn.publicKey, 'public'); + expect(dsn.secretKey, 'secret'); + expect(dsn.projectId, '1'); await client.close(); }); test('sends client auth header without secret', () async { From 209f18ab7a6922fc7f505ac361fc6505c7bb73e2 Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Wed, 28 Oct 2020 10:15:54 +0100 Subject: [PATCH 26/34] Fix the pubspecs SDK constraints (#146) --- CHANGELOG.md | 1 + dart/pubspec.yaml | 2 +- flutter/pubspec.yaml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ac46028e0..1d31d7b27f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - chore: new analysis options rules - Ref: rename the `throwable` argument name to `exception` in `captureEvents(...)` - Ref : remove dsn from Transport public Api +- update the Dart sdk constraint # `package:sentry` changelog diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index 7c9f4d8624..b1bb5988e6 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -6,7 +6,7 @@ description: > homepage: https://github.com/getsentry/sentry-dart environment: - sdk: ^2.0.0 + sdk: ">=2.0.0 <3.0.0" dependencies: http: ^0.12.0 diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 6d6d7a2c8b..2731c9a194 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://docs.sentry.io/platforms/flutter/ repository: https://github.com/getsentry/sentry-dart environment: - sdk: ^2.7.0 + sdk: ">=2.0.0 <3.0.0" flutter: ^1.17.0 dependencies: @@ -13,7 +13,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - sentry: + sentry: #^4.0.0 uncomment before publishing path: ../dart dev_dependencies: From 108ac6caaf03d7121a55a78e57b872d2c2b5cd3c Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Wed, 28 Oct 2020 13:45:47 +0100 Subject: [PATCH 27/34] fix : throws on invalid dsn (#148) --- CHANGELOG.md | 1 + dart/example/main.dart | 2 -- dart/lib/src/protocol/dsn.dart | 14 +++++-------- dart/lib/src/sentry.dart | 3 +++ dart/lib/src/sentry_browser_client.dart | 11 +++------- dart/lib/src/sentry_io_client.dart | 8 +------- dart/lib/src/sentry_options.dart | 24 +++++++++++----------- dart/lib/src/transport/http_transport.dart | 2 +- 8 files changed, 26 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d31d7b27f..a3399e38b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ - Ref: rename the `throwable` argument name to `exception` in `captureEvents(...)` - Ref : remove dsn from Transport public Api - update the Dart sdk constraint +- fix : throws on invalid dsn # `package:sentry` changelog diff --git a/dart/example/main.dart b/dart/example/main.dart index 9559110536..2c7277e86e 100644 --- a/dart/example/main.dart +++ b/dart/example/main.dart @@ -81,8 +81,6 @@ Future main() async { } finally { await Sentry.close(); } - - /* TODO(rxlabz) Sentry CaptureMessage(message, level) */ } Future loadConfig() async { diff --git a/dart/lib/src/protocol/dsn.dart b/dart/lib/src/protocol/dsn.dart index 23c5b3edf2..04267f420a 100644 --- a/dart/lib/src/protocol/dsn.dart +++ b/dart/lib/src/protocol/dsn.dart @@ -46,15 +46,11 @@ class Dsn { final uri = Uri.parse(dsn); final userInfo = uri.userInfo.split(':'); - assert(() { - if (uri.pathSegments.isEmpty) { - throw ArgumentError( - 'Project ID not found in the URI path of the DSN URI: $dsn', - ); - } - - return true; - }()); + if (uri.pathSegments.isEmpty) { + throw ArgumentError( + 'Project ID not found in the URI path of the DSN URI: $dsn', + ); + } return Dsn( publicKey: userInfo[0], diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 0ee4365472..8478a1c4ac 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -120,6 +120,9 @@ class Sentry { return false; } + // try parsing the dsn + Dsn.parse(options.dsn); + // if logger os NoOp, let's set a logger that prints on the console if (options.debug && options.logger == noOpLogger) { options.logger = dartLogger; diff --git a/dart/lib/src/sentry_browser_client.dart b/dart/lib/src/sentry_browser_client.dart index 2b43ed7957..68b841e07a 100644 --- a/dart/lib/src/sentry_browser_client.dart +++ b/dart/lib/src/sentry_browser_client.dart @@ -5,10 +5,8 @@ /// A pure Dart client for Sentry.io crash reporting. import 'dart:html' show window; -import 'protocol.dart'; import 'sentry_client.dart'; import 'sentry_options.dart'; -import 'version.dart'; SentryClient createSentryClient(SentryOptions options) => SentryBrowserClient(options); @@ -25,13 +23,10 @@ class SentryBrowserClient extends SentryClient { /// /// If [httpClient] is provided, it is used instead of the default client to /// make HTTP calls to Sentry.io. This is useful in tests. - factory SentryBrowserClient(SentryOptions options) { - options.sdk ??= Sdk(name: sdkName, version: sdkVersion); - - // origin is necessary for sentry to resolve stacktrace - return SentryBrowserClient._(options); - } + factory SentryBrowserClient(SentryOptions options) => + SentryBrowserClient._(options); SentryBrowserClient._(SentryOptions options) + // origin is necessary for sentry to resolve stacktrace : super.base(options, origin: '${window.location.origin}/'); } diff --git a/dart/lib/src/sentry_io_client.dart b/dart/lib/src/sentry_io_client.dart index ae7975af81..50db00c167 100644 --- a/dart/lib/src/sentry_io_client.dart +++ b/dart/lib/src/sentry_io_client.dart @@ -2,12 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'protocol.dart'; - /// A pure Dart client for Sentry.io crash reporting. import 'sentry_client.dart'; import 'sentry_options.dart'; -import 'version.dart'; SentryClient createSentryClient(SentryOptions options) => SentryIOClient(options); @@ -15,10 +12,7 @@ SentryClient createSentryClient(SentryOptions options) => /// Logs crash reports and events to the Sentry.io service. class SentryIOClient extends SentryClient { /// Instantiates a client using [SentryOptions] - factory SentryIOClient(SentryOptions options) { - options.sdk ??= Sdk(name: sdkName, version: sdkVersion); - return SentryIOClient._(options); - } + factory SentryIOClient(SentryOptions options) => SentryIOClient._(options); SentryIOClient._(SentryOptions options) : super.base(options); } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 2e5a594cfa..d5e71c5e67 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -37,10 +37,10 @@ class SentryOptions { set httpClient(Client httpClient) => _httpClient = httpClient ?? _httpClient; - /// If [clock] is provided, it is used to get time instead of the system - /// clock. This is useful in tests. Should be an implementation of [ClockProvider]. ClockProvider _clock = getUtcDateTime; + /// If [clock] is provided, it is used to get time instead of the system + /// clock. This is useful in tests. Should be an implementation of [ClockProvider]. ClockProvider get clock => _clock; set clock(ClockProvider clock) => _clock = clock ?? _clock; @@ -48,42 +48,42 @@ class SentryOptions { /// This variable controls the total amount of breadcrumbs that should be captured Default is 100 int maxBreadcrumbs = 100; - /// Logger interface to log useful debugging information if debug is enabled Logger _logger = noOpLogger; + /// Logger interface to log useful debugging information if debug is enabled 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 = []; + /// 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. 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 + /// Code that provides middlewares, bindings or hooks into certain frameworks or environments, + /// along with code that inserts those bindings and activates them. 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; } + /// minimum LogLevel to be used if debug is enabled SentryLevel get diagnosticLevel => _diagnosticLevel; /// Sentry client name used for the HTTP authHeader and userAgent eg @@ -111,17 +111,17 @@ class SentryOptions { /// sent. Events are picked randomly. Default is null (disabled) double sampleRate; + final List _inAppExcludes = []; + /// 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 = []; - 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 = []; + /// A list of string prefixes of module names that belong to the app. This option takes precedence + /// over inAppExcludes. List get inAppIncludes => List.unmodifiable(_inAppIncludes); Transport _transport = NoOpTransport(); diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 0330d5ecc3..b56f443155 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -28,7 +28,7 @@ class HttpTransport implements Transport { _dsn = Dsn.parse(options.dsn), _headers = _buildHeaders(sdkIdentifier: options.sdk.identifier) { _credentialBuilder = CredentialBuilder( - dsn: Dsn.parse(options.dsn), + dsn: _dsn, clientId: options.sdk.identifier, clock: options.clock, ); From a81da3e1eb25c1b3faab9122f5494a79875003a0 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 28 Oct 2020 13:47:31 +0100 Subject: [PATCH 28/34] add comment to change the DSN --- dart/example/main.dart | 1 + dart/example_web/web/main.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/dart/example/main.dart b/dart/example/main.dart index 2c7277e86e..1bade23828 100644 --- a/dart/example/main.dart +++ b/dart/example/main.dart @@ -10,6 +10,7 @@ import 'event_example.dart'; /// Sends a test exception report to Sentry.io using this Dart client. Future main() async { + // Change the DSN const dsn = 'https://cb0fad6f5d4e42ebb9c956cb0463edc9@o447951.ingest.sentry.io/5428562'; diff --git a/dart/example_web/web/main.dart b/dart/example_web/web/main.dart index edbf86c9c3..5377e5f448 100644 --- a/dart/example_web/web/main.dart +++ b/dart/example_web/web/main.dart @@ -5,6 +5,7 @@ import 'package:sentry/sentry.dart'; import 'event.dart'; +// Change the DSN const dsn = 'https://cb0fad6f5d4e42ebb9c956cb0463edc9@o447951.ingest.sentry.io/5428562'; From c1812b7ce312b70701d401f0b6bfb67308b3d16c Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Wed, 28 Oct 2020 15:07:02 +0100 Subject: [PATCH 29/34] update the workflow to run both on vm and on browser (#150) --- .github/workflows/dart.yml | 4 ++-- CHANGELOG.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index f12f4ecc90..d803c83bc8 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -36,8 +36,8 @@ jobs: release-channel: ${{ matrix.sdk }} - uses: actions/checkout@v2 - run: pub get - - name: Test on Chrome - run: pub run test -p chrome test + - name: Test (VM and browser) + run: pub run test test - run: dartanalyzer --fatal-warnings ./ - run: dartfmt -n --set-exit-if-changed ./ package-analysis: diff --git a/CHANGELOG.md b/CHANGELOG.md index a3399e38b8..4eedb13a67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ - Ref : remove dsn from Transport public Api - update the Dart sdk constraint - fix : throws on invalid dsn +- GH Action ( CI ) run test on vm and on browser # `package:sentry` changelog From f597604c4a37b9c4289db8037280603d364ac356 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Wed, 28 Oct 2020 16:53:16 +0100 Subject: [PATCH 30/34] Bump sentry sdk to 4.0.0-alpha.1 (#151) --- .github/workflows/dart.yml | 3 +- CHANGELOG.md | 2 +- README.md | 10 ++-- dart/README.md | 96 +++++++++++++++++++++++++++++++++- dart/example/main.dart | 2 +- dart/example_web/web/main.dart | 2 +- dart/lib/src/version.dart | 2 +- dart/pubspec.yaml | 2 +- flutter/example/lib/main.dart | 1 + 9 files changed, 107 insertions(+), 13 deletions(-) mode change 120000 => 100644 dart/README.md diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index d803c83bc8..48d212d815 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -53,10 +53,9 @@ jobs: env: TOTAL: ${{ steps.analysis.outputs.total }} TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} - # TODO: Once 4.0.0 lands, change to 100 run: | PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) - if (( $PERCENTAGE < 90 )) + if (( $PERCENTAGE < 100 )) then echo Score too low! exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eedb13a67..9603454919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # `package:sentry` and `package:sentry-flutter` changelog -## 4.0.0 +## 4.0.0-alpha.1 - BREAKING CHANGE: Fixed context screenDensity is of type double #53 - BREAKING CHANGE: Fixed context screenDpi is of type int #58 diff --git a/README.md b/README.md index 5c792aaf8e..d56bb3acba 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ We also run CI against the Flutter `stable` and `beta` channels so you should be ##### Versions -Versions `3.0.1` and higher support [Flutter][flutter] (mobile, web, desktop), -command-line/server Dart VM, and [AngularDart][angular_sentry]. +Versions `3.0.1` and higher support `Flutter` (mobile, web, desktop), +command-line/server Dart VM, and `AngularDart`. Versions below `3.0.1` are deprecated. @@ -81,7 +81,7 @@ main() async { ##### Tips for catching errors - Use a `try/catch` block, like in the example above. -- Create a `Zone` with an error handler, e.g. using [runZonedGuarded][run_zoned_guarded]. +- Create a `Zone` with an error handler, e.g. using `runZonedGuarded`. ```dart var sentry = SentryClient(dsn: "https://..."); @@ -102,7 +102,7 @@ main() async { }, ); ``` -- For Flutter-specific errors (such as layout failures), use [FlutterError.onError][flutter_error]. For example: +- For Flutter-specific errors (such as layout failures), use `FlutterError.onError`. For example: ```dart var sentry = SentryClient(dsn: "https://..."); @@ -129,4 +129,4 @@ main() async { * [![Forum](https://img.shields.io/badge/forum-sentry-green.svg)](https://forum.sentry.io/c/sdks) * [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr) * [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](https://stackoverflow.com/questions/tagged/sentry) -* [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) +* [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) \ No newline at end of file diff --git a/dart/README.md b/dart/README.md deleted file mode 120000 index 32d46ee883..0000000000 --- a/dart/README.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/dart/README.md b/dart/README.md new file mode 100644 index 0000000000..f1faa6003b --- /dev/null +++ b/dart/README.md @@ -0,0 +1,95 @@ +

+ + + +
+

+ +Sentry SDK for Dart and Flutter +=========== + +##### Usage + +Sign up for a Sentry.io account and get a DSN at http://sentry.io. + +In your Dart code, import `package:sentry/sentry.dart` and initialize the Sentry SDK using the DSN issued by Sentry.io: + +```dart +import 'package:sentry/sentry.dart'; + +Sentry.init((options) => options.dsn = 'https://example@sentry.io/add-your-dsn-here'); +``` + +In an exception handler, call `captureException()`: + +```dart +import 'dart:async'; +import 'package:sentry/sentry.dart'; + +void main() async { + try { + aMethodThatMightFail(); + } catch (exception, stackTrace) { + await Sentry.captureException( + exception, + stackTrace: stackTrace, + ); + } +} +``` + +##### Tips for catching errors + +- Use a `try/catch` block, like in the example above. +- Create a `Zone` with an error handler, e.g. using `runZonedGuarded`. + +```dart +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:sentry/sentry.dart'; + +// Wrap your 'runApp(MyApp())' as follows: + +Future main() async { + runZonedGuarded>(() async { + runApp(MyApp()); + }, (exception, stackTrace) async { + await Sentry.captureException( + exception, + stackTrace: stackTrace, + ); + }); +} +``` + +- For Flutter-specific errors (such as layout failures), use `FlutterError.onError`. For example: + +```dart +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:sentry/sentry.dart'; + +// Wrap your 'runApp(MyApp())' as follows: + +Future main() async { + FlutterError.onError = (FlutterErrorDetails details) async { + await Sentry.captureException( + details.exception, + stackTrace: details.stack, + ); + }; +} +``` + +- Use `Isolate.current.addErrorListener` to capture uncaught errors + in the root zone. + +#### Resources + +* [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/flutter/) +* [![Forum](https://img.shields.io/badge/forum-sentry-green.svg)](https://forum.sentry.io/c/sdks) +* [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr) +* [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](https://stackoverflow.com/questions/tagged/sentry) +* [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) diff --git a/dart/example/main.dart b/dart/example/main.dart index 1bade23828..aabcf2ea54 100644 --- a/dart/example/main.dart +++ b/dart/example/main.dart @@ -10,7 +10,7 @@ import 'event_example.dart'; /// Sends a test exception report to Sentry.io using this Dart client. Future main() async { - // Change the DSN + // ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const dsn = 'https://cb0fad6f5d4e42ebb9c956cb0463edc9@o447951.ingest.sentry.io/5428562'; diff --git a/dart/example_web/web/main.dart b/dart/example_web/web/main.dart index 5377e5f448..1d25121f17 100644 --- a/dart/example_web/web/main.dart +++ b/dart/example_web/web/main.dart @@ -5,7 +5,7 @@ import 'package:sentry/sentry.dart'; import 'event.dart'; -// Change the DSN +// ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const dsn = 'https://cb0fad6f5d4e42ebb9c956cb0463edc9@o447951.ingest.sentry.io/5428562'; diff --git a/dart/lib/src/version.dart b/dart/lib/src/version.dart index 820cd11bdf..487efeaa8a 100644 --- a/dart/lib/src/version.dart +++ b/dart/lib/src/version.dart @@ -11,7 +11,7 @@ library version; import 'utils.dart'; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '4.0.0'; +const String sdkVersion = '4.0.0-alpha.1'; String get sdkName => isWeb ? _browserSdkName : _ioSdkName; diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index b1bb5988e6..0060ea021b 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 4.0.0 +version: 4.0.0-alpha.1 description: > A crash reporting library for Dart that sends crash reports to Sentry.io. This library supports Dart Native, and Flutter for mobile, web, and desktop. diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 7c9dc8f024..c0048ba2bf 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -11,6 +11,7 @@ import 'package:universal_platform/universal_platform.dart'; const String _release = String.fromEnvironment('SENTRY_RELEASE', defaultValue: 'unknown'); +// ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io const String exampleDsn = 'https://cb0fad6f5d4e42ebb9c956cb0463edc9@o447951.ingest.sentry.io/5428562'; From 6d9caff5ed82e808ca3aa12e660bc285613cb097 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 28 Oct 2020 17:07:59 +0100 Subject: [PATCH 31/34] remove TODOs --- dart/lib/src/protocol/breadcrumb.dart | 2 +- dart/lib/src/protocol/sentry_event.dart | 9 ++------- dart/lib/src/protocol/user.dart | 4 ++-- dart/lib/src/sentry_browser_client.dart | 5 ----- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/dart/lib/src/protocol/breadcrumb.dart b/dart/lib/src/protocol/breadcrumb.dart index 03c41dfb44..57e99d8421 100644 --- a/dart/lib/src/protocol/breadcrumb.dart +++ b/dart/lib/src/protocol/breadcrumb.dart @@ -1,7 +1,7 @@ import '../utils.dart'; import 'sentry_level.dart'; -/// Structed data to describe more information pior to the event [captured][SentryClient.captureEvent]. +/// Structed data to describe more information pior to the event [captured][Sentry.captureEvent]. /// /// The outgoing JSON representation is: /// diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index e049baf392..15e30ec770 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -112,7 +112,7 @@ class SentryEvent { /// Information about the current user. /// /// The value in this field overrides the user context - /// set in [SentryClient.user] for this logged event. + /// set in [Scope.user] for this logged event. final User user; /// The context interfaces provide additional context data. @@ -133,15 +133,12 @@ class SentryEvent { /// // A completely custom fingerprint: /// var custom = ['foo', 'bar', 'baz']; /// // A fingerprint that supplements the default one with value 'foo': - /// var supplemented = [Event.defaultFingerprint, 'foo']; + /// var supplemented = [SentryEvent.defaultFingerprint, 'foo']; final List fingerprint; /// The SDK Interface describes the Sentry SDK and its configuration used to capture and transmit an event. final Sdk sdk; - // TODO: Request and DebugMeta Interface - // TODO: do we need a Threads interface? - SentryEvent copyWith({ SentryId eventId, DateTime timestamp, @@ -240,7 +237,6 @@ class SentryEvent { } if (exception != null) { - // TODO: create Exception and Mechanism Interface class json['exception'] = [ { 'type': '${exception.runtimeType}', @@ -248,7 +244,6 @@ class SentryEvent { } ]; if (exception is Error && exception.stackTrace != null) { - // TODO: create Stack Trace and Frame Interface json['stacktrace'] = { 'frames': encodeStackTrace( exception.stackTrace, diff --git a/dart/lib/src/protocol/user.dart b/dart/lib/src/protocol/user.dart index 1dd077b55e..24b17c5dca 100644 --- a/dart/lib/src/protocol/user.dart +++ b/dart/lib/src/protocol/user.dart @@ -1,8 +1,8 @@ /// Describes the current user associated with the application, such as the /// currently signed in user. /// -/// The user can be specified globally in the [SentryClient.user] field, -/// or per event in the [Event.user] field. +/// The user can be specified globally in the [Scope.user] field, +/// or per event in the [SentryEvent.user] field. /// /// You should provide at least either an [id] (a unique identifier for an /// authenticated user) or [ipAddress] (their IP address). diff --git a/dart/lib/src/sentry_browser_client.dart b/dart/lib/src/sentry_browser_client.dart index 68b841e07a..fe3401c26d 100644 --- a/dart/lib/src/sentry_browser_client.dart +++ b/dart/lib/src/sentry_browser_client.dart @@ -16,11 +16,6 @@ class SentryBrowserClient extends SentryClient { /// Instantiates a client using [dsn] issued to your project by Sentry.io as /// the endpoint for submitting events. /// - /// [environmentAttributes] contain event attributes that do not change over - /// the course of a program's lifecycle. These attributes will be added to - /// all events captured via this client. The following attributes often fall - /// under this category: [Event.serverName], [Event.release], [Event.environment]. - /// /// If [httpClient] is provided, it is used instead of the default client to /// make HTTP calls to Sentry.io. This is useful in tests. factory SentryBrowserClient(SentryOptions options) => From 32debd726aa7fb952d370a89550f8b1c8156f7ca Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Wed, 28 Oct 2020 20:50:15 +0100 Subject: [PATCH 32/34] fix the web example pubspec (#153) --- CHANGELOG.md | 1 + dart/example_web/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9603454919..f12f1b2a4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - update the Dart sdk constraint - fix : throws on invalid dsn - GH Action ( CI ) run test on vm and on browser +- fix: example_web pubspec # `package:sentry` changelog diff --git a/dart/example_web/pubspec.yaml b/dart/example_web/pubspec.yaml index e409ff6cdc..533d1135d6 100644 --- a/dart/example_web/pubspec.yaml +++ b/dart/example_web/pubspec.yaml @@ -6,7 +6,7 @@ environment: dependencies: sentry: - path: ../.. + path: ../ dev_dependencies: build_runner: ^1.10.0 From c80365f473a018e36504b2d09d5721b8dc2ebc72 Mon Sep 17 00:00:00 2001 From: Erick Ghaumez Date: Thu, 29 Oct 2020 09:05:42 +0100 Subject: [PATCH 33/34] Ref: remove platform specific clients to use SentryClient (#152) --- CHANGELOG.md | 1 + dart/lib/browser.dart | 6 ---- dart/lib/io.dart | 6 ---- dart/lib/src/sentry_browser_client.dart | 32 ---------------------- dart/lib/src/sentry_client.dart | 18 ++++-------- dart/lib/src/sentry_io_client.dart | 18 ------------ dart/lib/src/transport/http_transport.dart | 9 ++---- dart/lib/src/transport/noop_origin.dart | 1 + dart/lib/src/transport/origin.dart | 3 ++ dart/test/sentry_browser_test.dart | 11 +------- dart/test/sentry_io_client_test.dart | 11 +------- 11 files changed, 15 insertions(+), 101 deletions(-) delete mode 100644 dart/lib/browser.dart delete mode 100644 dart/lib/io.dart delete mode 100644 dart/lib/src/sentry_browser_client.dart delete mode 100644 dart/lib/src/sentry_io_client.dart create mode 100644 dart/lib/src/transport/noop_origin.dart create mode 100644 dart/lib/src/transport/origin.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f12f1b2a4c..6937bf3ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - update the Dart sdk constraint - fix : throws on invalid dsn - GH Action ( CI ) run test on vm and on browser +- ref: remove SentryBrowserClient & SentryIOClient, all platform can use SentryClient - fix: example_web pubspec # `package:sentry` changelog diff --git a/dart/lib/browser.dart b/dart/lib/browser.dart deleted file mode 100644 index 982e63a54a..0000000000 --- a/dart/lib/browser.dart +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -export 'src/sentry_browser_client.dart'; -export 'src/version.dart'; diff --git a/dart/lib/io.dart b/dart/lib/io.dart deleted file mode 100644 index e47a9d626b..0000000000 --- a/dart/lib/io.dart +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -export 'src/sentry_io_client.dart'; -export 'src/version.dart'; diff --git a/dart/lib/src/sentry_browser_client.dart b/dart/lib/src/sentry_browser_client.dart deleted file mode 100644 index 68b841e07a..0000000000 --- a/dart/lib/src/sentry_browser_client.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A pure Dart client for Sentry.io crash reporting. -import 'dart:html' show window; - -import 'sentry_client.dart'; -import 'sentry_options.dart'; - -SentryClient createSentryClient(SentryOptions options) => - SentryBrowserClient(options); - -/// Logs crash reports and events to the Sentry.io service. -class SentryBrowserClient extends SentryClient { - /// Instantiates a client using [dsn] issued to your project by Sentry.io as - /// the endpoint for submitting events. - /// - /// [environmentAttributes] contain event attributes that do not change over - /// the course of a program's lifecycle. These attributes will be added to - /// all events captured via this client. The following attributes often fall - /// under this category: [Event.serverName], [Event.release], [Event.environment]. - /// - /// If [httpClient] is provided, it is used instead of the default client to - /// make HTTP calls to Sentry.io. This is useful in tests. - factory SentryBrowserClient(SentryOptions options) => - SentryBrowserClient._(options); - - SentryBrowserClient._(SentryOptions options) - // origin is necessary for sentry to resolve stacktrace - : super.base(options, origin: '${window.location.origin}/'); -} diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 74be045723..9bb8d75c9f 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -3,30 +3,22 @@ import 'dart:math'; import 'protocol.dart'; import 'scope.dart'; -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 'version.dart'; /// Logs crash reports and events to the Sentry.io service. -abstract class SentryClient { - /// Creates a new platform appropriate client. - /// - /// Creates an `SentryIOClient` if `dart:io` is available and a `SentryBrowserClient` if - /// `dart:html` is available, otherwise it will throw an unsupported error. - factory SentryClient(SentryOptions options) => createSentryClient(options); - - SentryClient.base(this._options, {String origin}) { +class SentryClient { + /// Instantiates a client using [SentryOptions] + SentryClient(SentryOptions options) : _options = options { _random = _options.sampleRate == null ? null : Random(); if (_options.transport is NoOpTransport) { - _options.transport = HttpTransport(options: _options, origin: origin); + _options.transport = HttpTransport(options: _options); } } - SentryOptions _options; + final SentryOptions _options; Random _random; diff --git a/dart/lib/src/sentry_io_client.dart b/dart/lib/src/sentry_io_client.dart deleted file mode 100644 index 50db00c167..0000000000 --- a/dart/lib/src/sentry_io_client.dart +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A pure Dart client for Sentry.io crash reporting. -import 'sentry_client.dart'; -import 'sentry_options.dart'; - -SentryClient createSentryClient(SentryOptions options) => - SentryIOClient(options); - -/// Logs crash reports and events to the Sentry.io service. -class SentryIOClient extends SentryClient { - /// Instantiates a client using [SentryOptions] - factory SentryIOClient(SentryOptions options) => SentryIOClient._(options); - - SentryIOClient._(SentryOptions options) : super.base(options); -} diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index b56f443155..eefe2b325f 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -7,6 +7,7 @@ import '../protocol.dart'; import '../sentry_options.dart'; import '../utils.dart'; import 'noop_encode.dart' if (dart.library.io) 'encode.dart'; +import 'noop_origin.dart' if (dart.library.html) 'origin.dart'; import 'transport.dart'; /// A transport is in charge of sending the event to the Sentry server. @@ -15,16 +16,12 @@ class HttpTransport implements Transport { final Dsn _dsn; - /// Use for browser stacktrace - final String _origin; - CredentialBuilder _credentialBuilder; final Map _headers; - HttpTransport({@required SentryOptions options, String origin}) + HttpTransport({@required SentryOptions options}) : _options = options, - _origin = origin, _dsn = Dsn.parse(options.dsn), _headers = _buildHeaders(sdkIdentifier: options.sdk.identifier) { _credentialBuilder = CredentialBuilder( @@ -36,7 +33,7 @@ class HttpTransport implements Transport { @override Future send(SentryEvent event) async { - final data = event.toJson(origin: _origin); + final data = event.toJson(origin: eventOrigin); final body = _bodyEncoder( data, diff --git a/dart/lib/src/transport/noop_origin.dart b/dart/lib/src/transport/noop_origin.dart new file mode 100644 index 0000000000..6cecb4be62 --- /dev/null +++ b/dart/lib/src/transport/noop_origin.dart @@ -0,0 +1 @@ +String eventOrigin; diff --git a/dart/lib/src/transport/origin.dart b/dart/lib/src/transport/origin.dart new file mode 100644 index 0000000000..347709496c --- /dev/null +++ b/dart/lib/src/transport/origin.dart @@ -0,0 +1,3 @@ +import 'dart:html'; + +String get eventOrigin => '${window.location.origin}/'; diff --git a/dart/test/sentry_browser_test.dart b/dart/test/sentry_browser_test.dart index e4c4711ce4..8524019467 100644 --- a/dart/test/sentry_browser_test.dart +++ b/dart/test/sentry_browser_test.dart @@ -2,19 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @TestOn('browser') -import 'package:sentry/browser.dart'; -import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; import 'test_utils.dart'; void main() { - group('SentryBrowserClient', () { - test('SentryClient constructor build browser client', () { - final client = SentryClient(SentryOptions(dsn: testDsn)); - expect(client is SentryBrowserClient, isTrue); - }); - - runTest(isWeb: true); - }); + runTest(isWeb: true); } diff --git a/dart/test/sentry_io_client_test.dart b/dart/test/sentry_io_client_test.dart index 6e453c90c5..b2479a27c8 100644 --- a/dart/test/sentry_io_client_test.dart +++ b/dart/test/sentry_io_client_test.dart @@ -4,19 +4,10 @@ @TestOn('vm') import 'dart:io'; -import 'package:sentry/sentry.dart'; -import 'package:sentry/src/sentry_io_client.dart'; import 'package:test/test.dart'; import 'test_utils.dart'; void main() { - group(SentryIOClient, () { - test('SentryClient constructor build io client', () { - final client = SentryClient(SentryOptions(dsn: testDsn)); - expect(client is SentryIOClient, isTrue); - }); - - runTest(gzip: gzip); - }); + runTest(gzip: gzip); } From c3ab59feb6fee21c76ae200c8e2c1e3ac7bd3ba2 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 29 Oct 2020 09:37:31 +0100 Subject: [PATCH 34/34] rewrite changelog --- CHANGELOG.md | 66 ++++++++++++++-------------------------------------- 1 file changed, 18 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6937bf3ee7..dc9060519f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,57 +1,27 @@ # `package:sentry` and `package:sentry-flutter` changelog -## 4.0.0-alpha.1 +## 4.0.0 +- Development -- BREAKING CHANGE: Fixed context screenDensity is of type double #53 -- BREAKING CHANGE: Fixed context screenDpi is of type int #58 -- BREAKING CHANGE: Renamed capture method to captureEvent #64 -- BREAKING CHANGE: `package:http` min version bumped to 0.12.0 #104 -- BREAKING CHANGE: replace the `package:usage` by `package:uuid` #94 -- BREAKING CHANGE: `Event.message` must now be an instance of `Message` -- BREAKING CHANGE: SentryClient must now be initialized with a SentryOptions #118 -- By default no logger it set #63 -- Added missing Contexts to Event.copyWith() #62 -- remove the `package:args` dependency #94 -- move the `package:pedantic` to dev depencies #94 -- Added GH Action Changelog verifier #95 -- Added GH Action (CI) for Dart #97 -- new Dart code file structure #96 -- 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 -- 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 -- Ref: Hub passes the Scope to SentryClient #114 -- Feature: sentry options #116 -- Ref: SentryId generates UUID #119 -- Ref: Event now is SentryEvent and added GPU #121 -- Feat: before breadcrumb and scope ref. #122 -- Ref: Sentry init with null and empty DSN and close method #126 -- Ref: Hint is passed across Sentry static class, Hub and Client #124 -- Ref: Remove stackFrameFilter in favor of beforeSendCallback #125 -- Ref: added Transport #123 -- Feat: apply sample rate -- Ref: execute before send callback -- Feat: add lastEventId to the Sentry static API -- Feat: addBreadcrumb on Static API -- Add a Dart web example -- Fix: Integrations are executed on Hub creation -- Fix: NoOp encode for Web -- Fix: Breadcrumb data should accept serializable types and not only String values -- Ref: added Scope.applyToEvent -- Ref: rename sdk files accordely to their content -- chore: new analysis options rules -- Ref: rename the `throwable` argument name to `exception` in `captureEvents(...)` -- Ref : remove dsn from Transport public Api -- update the Dart sdk constraint -- fix : throws on invalid dsn -- GH Action ( CI ) run test on vm and on browser -- ref: remove SentryBrowserClient & SentryIOClient, all platform can use SentryClient -- fix: example_web pubspec # `package:sentry` changelog +## 4.0.0-alpha.1 + +First Release of Sentry's new SDK for Dart/Flutter. + +New features not offered by <= v3.0.0: + +- Sentry's [Unified API](https://develop.sentry.dev/sdk/unified-api/). +- Complete Sentry [Protocol](https://develop.sentry.dev/sdk/event-payloads/) available. +- Docs and Migration is under review on this [PR](https://github.com/getsentry/sentry-docs/pull/2599) +- For all the breaking changes follow this [PR](https://github.com/getsentry/sentry-dart/pull/117), they'll be soon available on the Migration page. + +Packages were released on [pubdev](https://pub.dev/packages/sentry) + +We'd love to get feedback and we'll work in getting the GA 4.0.0 out soon. +Until then, the stable SDK offered by Sentry is at version [3.0.1](https://github.com/getsentry/sentry-dart/releases/tag/3.0.1) + ## 3.0.1 - Add support for Contexts in Sentry events