From 54048ea9e201581c7e691b1ecc67773039c58be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 28 Mar 2022 15:13:17 +0200 Subject: [PATCH 1/8] Introduce client report recorder --- .../lib/src/client_reports/client_report.dart | 11 +++ .../client_report_recorder.dart | 53 +++++++++++ .../src/client_reports/discarded_event.dart | 13 +++ dart/lib/src/client_reports/outcome.dart | 15 +++ .../client_report_recorder_test.dart | 94 +++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 dart/lib/src/client_reports/client_report.dart create mode 100644 dart/lib/src/client_reports/client_report_recorder.dart create mode 100644 dart/lib/src/client_reports/discarded_event.dart create mode 100644 dart/lib/src/client_reports/outcome.dart create mode 100644 dart/test/client_reports/client_report_recorder_test.dart diff --git a/dart/lib/src/client_reports/client_report.dart b/dart/lib/src/client_reports/client_report.dart new file mode 100644 index 0000000000..fdf9e6ff9e --- /dev/null +++ b/dart/lib/src/client_reports/client_report.dart @@ -0,0 +1,11 @@ +import 'package:meta/meta.dart'; + +import 'discarded_event.dart'; + +@internal +class ClientReport { + ClientReport(this.timestamp, this.discardedEvents); + + final DateTime? timestamp; + final List discardedEvents; +} diff --git a/dart/lib/src/client_reports/client_report_recorder.dart b/dart/lib/src/client_reports/client_report_recorder.dart new file mode 100644 index 0000000000..f6550f464d --- /dev/null +++ b/dart/lib/src/client_reports/client_report_recorder.dart @@ -0,0 +1,53 @@ +import 'package:meta/meta.dart'; + +import '../utils.dart'; +import 'client_report.dart'; +import 'discarded_event.dart'; +import 'outcome.dart'; +import '../transport/rate_limit_category.dart'; + +@internal +class ClientReportRecorder { + ClientReportRecorder([this._dateTimeProvider = getUtcDateTime]); + + final DateTime Function() _dateTimeProvider; + final Map<_ClientReportKey, int> _quantities = {}; + + void recordLostEvent(final Outcome reason, final RateLimitCategory category) { + final key = _ClientReportKey(reason, category); + var current = _quantities[key] ?? 0; + _quantities[key] = current + 1; + } + + ClientReport? flush() { + if (_quantities.isEmpty) { + return null; + } + + final events = _quantities.keys.map((key) { + final quantity = _quantities[key] ?? 0; + return DiscardedEvent(key.reason, key.category, quantity); + }).toList(); + + _quantities.clear(); + + return ClientReport(_dateTimeProvider(), events); + } +} + +class _ClientReportKey { + _ClientReportKey(this.reason, this.category); + + final Outcome reason; + final RateLimitCategory category; + + @override + int get hashCode => Object.hash(reason, category); + + @override + bool operator ==(dynamic other) { + return other is _ClientReportKey && + other.reason == reason && + other.category == category; + } +} diff --git a/dart/lib/src/client_reports/discarded_event.dart b/dart/lib/src/client_reports/discarded_event.dart new file mode 100644 index 0000000000..c12b4c4f9a --- /dev/null +++ b/dart/lib/src/client_reports/discarded_event.dart @@ -0,0 +1,13 @@ +import 'package:meta/meta.dart'; + +import 'outcome.dart'; +import '../transport/rate_limit_category.dart'; + +@internal +class DiscardedEvent { + DiscardedEvent(this.reason, this.category, this.quantity); + + final Outcome reason; + final RateLimitCategory category; + final int quantity; +} diff --git a/dart/lib/src/client_reports/outcome.dart b/dart/lib/src/client_reports/outcome.dart new file mode 100644 index 0000000000..122fa4efce --- /dev/null +++ b/dart/lib/src/client_reports/outcome.dart @@ -0,0 +1,15 @@ +import 'package:meta/meta.dart'; + +@internal +enum Outcome { ratelimitBackoff, networkError } + +extension OutcomeExtension on Outcome { + String toStringValue() { + switch (this) { + case Outcome.ratelimitBackoff: + return 'ratelimit_backoff'; + case Outcome.networkError: + return 'network_error'; + } + } +} diff --git a/dart/test/client_reports/client_report_recorder_test.dart b/dart/test/client_reports/client_report_recorder_test.dart new file mode 100644 index 0000000000..c8b0710f24 --- /dev/null +++ b/dart/test/client_reports/client_report_recorder_test.dart @@ -0,0 +1,94 @@ +import 'package:sentry/src/client_reports/outcome.dart'; +import 'package:sentry/src/transport/rate_limit_category.dart'; +import 'package:test/test.dart'; + +import 'package:sentry/src/client_reports/client_report_recorder.dart'; + +void main() { + group(ClientReportRecorder, () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('flush returns null when there was nothing recorded', () { + final sut = fixture.getSut(); + + final clientReport = sut.flush(); + + expect(clientReport, null); + }); + + test('flush returns client report with current date', () { + final sut = fixture.getSut(); + + sut.recordLostEvent(Outcome.ratelimitBackoff, RateLimitCategory.error); + + final clientReport = sut.flush(); + + expect(clientReport?.timestamp, DateTime(0)); + }); + + test('record lost event', () { + final sut = fixture.getSut(); + + sut.recordLostEvent(Outcome.ratelimitBackoff, RateLimitCategory.error); + sut.recordLostEvent(Outcome.ratelimitBackoff, RateLimitCategory.error); + + final clientReport = sut.flush(); + + final event = clientReport?.discardedEvents + .firstWhere((element) => element.category == RateLimitCategory.error); + + expect(event?.reason, Outcome.ratelimitBackoff); + expect(event?.category, RateLimitCategory.error); + expect(event?.quantity, 2); + }); + + test('record outcomes with different categories recorded separately', () { + final sut = fixture.getSut(); + + sut.recordLostEvent(Outcome.ratelimitBackoff, RateLimitCategory.error); + sut.recordLostEvent( + Outcome.ratelimitBackoff, RateLimitCategory.transaction); + + final clientReport = sut.flush(); + + final first = clientReport?.discardedEvents + .firstWhere((event) => event.category == RateLimitCategory.error); + + final second = clientReport?.discardedEvents.firstWhere( + (event) => event.category == RateLimitCategory.transaction); + + expect(first?.reason, Outcome.ratelimitBackoff); + expect(first?.category, RateLimitCategory.error); + expect(first?.quantity, 1); + + expect(second?.reason, Outcome.ratelimitBackoff); + expect(second?.category, RateLimitCategory.transaction); + expect(second?.quantity, 1); + }); + + test('calling flush multiple times returns null', () { + final sut = fixture.getSut(); + + sut.recordLostEvent(Outcome.ratelimitBackoff, RateLimitCategory.error); + + sut.flush(); + final clientReport = sut.flush(); + + expect(clientReport, null); + }); + }); +} + +class Fixture { + final _dateTimeProvider = () { + return DateTime(0); + }; + + ClientReportRecorder getSut() { + return ClientReportRecorder(_dateTimeProvider); + } +} From cd0dadf46b88c6475f862911ce8112ab0564851e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 28 Mar 2022 15:20:08 +0200 Subject: [PATCH 2/8] Rename class --- .../lib/src/client_reports/client_report_recorder.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dart/lib/src/client_reports/client_report_recorder.dart b/dart/lib/src/client_reports/client_report_recorder.dart index f6550f464d..a3069be890 100644 --- a/dart/lib/src/client_reports/client_report_recorder.dart +++ b/dart/lib/src/client_reports/client_report_recorder.dart @@ -11,10 +11,10 @@ class ClientReportRecorder { ClientReportRecorder([this._dateTimeProvider = getUtcDateTime]); final DateTime Function() _dateTimeProvider; - final Map<_ClientReportKey, int> _quantities = {}; + final Map<_QuantityKey, int> _quantities = {}; void recordLostEvent(final Outcome reason, final RateLimitCategory category) { - final key = _ClientReportKey(reason, category); + final key = _QuantityKey(reason, category); var current = _quantities[key] ?? 0; _quantities[key] = current + 1; } @@ -35,8 +35,8 @@ class ClientReportRecorder { } } -class _ClientReportKey { - _ClientReportKey(this.reason, this.category); +class _QuantityKey { + _QuantityKey(this.reason, this.category); final Outcome reason; final RateLimitCategory category; @@ -46,7 +46,7 @@ class _ClientReportKey { @override bool operator ==(dynamic other) { - return other is _ClientReportKey && + return other is _QuantityKey && other.reason == reason && other.category == category; } From 10c8bb5ae4740bc673475dd49bb3837cd633112e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Tue, 29 Mar 2022 14:54:23 +0200 Subject: [PATCH 3/8] use ClockProvider typedef --- dart/lib/src/client_reports/client_report_recorder.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dart/lib/src/client_reports/client_report_recorder.dart b/dart/lib/src/client_reports/client_report_recorder.dart index a3069be890..1a03aed443 100644 --- a/dart/lib/src/client_reports/client_report_recorder.dart +++ b/dart/lib/src/client_reports/client_report_recorder.dart @@ -1,6 +1,6 @@ import 'package:meta/meta.dart'; -import '../utils.dart'; +import '../sentry_options.dart'; import 'client_report.dart'; import 'discarded_event.dart'; import 'outcome.dart'; @@ -8,9 +8,9 @@ import '../transport/rate_limit_category.dart'; @internal class ClientReportRecorder { - ClientReportRecorder([this._dateTimeProvider = getUtcDateTime]); + ClientReportRecorder(this._clock); - final DateTime Function() _dateTimeProvider; + final ClockProvider _clock; final Map<_QuantityKey, int> _quantities = {}; void recordLostEvent(final Outcome reason, final RateLimitCategory category) { @@ -31,7 +31,7 @@ class ClientReportRecorder { _quantities.clear(); - return ClientReport(_dateTimeProvider(), events); + return ClientReport(_clock(), events); } } From 75ab5bbc5ac35e34eed60a2adc0016071cd531f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Tue, 29 Mar 2022 14:55:13 +0200 Subject: [PATCH 4/8] use non-growable list --- dart/lib/src/client_reports/client_report_recorder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart/lib/src/client_reports/client_report_recorder.dart b/dart/lib/src/client_reports/client_report_recorder.dart index 1a03aed443..b81026ac37 100644 --- a/dart/lib/src/client_reports/client_report_recorder.dart +++ b/dart/lib/src/client_reports/client_report_recorder.dart @@ -27,7 +27,7 @@ class ClientReportRecorder { final events = _quantities.keys.map((key) { final quantity = _quantities[key] ?? 0; return DiscardedEvent(key.reason, key.category, quantity); - }).toList(); + }).toList(growable: false); _quantities.clear(); From 659f5b2079cf39bda23e3aae38f590e84caabf5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 4 Apr 2022 14:04:01 +0200 Subject: [PATCH 5/8] rename classes --- .../client_report_recorder.dart | 9 ++-- .../src/client_reports/discarded_event.dart | 6 +-- dart/lib/src/client_reports/outcome.dart | 8 +-- dart/lib/src/transport/data_category.dart | 54 +++++++++++++++++++ dart/lib/src/transport/rate_limit.dart | 6 +-- .../src/transport/rate_limit_category.dart | 54 ------------------- dart/lib/src/transport/rate_limit_parser.dart | 13 ++--- dart/lib/src/transport/rate_limiter.dart | 27 +++++----- .../client_report_recorder_test.dart | 34 ++++++------ .../test/protocol/rate_limit_parser_test.dart | 28 +++++----- 10 files changed, 118 insertions(+), 121 deletions(-) create mode 100644 dart/lib/src/transport/data_category.dart delete mode 100644 dart/lib/src/transport/rate_limit_category.dart diff --git a/dart/lib/src/client_reports/client_report_recorder.dart b/dart/lib/src/client_reports/client_report_recorder.dart index b81026ac37..e5194e4f03 100644 --- a/dart/lib/src/client_reports/client_report_recorder.dart +++ b/dart/lib/src/client_reports/client_report_recorder.dart @@ -4,7 +4,7 @@ import '../sentry_options.dart'; import 'client_report.dart'; import 'discarded_event.dart'; import 'outcome.dart'; -import '../transport/rate_limit_category.dart'; +import '../transport/data_category.dart'; @internal class ClientReportRecorder { @@ -13,7 +13,8 @@ class ClientReportRecorder { final ClockProvider _clock; final Map<_QuantityKey, int> _quantities = {}; - void recordLostEvent(final Outcome reason, final RateLimitCategory category) { + void recordLostEvent( + final DiscardReason reason, final DataCategory category) { final key = _QuantityKey(reason, category); var current = _quantities[key] ?? 0; _quantities[key] = current + 1; @@ -38,8 +39,8 @@ class ClientReportRecorder { class _QuantityKey { _QuantityKey(this.reason, this.category); - final Outcome reason; - final RateLimitCategory category; + final DiscardReason reason; + final DataCategory category; @override int get hashCode => Object.hash(reason, category); diff --git a/dart/lib/src/client_reports/discarded_event.dart b/dart/lib/src/client_reports/discarded_event.dart index c12b4c4f9a..33a087939d 100644 --- a/dart/lib/src/client_reports/discarded_event.dart +++ b/dart/lib/src/client_reports/discarded_event.dart @@ -1,13 +1,13 @@ import 'package:meta/meta.dart'; import 'outcome.dart'; -import '../transport/rate_limit_category.dart'; +import '../transport/data_category.dart'; @internal class DiscardedEvent { DiscardedEvent(this.reason, this.category, this.quantity); - final Outcome reason; - final RateLimitCategory category; + final DiscardReason reason; + final DataCategory category; final int quantity; } diff --git a/dart/lib/src/client_reports/outcome.dart b/dart/lib/src/client_reports/outcome.dart index 122fa4efce..deefb3b20f 100644 --- a/dart/lib/src/client_reports/outcome.dart +++ b/dart/lib/src/client_reports/outcome.dart @@ -1,14 +1,14 @@ import 'package:meta/meta.dart'; @internal -enum Outcome { ratelimitBackoff, networkError } +enum DiscardReason { ratelimitBackoff, networkError } -extension OutcomeExtension on Outcome { +extension OutcomeExtension on DiscardReason { String toStringValue() { switch (this) { - case Outcome.ratelimitBackoff: + case DiscardReason.ratelimitBackoff: return 'ratelimit_backoff'; - case Outcome.networkError: + case DiscardReason.networkError: return 'network_error'; } } diff --git a/dart/lib/src/transport/data_category.dart b/dart/lib/src/transport/data_category.dart new file mode 100644 index 0000000000..fa9e680445 --- /dev/null +++ b/dart/lib/src/transport/data_category.dart @@ -0,0 +1,54 @@ +/// Different category types of data sent to Sentry. Used for rate limiting and client reports. +enum DataCategory { + all, + rate_limit_default, // default + error, + session, + transaction, + attachment, + security, + unknown +} + +extension DataCategoryExtension on DataCategory { + static DataCategory fromStringValue(String stringValue) { + switch (stringValue) { + case '__all__': + return DataCategory.all; + case 'default': + return DataCategory.rate_limit_default; + case 'error': + return DataCategory.error; + case 'session': + return DataCategory.session; + case 'transaction': + return DataCategory.transaction; + case 'attachment': + return DataCategory.attachment; + case 'security': + return DataCategory.security; + } + return DataCategory.unknown; + } + + String toStringValue() { + switch (this) { + case DataCategory.all: + return '__all__'; + case DataCategory.rate_limit_default: + return 'default'; + case DataCategory.error: + return 'error'; + case DataCategory.session: + return 'session'; + case DataCategory.transaction: + return 'transaction'; + case DataCategory.attachment: + return 'attachment'; + case DataCategory.security: + return 'security'; + case DataCategory.unknown: + return 'unknown'; + } + } +} diff --git a/dart/lib/src/transport/rate_limit.dart b/dart/lib/src/transport/rate_limit.dart index e95285f716..8f41b91d81 100644 --- a/dart/lib/src/transport/rate_limit.dart +++ b/dart/lib/src/transport/rate_limit.dart @@ -1,9 +1,9 @@ -import 'rate_limit_category.dart'; +import 'data_category.dart'; -/// `RateLimit` containing limited `RateLimitCategory` and duration in milliseconds. +/// `RateLimit` containing limited `DataCategory` and duration in milliseconds. class RateLimit { RateLimit(this.category, this.duration); - final RateLimitCategory category; + final DataCategory category; final Duration duration; } diff --git a/dart/lib/src/transport/rate_limit_category.dart b/dart/lib/src/transport/rate_limit_category.dart deleted file mode 100644 index cb0040a0e6..0000000000 --- a/dart/lib/src/transport/rate_limit_category.dart +++ /dev/null @@ -1,54 +0,0 @@ -/// Different category types of data sent to Sentry. Used for rate limiting. -enum RateLimitCategory { - all, - rate_limit_default, // default - error, - session, - transaction, - attachment, - security, - unknown -} - -extension RateLimitCategoryExtension on RateLimitCategory { - static RateLimitCategory fromStringValue(String stringValue) { - switch (stringValue) { - case '__all__': - return RateLimitCategory.all; - case 'default': - return RateLimitCategory.rate_limit_default; - case 'error': - return RateLimitCategory.error; - case 'session': - return RateLimitCategory.session; - case 'transaction': - return RateLimitCategory.transaction; - case 'attachment': - return RateLimitCategory.attachment; - case 'security': - return RateLimitCategory.security; - } - return RateLimitCategory.unknown; - } - - String toStringValue() { - switch (this) { - case RateLimitCategory.all: - return '__all__'; - case RateLimitCategory.rate_limit_default: - return 'default'; - case RateLimitCategory.error: - return 'error'; - case RateLimitCategory.session: - return 'session'; - case RateLimitCategory.transaction: - return 'transaction'; - case RateLimitCategory.attachment: - return 'attachment'; - case RateLimitCategory.security: - return 'security'; - case RateLimitCategory.unknown: - return 'unknown'; - } - } -} diff --git a/dart/lib/src/transport/rate_limit_parser.dart b/dart/lib/src/transport/rate_limit_parser.dart index fe30121162..63f4f179d1 100644 --- a/dart/lib/src/transport/rate_limit_parser.dart +++ b/dart/lib/src/transport/rate_limit_parser.dart @@ -1,4 +1,4 @@ -import 'rate_limit_category.dart'; +import 'data_category.dart'; import 'rate_limit.dart'; /// Parse rate limit categories and times from response header payloads. @@ -29,23 +29,20 @@ class RateLimitParser { if (allCategories.isNotEmpty) { final categoryValues = allCategories.split(';'); for (final categoryValue in categoryValues) { - final category = - RateLimitCategoryExtension.fromStringValue(categoryValue); - if (category != RateLimitCategory.unknown) { + final category = DataCategoryExtension.fromStringValue(categoryValue); + if (category != DataCategory.unknown) { rateLimits.add(RateLimit(category, duration)); } } } else { - rateLimits.add(RateLimit(RateLimitCategory.all, duration)); + rateLimits.add(RateLimit(DataCategory.all, duration)); } } return rateLimits; } List parseRetryAfterHeader() { - return [ - RateLimit(RateLimitCategory.all, _parseRetryAfterOrDefault(_header)) - ]; + return [RateLimit(DataCategory.all, _parseRetryAfterOrDefault(_header))]; } // Helper diff --git a/dart/lib/src/transport/rate_limiter.dart b/dart/lib/src/transport/rate_limiter.dart index 9c0c432a28..2deb497706 100644 --- a/dart/lib/src/transport/rate_limiter.dart +++ b/dart/lib/src/transport/rate_limiter.dart @@ -4,14 +4,14 @@ import '../sentry_options.dart'; import '../sentry_envelope.dart'; import '../sentry_envelope_item.dart'; import 'rate_limit.dart'; -import 'rate_limit_category.dart'; +import 'data_category.dart'; /// Controls retry limits on different category types sent to Sentry. class RateLimiter { RateLimiter(this._clockProvider); final ClockProvider _clockProvider; - final _rateLimitedUntil = {}; + final _rateLimitedUntil = {}; /// Filter out envelopes that are rate limited. SentryEnvelope? filter(SentryEnvelope envelope) { @@ -75,7 +75,7 @@ class RateLimiter { _clockProvider().millisecondsSinceEpoch); // check all categories - final dateAllCategories = _rateLimitedUntil[RateLimitCategory.all]; + final dateAllCategories = _rateLimitedUntil[DataCategory.all]; if (dateAllCategories != null) { if (!currentDate.isAfter(dateAllCategories)) { return true; @@ -83,7 +83,7 @@ class RateLimiter { } // Unknown should not be rate limited - if (RateLimitCategory.unknown == dataCategory) { + if (DataCategory.unknown == dataCategory) { return false; } @@ -96,28 +96,27 @@ class RateLimiter { return false; } - RateLimitCategory _categoryFromItemType(String itemType) { + DataCategory _categoryFromItemType(String itemType) { switch (itemType) { case 'event': - return RateLimitCategory.error; + return DataCategory.error; case 'session': - return RateLimitCategory.session; + return DataCategory.session; case 'attachment': - return RateLimitCategory.attachment; + return DataCategory.attachment; case 'transaction': - return RateLimitCategory.transaction; + return DataCategory.transaction; default: - return RateLimitCategory.unknown; + return DataCategory.unknown; } } - void _applyRetryAfterOnlyIfLonger( - RateLimitCategory rateLimitCategory, DateTime date) { - final oldDate = _rateLimitedUntil[rateLimitCategory]; + void _applyRetryAfterOnlyIfLonger(DataCategory dataCategory, DateTime date) { + final oldDate = _rateLimitedUntil[dataCategory]; // only overwrite its previous date if the limit is even longer if (oldDate == null || date.isAfter(oldDate)) { - _rateLimitedUntil[rateLimitCategory] = date; + _rateLimitedUntil[dataCategory] = date; } } } diff --git a/dart/test/client_reports/client_report_recorder_test.dart b/dart/test/client_reports/client_report_recorder_test.dart index c8b0710f24..94a223bc5b 100644 --- a/dart/test/client_reports/client_report_recorder_test.dart +++ b/dart/test/client_reports/client_report_recorder_test.dart @@ -1,5 +1,5 @@ import 'package:sentry/src/client_reports/outcome.dart'; -import 'package:sentry/src/transport/rate_limit_category.dart'; +import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; import 'package:sentry/src/client_reports/client_report_recorder.dart'; @@ -23,7 +23,7 @@ void main() { test('flush returns client report with current date', () { final sut = fixture.getSut(); - sut.recordLostEvent(Outcome.ratelimitBackoff, RateLimitCategory.error); + sut.recordLostEvent(DiscardReason.ratelimitBackoff, DataCategory.error); final clientReport = sut.flush(); @@ -33,47 +33,47 @@ void main() { test('record lost event', () { final sut = fixture.getSut(); - sut.recordLostEvent(Outcome.ratelimitBackoff, RateLimitCategory.error); - sut.recordLostEvent(Outcome.ratelimitBackoff, RateLimitCategory.error); + sut.recordLostEvent(DiscardReason.ratelimitBackoff, DataCategory.error); + sut.recordLostEvent(DiscardReason.ratelimitBackoff, DataCategory.error); final clientReport = sut.flush(); final event = clientReport?.discardedEvents - .firstWhere((element) => element.category == RateLimitCategory.error); + .firstWhere((element) => element.category == DataCategory.error); - expect(event?.reason, Outcome.ratelimitBackoff); - expect(event?.category, RateLimitCategory.error); + expect(event?.reason, DiscardReason.ratelimitBackoff); + expect(event?.category, DataCategory.error); expect(event?.quantity, 2); }); test('record outcomes with different categories recorded separately', () { final sut = fixture.getSut(); - sut.recordLostEvent(Outcome.ratelimitBackoff, RateLimitCategory.error); + sut.recordLostEvent(DiscardReason.ratelimitBackoff, DataCategory.error); sut.recordLostEvent( - Outcome.ratelimitBackoff, RateLimitCategory.transaction); + DiscardReason.ratelimitBackoff, DataCategory.transaction); final clientReport = sut.flush(); final first = clientReport?.discardedEvents - .firstWhere((event) => event.category == RateLimitCategory.error); + .firstWhere((event) => event.category == DataCategory.error); - final second = clientReport?.discardedEvents.firstWhere( - (event) => event.category == RateLimitCategory.transaction); + final second = clientReport?.discardedEvents + .firstWhere((event) => event.category == DataCategory.transaction); - expect(first?.reason, Outcome.ratelimitBackoff); - expect(first?.category, RateLimitCategory.error); + expect(first?.reason, DiscardReason.ratelimitBackoff); + expect(first?.category, DataCategory.error); expect(first?.quantity, 1); - expect(second?.reason, Outcome.ratelimitBackoff); - expect(second?.category, RateLimitCategory.transaction); + expect(second?.reason, DiscardReason.ratelimitBackoff); + expect(second?.category, DataCategory.transaction); expect(second?.quantity, 1); }); test('calling flush multiple times returns null', () { final sut = fixture.getSut(); - sut.recordLostEvent(Outcome.ratelimitBackoff, RateLimitCategory.error); + sut.recordLostEvent(DiscardReason.ratelimitBackoff, DataCategory.error); sut.flush(); final clientReport = sut.flush(); diff --git a/dart/test/protocol/rate_limit_parser_test.dart b/dart/test/protocol/rate_limit_parser_test.dart index a8d5905c0e..c898915e04 100644 --- a/dart/test/protocol/rate_limit_parser_test.dart +++ b/dart/test/protocol/rate_limit_parser_test.dart @@ -1,5 +1,5 @@ import 'package:sentry/src/transport/rate_limit_parser.dart'; -import 'package:sentry/src/transport/rate_limit_category.dart'; +import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; void main() { @@ -8,7 +8,7 @@ void main() { final sut = RateLimitParser('50:transaction').parseRateLimitHeader(); expect(sut.length, 1); - expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].category, DataCategory.transaction); expect(sut[0].duration.inMilliseconds, 50000); }); @@ -17,9 +17,9 @@ void main() { RateLimitParser('50:transaction;session').parseRateLimitHeader(); expect(sut.length, 2); - expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].category, DataCategory.transaction); expect(sut[0].duration.inMilliseconds, 50000); - expect(sut[1].category, RateLimitCategory.session); + expect(sut[1].category, DataCategory.session); expect(sut[1].duration.inMilliseconds, 50000); }); @@ -33,7 +33,7 @@ void main() { final sut = RateLimitParser('50::key').parseRateLimitHeader(); expect(sut.length, 1); - expect(sut[0].category, RateLimitCategory.all); + expect(sut[0].category, DataCategory.all); expect(sut[0].duration.inMilliseconds, 50000); }); @@ -42,9 +42,9 @@ void main() { RateLimitParser('50:transaction, 70:session').parseRateLimitHeader(); expect(sut.length, 2); - expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].category, DataCategory.transaction); expect(sut[0].duration.inMilliseconds, 50000); - expect(sut[1].category, RateLimitCategory.session); + expect(sut[1].category, DataCategory.session); expect(sut[1].duration.inMilliseconds, 70000); }); @@ -53,9 +53,9 @@ void main() { .parseRateLimitHeader(); expect(sut.length, 2); - expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].category, DataCategory.transaction); expect(sut[0].duration.inMilliseconds, 50000); - expect(sut[1].category, RateLimitCategory.transaction); + expect(sut[1].category, DataCategory.transaction); expect(sut[1].duration.inMilliseconds, 70000); }); @@ -63,7 +63,7 @@ void main() { final sut = RateLimitParser('50:TRANSACTION').parseRateLimitHeader(); expect(sut.length, 1); - expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].category, DataCategory.transaction); expect(sut[0].duration.inMilliseconds, 50000); }); @@ -71,7 +71,7 @@ void main() { final sut = RateLimitParser('foobar:transaction').parseRateLimitHeader(); expect(sut.length, 1); - expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].category, DataCategory.transaction); expect(sut[0].duration.inMilliseconds, RateLimitParser.httpRetryAfterDefaultDelay.inMilliseconds); }); @@ -82,7 +82,7 @@ void main() { final sut = RateLimitParser(null).parseRetryAfterHeader(); expect(sut.length, 1); - expect(sut[0].category, RateLimitCategory.all); + expect(sut[0].category, DataCategory.all); expect(sut[0].duration.inMilliseconds, RateLimitParser.httpRetryAfterDefaultDelay.inMilliseconds); }); @@ -91,7 +91,7 @@ void main() { final sut = RateLimitParser('8').parseRetryAfterHeader(); expect(sut.length, 1); - expect(sut[0].category, RateLimitCategory.all); + expect(sut[0].category, DataCategory.all); expect(sut[0].duration.inMilliseconds, 8000); }); @@ -99,7 +99,7 @@ void main() { final sut = RateLimitParser('foobar').parseRetryAfterHeader(); expect(sut.length, 1); - expect(sut[0].category, RateLimitCategory.all); + expect(sut[0].category, DataCategory.all); expect(sut[0].duration.inMilliseconds, RateLimitParser.httpRetryAfterDefaultDelay.inMilliseconds); }); From 93c8cea82b04637418a2973f7749f67259f8f9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 4 Apr 2022 18:13:32 +0200 Subject: [PATCH 6/8] Replace rate limit naming from default case prefix --- dart/lib/src/transport/data_category.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dart/lib/src/transport/data_category.dart b/dart/lib/src/transport/data_category.dart index fa9e680445..aa43ef569b 100644 --- a/dart/lib/src/transport/data_category.dart +++ b/dart/lib/src/transport/data_category.dart @@ -1,7 +1,7 @@ /// Different category types of data sent to Sentry. Used for rate limiting and client reports. enum DataCategory { all, - rate_limit_default, // default + data_category_default, // default error, session, transaction, @@ -16,7 +16,7 @@ extension DataCategoryExtension on DataCategory { case '__all__': return DataCategory.all; case 'default': - return DataCategory.rate_limit_default; + return DataCategory.data_category_default; case 'error': return DataCategory.error; case 'session': @@ -35,7 +35,7 @@ extension DataCategoryExtension on DataCategory { switch (this) { case DataCategory.all: return '__all__'; - case DataCategory.rate_limit_default: + case DataCategory.data_category_default: return 'default'; case DataCategory.error: return 'error'; From 4576880fa07c2034f709c5cea2eb19a800e126bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 4 Apr 2022 18:47:35 +0200 Subject: [PATCH 7/8] rename file, introduce remaining enum cases --- .../client_report_recorder.dart | 2 +- .../src/client_reports/discard_reason.dart | 35 +++++++++++++++++++ .../src/client_reports/discarded_event.dart | 2 +- dart/lib/src/client_reports/outcome.dart | 15 -------- .../client_report_recorder_test.dart | 20 +++++------ 5 files changed, 47 insertions(+), 27 deletions(-) create mode 100644 dart/lib/src/client_reports/discard_reason.dart delete mode 100644 dart/lib/src/client_reports/outcome.dart diff --git a/dart/lib/src/client_reports/client_report_recorder.dart b/dart/lib/src/client_reports/client_report_recorder.dart index e5194e4f03..7122e17c2a 100644 --- a/dart/lib/src/client_reports/client_report_recorder.dart +++ b/dart/lib/src/client_reports/client_report_recorder.dart @@ -3,7 +3,7 @@ import 'package:meta/meta.dart'; import '../sentry_options.dart'; import 'client_report.dart'; import 'discarded_event.dart'; -import 'outcome.dart'; +import 'discard_reason.dart'; import '../transport/data_category.dart'; @internal diff --git a/dart/lib/src/client_reports/discard_reason.dart b/dart/lib/src/client_reports/discard_reason.dart new file mode 100644 index 0000000000..1b990f8dd2 --- /dev/null +++ b/dart/lib/src/client_reports/discard_reason.dart @@ -0,0 +1,35 @@ +import 'package:meta/meta.dart'; + +/// A reason that defines why events were lost, see +/// https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload. +@internal +enum DiscardReason { + beforeSend, + eventProcessor, + sampleRate, + networkError, + queueOverflow, + cacheOverflow, + rateLimitBackoff, +} + +extension OutcomeExtension on DiscardReason { + String toStringValue() { + switch (this) { + case DiscardReason.beforeSend: + return 'before_send'; + case DiscardReason.eventProcessor: + return 'event_processor'; + case DiscardReason.sampleRate: + return 'sample_rate'; + case DiscardReason.networkError: + return 'network_error'; + case DiscardReason.queueOverflow: + return 'queue_overflow'; + case DiscardReason.cacheOverflow: + return 'cache_overflow'; + case DiscardReason.rateLimitBackoff: + return 'ratelimit_backoff'; + } + } +} diff --git a/dart/lib/src/client_reports/discarded_event.dart b/dart/lib/src/client_reports/discarded_event.dart index 33a087939d..4aa3964373 100644 --- a/dart/lib/src/client_reports/discarded_event.dart +++ b/dart/lib/src/client_reports/discarded_event.dart @@ -1,6 +1,6 @@ import 'package:meta/meta.dart'; -import 'outcome.dart'; +import 'discard_reason.dart'; import '../transport/data_category.dart'; @internal diff --git a/dart/lib/src/client_reports/outcome.dart b/dart/lib/src/client_reports/outcome.dart deleted file mode 100644 index deefb3b20f..0000000000 --- a/dart/lib/src/client_reports/outcome.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:meta/meta.dart'; - -@internal -enum DiscardReason { ratelimitBackoff, networkError } - -extension OutcomeExtension on DiscardReason { - String toStringValue() { - switch (this) { - case DiscardReason.ratelimitBackoff: - return 'ratelimit_backoff'; - case DiscardReason.networkError: - return 'network_error'; - } - } -} diff --git a/dart/test/client_reports/client_report_recorder_test.dart b/dart/test/client_reports/client_report_recorder_test.dart index 94a223bc5b..3466d89f4d 100644 --- a/dart/test/client_reports/client_report_recorder_test.dart +++ b/dart/test/client_reports/client_report_recorder_test.dart @@ -1,4 +1,4 @@ -import 'package:sentry/src/client_reports/outcome.dart'; +import 'package:sentry/src/client_reports/discard_reason.dart'; import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; @@ -23,7 +23,7 @@ void main() { test('flush returns client report with current date', () { final sut = fixture.getSut(); - sut.recordLostEvent(DiscardReason.ratelimitBackoff, DataCategory.error); + sut.recordLostEvent(DiscardReason.rateLimitBackoff, DataCategory.error); final clientReport = sut.flush(); @@ -33,15 +33,15 @@ void main() { test('record lost event', () { final sut = fixture.getSut(); - sut.recordLostEvent(DiscardReason.ratelimitBackoff, DataCategory.error); - sut.recordLostEvent(DiscardReason.ratelimitBackoff, DataCategory.error); + sut.recordLostEvent(DiscardReason.rateLimitBackoff, DataCategory.error); + sut.recordLostEvent(DiscardReason.rateLimitBackoff, DataCategory.error); final clientReport = sut.flush(); final event = clientReport?.discardedEvents .firstWhere((element) => element.category == DataCategory.error); - expect(event?.reason, DiscardReason.ratelimitBackoff); + expect(event?.reason, DiscardReason.rateLimitBackoff); expect(event?.category, DataCategory.error); expect(event?.quantity, 2); }); @@ -49,9 +49,9 @@ void main() { test('record outcomes with different categories recorded separately', () { final sut = fixture.getSut(); - sut.recordLostEvent(DiscardReason.ratelimitBackoff, DataCategory.error); + sut.recordLostEvent(DiscardReason.rateLimitBackoff, DataCategory.error); sut.recordLostEvent( - DiscardReason.ratelimitBackoff, DataCategory.transaction); + DiscardReason.rateLimitBackoff, DataCategory.transaction); final clientReport = sut.flush(); @@ -61,11 +61,11 @@ void main() { final second = clientReport?.discardedEvents .firstWhere((event) => event.category == DataCategory.transaction); - expect(first?.reason, DiscardReason.ratelimitBackoff); + expect(first?.reason, DiscardReason.rateLimitBackoff); expect(first?.category, DataCategory.error); expect(first?.quantity, 1); - expect(second?.reason, DiscardReason.ratelimitBackoff); + expect(second?.reason, DiscardReason.rateLimitBackoff); expect(second?.category, DataCategory.transaction); expect(second?.quantity, 1); }); @@ -73,7 +73,7 @@ void main() { test('calling flush multiple times returns null', () { final sut = fixture.getSut(); - sut.recordLostEvent(DiscardReason.ratelimitBackoff, DataCategory.error); + sut.recordLostEvent(DiscardReason.rateLimitBackoff, DataCategory.error); sut.flush(); final clientReport = sut.flush(); From 9c40c3bf72117622738d898e61fff08c4a2d5f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Wed, 13 Apr 2022 09:28:35 +0200 Subject: [PATCH 8/8] Feat: Client Report Envelope Item (#810) --- dart/lib/sentry.dart | 1 + .../lib/src/client_reports/client_report.dart | 19 ++ .../src/client_reports/discarded_event.dart | 8 + .../noop_client_report_recorder.dart | 19 ++ dart/lib/src/hub.dart | 17 +- dart/lib/src/sentry_client.dart | 34 +++- dart/lib/src/sentry_envelope.dart | 13 +- dart/lib/src/sentry_envelope_item.dart | 69 ++++---- dart/lib/src/sentry_item_type.dart | 1 + dart/lib/src/sentry_options.dart | 16 +- dart/lib/src/transport/rate_limiter.dart | 11 +- .../client_reports/client_report_test.dart | 54 ++++++ dart/test/hub_test.dart | 25 +++ .../mocks/mock_client_report_recorder.dart | 25 +++ dart/test/mocks/mock_envelope.dart | 26 +++ dart/test/mocks/mock_transport.dart | 4 +- dart/test/protocol/rate_limiter_test.dart | 63 ++++++- dart/test/sentry_client_test.dart | 163 +++++++++++++++++- dart/test/sentry_envelope_item_test.dart | 30 +++- dart/test/sentry_envelope_test.dart | 1 - dart/test/sentry_envelope_vm_test.dart | 1 - dart/test/transport/http_transport_test.dart | 17 +- dio/test/mocks/mock_transport.dart | 1 - flutter/test/file_system_transport_test.dart | 3 +- flutter/test/mocks.mocks.dart | 13 +- 25 files changed, 554 insertions(+), 80 deletions(-) create mode 100644 dart/lib/src/client_reports/noop_client_report_recorder.dart create mode 100644 dart/test/client_reports/client_report_test.dart create mode 100644 dart/test/mocks/mock_client_report_recorder.dart create mode 100644 dart/test/mocks/mock_envelope.dart diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 6a5435d009..77ac7a482f 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -14,6 +14,7 @@ export 'src/protocol.dart'; export 'src/scope.dart'; export 'src/sentry.dart'; export 'src/sentry_envelope.dart'; +export 'src/sentry_envelope_item.dart'; export 'src/sentry_client.dart'; export 'src/sentry_options.dart'; // useful for integrations diff --git a/dart/lib/src/client_reports/client_report.dart b/dart/lib/src/client_reports/client_report.dart index fdf9e6ff9e..38a1a34c67 100644 --- a/dart/lib/src/client_reports/client_report.dart +++ b/dart/lib/src/client_reports/client_report.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import 'discarded_event.dart'; +import '../utils.dart'; @internal class ClientReport { @@ -8,4 +9,22 @@ class ClientReport { final DateTime? timestamp; final List discardedEvents; + + Map toJson() { + final json = {}; + + if (timestamp != null) { + json['timestamp'] = formatDateAsIso8601WithMillisPrecision(timestamp!); + } + + final eventsJson = discardedEvents + .map((e) => e.toJson()) + .where((e) => e.isNotEmpty) + .toList(growable: false); + if (eventsJson.isNotEmpty) { + json['discarded_events'] = eventsJson; + } + + return json; + } } diff --git a/dart/lib/src/client_reports/discarded_event.dart b/dart/lib/src/client_reports/discarded_event.dart index 4aa3964373..0b989aa4cf 100644 --- a/dart/lib/src/client_reports/discarded_event.dart +++ b/dart/lib/src/client_reports/discarded_event.dart @@ -10,4 +10,12 @@ class DiscardedEvent { final DiscardReason reason; final DataCategory category; final int quantity; + + Map toJson() { + return { + 'reason': reason.toStringValue(), + 'category': category.toStringValue(), + 'quantity': quantity, + }; + } } diff --git a/dart/lib/src/client_reports/noop_client_report_recorder.dart b/dart/lib/src/client_reports/noop_client_report_recorder.dart new file mode 100644 index 0000000000..acd6472347 --- /dev/null +++ b/dart/lib/src/client_reports/noop_client_report_recorder.dart @@ -0,0 +1,19 @@ +import 'package:meta/meta.dart'; + +import '../transport/data_category.dart'; +import 'client_report.dart'; +import 'client_report_recorder.dart'; +import 'discard_reason.dart'; + +@internal +class NoOpClientReportRecorder implements ClientReportRecorder { + const NoOpClientReportRecorder(); + + @override + ClientReport? flush() { + return null; + } + + @override + void recordLostEvent(DiscardReason reason, DataCategory category) {} +} diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 63354581ed..92e295daf8 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -2,15 +2,12 @@ import 'dart:async'; import 'dart:collection'; import 'package:meta/meta.dart'; +import 'transport/data_category.dart'; -import 'protocol.dart'; -import 'scope.dart'; -import 'sentry_client.dart'; -import 'sentry_options.dart'; +import '../sentry.dart'; +import 'client_reports/discard_reason.dart'; import 'sentry_tracer.dart'; import 'sentry_traces_sampler.dart'; -import 'sentry_user_feedback.dart'; -import 'tracing.dart'; /// Configures the scope through the callback. typedef ScopeCallback = void Function(Scope); @@ -462,14 +459,18 @@ class Hub { 'Capturing unfinished transaction: ${transaction.eventId}', ); } else { + final item = _peek(); + if (!transaction.sampled) { + _options.recorder.recordLostEvent( + DiscardReason.sampleRate, + DataCategory.transaction, + ); _options.logger( SentryLevel.warning, 'Transaction ${transaction.eventId} was dropped due to sampling decision.', ); } else { - final item = _peek(); - try { sentryId = await item.client.captureTransaction( transaction, diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 020be3046e..7c096e20e4 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -14,6 +14,9 @@ import 'transport/http_transport.dart'; import 'transport/noop_transport.dart'; import 'version.dart'; import 'sentry_envelope.dart'; +import 'client_reports/client_report_recorder.dart'; +import 'client_reports/discard_reason.dart'; +import 'transport/data_category.dart'; /// Default value for [User.ipAddress]. It gets set when an event does not have /// a user and IP address. Only applies if [SentryOptions.sendDefaultPii] is set @@ -34,10 +37,13 @@ class SentryClient { /// Instantiates a client using [SentryOptions] factory SentryClient(SentryOptions options) { + if (options.sendClientReports) { + options.recorder = ClientReportRecorder(options.clock); + } if (options.transport is NoOpTransport) { - options.transport = HttpTransport(options, RateLimiter(options.clock)); + final rateLimiter = RateLimiter(options.clock, options.recorder); + options.transport = HttpTransport(options, rateLimiter); } - return SentryClient._(options); } @@ -53,6 +59,7 @@ class SentryClient { dynamic hint, }) async { if (_sampleRate()) { + _recordLostEvent(event, DiscardReason.sampleRate); _options.logger( SentryLevel.debug, 'Event ${event.eventId.toString()} was dropped due to sampling decision.', @@ -87,6 +94,7 @@ class SentryClient { final beforeSend = _options.beforeSend; if (beforeSend != null) { + final beforeSendEvent = preparedEvent; try { preparedEvent = await beforeSend(preparedEvent, hint: hint); } catch (exception, stackTrace) { @@ -98,6 +106,7 @@ class SentryClient { ); } if (preparedEvent == null) { + _recordLostEvent(beforeSendEvent, DiscardReason.beforeSend); _options.logger( SentryLevel.debug, 'Event was dropped by BeforeSend callback', @@ -264,7 +273,7 @@ class SentryClient { /// Reports the [envelope] to Sentry.io. Future captureEnvelope(SentryEnvelope envelope) { - return _options.transport.send(envelope); + return _attachClientReportsAndSend(envelope); } /// Reports the [userFeedback] to Sentry.io. @@ -273,7 +282,7 @@ class SentryClient { userFeedback, _options.sdk, ); - return _options.transport.send(envelope); + return _attachClientReportsAndSend(envelope); } void close() => _options.httpClient.close(); @@ -296,6 +305,7 @@ class SentryClient { ); } if (processedEvent == null) { + _recordLostEvent(event, DiscardReason.eventProcessor); _options.logger(SentryLevel.debug, 'Event was dropped by a processor'); break; } @@ -309,4 +319,20 @@ class SentryClient { } return false; } + + void _recordLostEvent(SentryEvent event, DiscardReason reason) { + DataCategory category; + if (event is SentryTransaction) { + category = DataCategory.transaction; + } else { + category = DataCategory.error; + } + _options.recorder.recordLostEvent(reason, category); + } + + Future _attachClientReportsAndSend(SentryEnvelope envelope) { + final clientReport = _options.recorder.flush(); + envelope.addClientReport(clientReport); + return _options.transport.send(envelope); + } } diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart index 25bd031974..76182a8ab9 100644 --- a/dart/lib/src/sentry_envelope.dart +++ b/dart/lib/src/sentry_envelope.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'client_reports/client_report.dart'; import 'protocol.dart'; import 'sentry_item_type.dart'; import 'sentry_options.dart'; @@ -12,7 +13,7 @@ import 'sentry_user_feedback.dart'; class SentryEnvelope { SentryEnvelope(this.header, this.items); - /// Header descriping envelope content. + /// Header describing envelope content. final SentryEnvelopeHeader header; /// All items contained in the envelope. @@ -74,7 +75,7 @@ class SentryEnvelope { if (length < 0) { continue; } - // Olny attachments should be filtered according to + // Only attachments should be filtered according to // SentryOptions.maxAttachmentSize if (item.header.type == SentryItemType.attachment) { if (await item.header.length() > options.maxAttachmentSize) { @@ -88,4 +89,12 @@ class SentryEnvelope { } } } + + /// Add an envelope item containing client report data. + void addClientReport(ClientReport? clientReport) { + if (clientReport != null) { + final envelopeItem = SentryEnvelopeItem.fromClientReport(clientReport); + items.add(envelopeItem); + } + } } diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index b274334127..b9d96afecd 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'client_reports/client_report.dart'; import 'protocol.dart'; import 'utils.dart'; import 'sentry_attachment/sentry_attachment.dart'; @@ -12,21 +13,12 @@ class SentryEnvelopeItem { /// Creates an [SentryEnvelopeItem] which sends [SentryTransaction]. factory SentryEnvelopeItem.fromTransaction(SentryTransaction transaction) { - final cachedItem = _CachedItem(() async { - final jsonEncoded = jsonEncode( - transaction.toJson(), - toEncodable: jsonSerializationFallback, - ); - return utf8.encode(jsonEncoded); - }); - - final getLength = () async { - return (await cachedItem.getData()).length; - }; + final cachedItem = + _CachedItem(() async => _jsonToBytes(transaction.toJson())); final header = SentryEnvelopeItemHeader( SentryItemType.transaction, - getLength, + cachedItem.getDataLength, contentType: 'application/json', ); return SentryEnvelopeItem(header, cachedItem.getData); @@ -35,17 +27,9 @@ class SentryEnvelopeItem { factory SentryEnvelopeItem.fromAttachment(SentryAttachment attachment) { final cachedItem = _CachedItem(() async => await attachment.bytes); - final getLength = () async { - try { - return (await cachedItem.getData()).length; - } catch (_) { - return -1; - } - }; - final header = SentryEnvelopeItemHeader( SentryItemType.attachment, - getLength, + cachedItem.getDataLength, contentType: attachment.contentType, fileName: attachment.filename, attachmentType: attachment.attachmentType, @@ -55,27 +39,42 @@ class SentryEnvelopeItem { /// Create an [SentryEnvelopeItem] which sends [SentryUserFeedback]. factory SentryEnvelopeItem.fromUserFeedback(SentryUserFeedback feedback) { - final bytes = _jsonToBytes(feedback.toJson()); + final cachedItem = _CachedItem(() async => _jsonToBytes(feedback.toJson())); final header = SentryEnvelopeItemHeader( SentryItemType.userFeedback, - () async => bytes.length, + cachedItem.getDataLength, contentType: 'application/json', ); - return SentryEnvelopeItem(header, () async => bytes); + return SentryEnvelopeItem(header, cachedItem.getData); } /// Create an [SentryEnvelopeItem] which holds the [SentryEvent] data. factory SentryEnvelopeItem.fromEvent(SentryEvent event) { - final bytes = _jsonToBytes(event.toJson()); + final cachedItem = _CachedItem(() async => _jsonToBytes(event.toJson())); return SentryEnvelopeItem( SentryEnvelopeItemHeader( SentryItemType.event, - () async => bytes.length, + cachedItem.getDataLength, + contentType: 'application/json', + ), + cachedItem.getData, + ); + } + + /// Create an [SentryEnvelopeItem] which holds the [ClientReport] data. + factory SentryEnvelopeItem.fromClientReport(ClientReport clientReport) { + final cachedItem = + _CachedItem(() async => _jsonToBytes(clientReport.toJson())); + + return SentryEnvelopeItem( + SentryEnvelopeItemHeader( + SentryItemType.clientReport, + cachedItem.getDataLength, contentType: 'application/json', ), - () async => bytes, + cachedItem.getData, ); } @@ -88,13 +87,11 @@ class SentryEnvelopeItem { /// Stream binary data of `Envelope` item. Future> envelopeItemStream() async { // Each item needs to be encoded as one unit. - // Otherwise the header alredy got yielded if the content throws + // Otherwise the header already got yielded if the content throws // an exception. try { - final itemHeader = utf8.encode(jsonEncode( - await header.toJson(), - toEncodable: jsonSerializationFallback, - )); + final itemHeader = _jsonToBytes(await header.toJson()); + final newLine = utf8.encode('\n'); final data = await dataFactory(); return [...itemHeader, ...newLine, ...data]; @@ -123,4 +120,12 @@ class _CachedItem { _data ??= await _dataFactory(); return _data!; } + + Future getDataLength() async { + try { + return (await getData()).length; + } catch (_) { + return -1; + } + } } diff --git a/dart/lib/src/sentry_item_type.dart b/dart/lib/src/sentry_item_type.dart index b90dff8538..c74b6b6049 100644 --- a/dart/lib/src/sentry_item_type.dart +++ b/dart/lib/src/sentry_item_type.dart @@ -3,5 +3,6 @@ class SentryItemType { static const String userFeedback = 'user_report'; static const String attachment = 'attachment'; static const String transaction = 'transaction'; + static const String clientReport = 'client_report'; static const String unknown = '__unknown__'; } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 3afa664ea8..47c88ba4e8 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -4,19 +4,15 @@ import 'dart:developer'; import 'package:meta/meta.dart'; import 'package:http/http.dart'; +import '../sentry.dart'; +import 'client_reports/client_report_recorder.dart'; +import 'client_reports/noop_client_report_recorder.dart'; import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.dart'; import 'diagnostic_logger.dart'; import 'environment/environment_variables.dart'; -import 'event_processor.dart'; -import 'http_client/sentry_http_client.dart'; -import 'integration.dart'; import 'noop_client.dart'; -import 'platform_checker.dart'; -import 'protocol.dart'; -import 'tracing.dart'; import 'transport/noop_transport.dart'; -import 'transport/transport.dart'; import 'utils.dart'; import 'version.dart'; @@ -262,6 +258,12 @@ class SentryOptions { /// to be sent to Sentry. TracesSamplerCallback? tracesSampler; + /// Send statistics to sentry when the client drops events. + bool sendClientReports = true; + + @internal + late ClientReportRecorder recorder = NoOpClientReportRecorder(); + SentryOptions({this.dsn, PlatformChecker? checker}) { if (checker != null) { platformChecker = checker; diff --git a/dart/lib/src/transport/rate_limiter.dart b/dart/lib/src/transport/rate_limiter.dart index 2deb497706..ed5c247b19 100644 --- a/dart/lib/src/transport/rate_limiter.dart +++ b/dart/lib/src/transport/rate_limiter.dart @@ -1,16 +1,18 @@ import '../transport/rate_limit_parser.dart'; - import '../sentry_options.dart'; import '../sentry_envelope.dart'; import '../sentry_envelope_item.dart'; import 'rate_limit.dart'; import 'data_category.dart'; +import '../client_reports/client_report_recorder.dart'; +import '../client_reports/discard_reason.dart'; /// Controls retry limits on different category types sent to Sentry. class RateLimiter { - RateLimiter(this._clockProvider); + RateLimiter(this._clockProvider, this._clientReportRecorder); final ClockProvider _clockProvider; + final ClientReportRecorder _clientReportRecorder; final _rateLimitedUntil = {}; /// Filter out envelopes that are rate limited. @@ -22,6 +24,11 @@ class RateLimiter { if (_isRetryAfter(item.header.type)) { dropItems ??= []; dropItems.add(item); + + _clientReportRecorder.recordLostEvent( + DiscardReason.rateLimitBackoff, + _categoryFromItemType(item.header.type), + ); } } diff --git a/dart/test/client_reports/client_report_test.dart b/dart/test/client_reports/client_report_test.dart new file mode 100644 index 0000000000..00f5736cc2 --- /dev/null +++ b/dart/test/client_reports/client_report_test.dart @@ -0,0 +1,54 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/src/client_reports/client_report.dart'; +import 'package:sentry/src/client_reports/discard_reason.dart'; +import 'package:sentry/src/client_reports/discarded_event.dart'; +import 'package:sentry/src/transport/data_category.dart'; +import 'package:test/test.dart'; +import 'package:sentry/src/utils.dart'; + +void main() { + group('json', () { + late Fixture fixture; + + setUp(() async { + fixture = Fixture(); + }); + + test('toJson', () { + final sut = fixture.getSut(); + final json = sut.toJson(); + + expect( + DeepCollectionEquality().equals(fixture.clientReportJson, json), + true, + ); + }); + }); +} + +class Fixture { + final timestamp = DateTime.fromMillisecondsSinceEpoch(0); + late Map clientReportJson; + + Fixture() { + clientReportJson = { + 'timestamp': formatDateAsIso8601WithMillisPrecision(timestamp), + 'discarded_events': [ + { + 'reason': 'ratelimit_backoff', + 'category': 'error', + 'quantity': 2, + } + ], + }; + } + + ClientReport getSut() { + return ClientReport( + timestamp, + [ + DiscardedEvent(DiscardReason.rateLimitBackoff, DataCategory.error, 2), + ], + ); + } +} diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index 8a4cbeef51..3957a5a66f 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -1,9 +1,12 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; +import 'package:sentry/src/client_reports/discard_reason.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; import 'mocks.dart'; +import 'mocks/mock_client_report_recorder.dart'; import 'mocks/mock_sentry_client.dart'; void main() { @@ -465,10 +468,30 @@ void main() { expect(calls[2].formatted, 'foo bar 2'); }); }); + + group('ClientReportRecorder', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('record sample rate dropping transaction', () async { + final hub = fixture.getSut(sampled: false); + var transaction = SentryTransaction(fixture.tracer); + + await hub.captureTransaction(transaction); + + expect(fixture.recorder.reason, DiscardReason.sampleRate); + expect(fixture.recorder.category, DataCategory.transaction); + }); + }); } class Fixture { final client = MockSentryClient(); + final recorder = MockClientReportRecorder(); + final options = SentryOptions(dsn: fakeDsn); late SentryTransactionContext _context; late SentryTracer tracer; @@ -491,6 +514,8 @@ class Fixture { tracer = SentryTracer(_context, hub); hub.bindClient(client); + options.recorder = recorder; + return hub; } } diff --git a/dart/test/mocks/mock_client_report_recorder.dart b/dart/test/mocks/mock_client_report_recorder.dart new file mode 100644 index 0000000000..fa1af54ed8 --- /dev/null +++ b/dart/test/mocks/mock_client_report_recorder.dart @@ -0,0 +1,25 @@ +import 'package:sentry/src/client_reports/client_report_recorder.dart'; +import 'package:sentry/src/client_reports/discard_reason.dart'; +import 'package:sentry/src/client_reports/client_report.dart'; +import 'package:sentry/src/transport/data_category.dart'; + +class MockClientReportRecorder implements ClientReportRecorder { + DiscardReason? reason; + DataCategory? category; + + ClientReport? clientReport; + + bool flushCalled = false; + + @override + ClientReport? flush() { + flushCalled = true; + return clientReport; + } + + @override + void recordLostEvent(DiscardReason reason, DataCategory category) { + this.reason = reason; + this.category = category; + } +} diff --git a/dart/test/mocks/mock_envelope.dart b/dart/test/mocks/mock_envelope.dart new file mode 100644 index 0000000000..9b43a41b8a --- /dev/null +++ b/dart/test/mocks/mock_envelope.dart @@ -0,0 +1,26 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_envelope_header.dart'; +import 'package:sentry/src/client_reports/client_report.dart'; + +class MockEnvelope implements SentryEnvelope { + ClientReport? clientReport; + + @override + void addClientReport(ClientReport? clientReport) { + this.clientReport = clientReport; + } + + @override + Stream> envelopeStream(SentryOptions options) async* { + yield [0]; + } + + @override + SentryEnvelopeHeader get header => SentryEnvelopeHeader( + SentryId.empty(), + SdkVersion(name: 'fixture-name', version: '1'), + ); + + @override + List items = []; +} diff --git a/dart/test/mocks/mock_transport.dart b/dart/test/mocks/mock_transport.dart index 5b03835b77..693debe1e9 100644 --- a/dart/test/mocks/mock_transport.dart +++ b/dart/test/mocks/mock_transport.dart @@ -2,9 +2,7 @@ import 'dart:convert'; import 'package:sentry/sentry.dart'; -import 'no_such_method_provider.dart'; - -class MockTransport with NoSuchMethodProvider implements Transport { +class MockTransport implements Transport { List envelopes = []; List events = []; int calls = 0; diff --git a/dart/test/protocol/rate_limiter_test.dart b/dart/test/protocol/rate_limiter_test.dart index 23f6cf0cde..5845422287 100644 --- a/dart/test/protocol/rate_limiter_test.dart +++ b/dart/test/protocol/rate_limiter_test.dart @@ -1,10 +1,14 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/client_reports/discard_reason.dart'; +import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; import 'package:sentry/src/transport/rate_limiter.dart'; -import 'package:sentry/src/protocol/sentry_event.dart'; -import 'package:sentry/src/sentry_envelope.dart'; +import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry/src/sentry_envelope_header.dart'; -import 'package:sentry/src/sentry_envelope_item.dart'; + +import '../mocks/mock_client_report_recorder.dart'; +import '../mocks/mock_hub.dart'; void main() { var fixture = Fixture(); @@ -185,16 +189,67 @@ void main() { final result = rateLimiter.filter(envelope); expect(result, isNull); }); + + test('dropping of event recorded', () { + final rateLimiter = fixture.getSUT(); + + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + final eventEnvelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + rateLimiter.updateRetryAfterLimits( + '1:error:key, 5:error:organization', null, 1); + + final result = rateLimiter.filter(eventEnvelope); + expect(result, isNull); + + expect(fixture.mockRecorder.category, DataCategory.error); + expect(fixture.mockRecorder.reason, DiscardReason.rateLimitBackoff); + }); + + test('dropping of transaction recorded', () { + final rateLimiter = fixture.getSUT(); + + final transaction = fixture.getTransaction(); + final eventItem = SentryEnvelopeItem.fromTransaction(transaction); + final eventEnvelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + rateLimiter.updateRetryAfterLimits( + '1:transaction:key, 5:transaction:organization', null, 1); + + final result = rateLimiter.filter(eventEnvelope); + expect(result, isNull); + + expect(fixture.mockRecorder.category, DataCategory.transaction); + expect(fixture.mockRecorder.reason, DiscardReason.rateLimitBackoff); + }); } class Fixture { var dateTimeToReturn = 0; + late var mockRecorder = MockClientReportRecorder(); + RateLimiter getSUT() { - return RateLimiter(_currentDateTime); + return RateLimiter(_currentDateTime, mockRecorder); } DateTime _currentDateTime() { return DateTime.fromMillisecondsSinceEpoch(dateTimeToReturn); } + + SentryTransaction getTransaction() { + final context = SentryTransactionContext( + 'name', + 'op', + sampled: true, + ); + final tracer = SentryTracer(context, MockHub()); + return SentryTransaction(tracer); + } } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index a6acb41ceb..2aab69c9d3 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -3,12 +3,20 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:sentry/sentry.dart'; +import 'package:sentry/src/client_reports/client_report.dart'; +import 'package:sentry/src/client_reports/discard_reason.dart'; +import 'package:sentry/src/client_reports/discarded_event.dart'; +import 'package:sentry/src/client_reports/noop_client_report_recorder.dart'; import 'package:sentry/src/sentry_item_type.dart'; import 'package:sentry/src/sentry_stack_trace_factory.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry/src/transport/data_category.dart'; import 'package:test/test.dart'; import 'mocks.dart'; +import 'mocks/mock_client_report_recorder.dart'; +import 'mocks/mock_envelope.dart'; +import 'mocks/mock_hub.dart'; import 'mocks/mock_transport.dart'; void main() { @@ -805,6 +813,149 @@ void main() { expect(capturedEnvelope, fakeEnvelope); }); }); + + group('ClientReportRecorder', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('recorder is not noop if client reports are enabled', () async { + fixture.options.sendClientReports = true; + + fixture.getSut( + eventProcessor: DropAllEventProcessor(), + provideMockRecorder: false, + ); + + expect(fixture.options.recorder is NoOpClientReportRecorder, false); + expect(fixture.options.recorder is MockClientReportRecorder, false); + }); + + test('recorder is noop if client reports are disabled', () { + fixture.options.sendClientReports = false; + + fixture.getSut( + eventProcessor: DropAllEventProcessor(), + provideMockRecorder: false, + ); + + expect(fixture.options.recorder is NoOpClientReportRecorder, true); + }); + + test('captureEnvelope calls flush', () async { + final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); + + final envelope = MockEnvelope(); + envelope.items = [SentryEnvelopeItem.fromEvent(SentryEvent())]; + + await client.captureEnvelope(envelope); + + expect(fixture.recorder.flushCalled, true); + }); + + test('captureEnvelope adds client report', () async { + final clientReport = ClientReport( + DateTime(0), + [DiscardedEvent(DiscardReason.rateLimitBackoff, DataCategory.error, 1)], + ); + fixture.recorder.clientReport = clientReport; + + final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); + + final envelope = MockEnvelope(); + envelope.items = [SentryEnvelopeItem.fromEvent(SentryEvent())]; + + await client.captureEnvelope(envelope); + + expect(envelope.clientReport, clientReport); + }); + + test('captureUserFeedback calls flush', () async { + final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); + + final id = SentryId.newId(); + final feedback = SentryUserFeedback( + eventId: id, + comments: 'this is awesome', + email: 'sentry@example.com', + name: 'Rockstar Developer', + ); + await client.captureUserFeedback(feedback); + + expect(fixture.recorder.flushCalled, true); + }); + + test('captureUserFeedback adds client report', () async { + final clientReport = ClientReport( + DateTime(0), + [DiscardedEvent(DiscardReason.rateLimitBackoff, DataCategory.error, 1)], + ); + fixture.recorder.clientReport = clientReport; + + final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); + + final id = SentryId.newId(); + final feedback = SentryUserFeedback( + eventId: id, + comments: 'this is awesome', + email: 'sentry@example.com', + name: 'Rockstar Developer', + ); + await client.captureUserFeedback(feedback); + + final envelope = fixture.transport.envelopes.first; + final item = envelope.items.last; + + // Only partial test, as the envelope is created internally from feedback. + expect(item.header.type, SentryItemType.clientReport); + }); + + test('record event processor dropping event', () async { + final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); + + await client.captureEvent(fakeEvent); + + expect(fixture.recorder.reason, DiscardReason.eventProcessor); + expect(fixture.recorder.category, DataCategory.error); + }); + + test('record event processor dropping transaction', () async { + final client = fixture.getSut(eventProcessor: DropAllEventProcessor()); + + final context = SentryTransactionContext('name', 'op'); + final tracer = SentryTracer(context, MockHub()); + final transaction = SentryTransaction(tracer); + + await client.captureTransaction(transaction); + + expect(fixture.recorder.reason, DiscardReason.eventProcessor); + expect(fixture.recorder.category, DataCategory.transaction); + }); + + test('record beforeSend dropping event', () async { + final client = fixture.getSut(); + + fixture.options.beforeSend = fixture.droppingBeforeSend; + + await client.captureEvent(fakeEvent); + + expect(fixture.recorder.reason, DiscardReason.beforeSend); + expect(fixture.recorder.category, DataCategory.error); + }); + + test('record sample rate dropping event', () async { + final client = fixture.getSut(sampleRate: 0.0); + + fixture.options.beforeSend = fixture.droppingBeforeSend; + + await client.captureEvent(fakeEvent); + + expect(fixture.recorder.reason, DiscardReason.sampleRate); + expect(fixture.recorder.category, DataCategory.error); + }); + }); } Future eventFromEnvelope(SentryEnvelope envelope) async { @@ -852,9 +1003,11 @@ FutureOr beforeSendCallback(SentryEvent event, {dynamic hint}) { } class Fixture { + final recorder = MockClientReportRecorder(); final transport = MockTransport(); final options = SentryOptions(dsn: fakeDsn); + late SentryTransactionContext _context; late SentryTracer tracer; @@ -864,6 +1017,7 @@ class Fixture { double? sampleRate, BeforeSendCallback? beforeSend, EventProcessor? eventProcessor, + bool provideMockRecorder = true, }) { final hub = Hub(options); _context = SentryTransactionContext( @@ -883,7 +1037,14 @@ class Fixture { options.transport = transport; final client = SentryClient(options); // hub.bindClient(client); - + if (provideMockRecorder) { + options.recorder = recorder; + } return client; } + + FutureOr droppingBeforeSend(SentryEvent event, + {dynamic hint}) async { + return null; + } } diff --git a/dart/test/sentry_envelope_item_test.dart b/dart/test/sentry_envelope_item_test.dart index 35c553c468..4fe01e5a8e 100644 --- a/dart/test/sentry_envelope_item_test.dart +++ b/dart/test/sentry_envelope_item_test.dart @@ -1,10 +1,13 @@ import 'dart:convert'; import 'package:sentry/sentry.dart'; +import 'package:sentry/src/client_reports/client_report.dart'; +import 'package:sentry/src/client_reports/discard_reason.dart'; +import 'package:sentry/src/client_reports/discarded_event.dart'; import 'package:sentry/src/sentry_envelope_item_header.dart'; -import 'package:sentry/src/sentry_envelope_item.dart'; import 'package:sentry/src/sentry_item_type.dart'; import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry/src/transport/data_category.dart'; import 'package:sentry/src/utils.dart'; import 'package:test/test.dart'; @@ -82,5 +85,30 @@ void main() { expect(actualLength, expectedLength); expect(actualData, expectedData); }); + + test('fromClientReport', () async { + final timestamp = DateTime(0); + final discardedEvents = [ + DiscardedEvent(DiscardReason.rateLimitBackoff, DataCategory.error, 1) + ]; + + final cr = ClientReport(timestamp, discardedEvents); + + final sut = SentryEnvelopeItem.fromClientReport(cr); + + final expectedData = utf8.encode(jsonEncode( + cr.toJson(), + toEncodable: jsonSerializationFallback, + )); + final actualData = await sut.dataFactory(); + + final expectedLength = expectedData.length; + final actualLength = await sut.header.length(); + + expect(sut.header.contentType, 'application/json'); + expect(sut.header.type, SentryItemType.clientReport); + expect(actualLength, expectedLength); + expect(actualData, expectedData); + }); }); } diff --git a/dart/test/sentry_envelope_test.dart b/dart/test/sentry_envelope_test.dart index bc28c508de..d7eaf62045 100644 --- a/dart/test/sentry_envelope_test.dart +++ b/dart/test/sentry_envelope_test.dart @@ -4,7 +4,6 @@ import 'dart:typed_data'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/sentry_envelope_header.dart'; import 'package:sentry/src/sentry_envelope_item_header.dart'; -import 'package:sentry/src/sentry_envelope_item.dart'; import 'package:sentry/src/sentry_item_type.dart'; import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry/src/utils.dart'; diff --git a/dart/test/sentry_envelope_vm_test.dart b/dart/test/sentry_envelope_vm_test.dart index b1bd880f91..33dce0515d 100644 --- a/dart/test/sentry_envelope_vm_test.dart +++ b/dart/test/sentry_envelope_vm_test.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:sentry/sentry_io.dart'; import 'package:sentry/src/sentry_envelope_header.dart'; import 'package:sentry/src/sentry_envelope_item_header.dart'; -import 'package:sentry/src/sentry_envelope_item.dart'; import 'package:test/test.dart'; void main() { diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart index 6b7319b5ee..d97a4fb63f 100644 --- a/dart/test/transport/http_transport_test.dart +++ b/dart/test/transport/http_transport_test.dart @@ -3,16 +3,18 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:sentry/src/sentry_envelope_header.dart'; -import 'package:sentry/src/sentry_envelope_item.dart'; import 'package:sentry/src/sentry_envelope_item_header.dart'; import 'package:sentry/src/sentry_item_type.dart'; import 'package:sentry/src/transport/rate_limiter.dart'; import 'package:test/test.dart'; +import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/transport/http_transport.dart'; import '../mocks.dart'; +import '../mocks/mock_client_report_recorder.dart'; +import '../mocks/mock_hub.dart'; void main() { SentryEnvelope givenEnvelope() { @@ -152,8 +154,21 @@ class Fixture { dsn: 'https://public:secret@sentry.example.com/1', ); + late var clientReportRecorder = MockClientReportRecorder(); + HttpTransport getSut(http.Client client, RateLimiter rateLimiter) { options.httpClient = client; return HttpTransport(options, rateLimiter); } + + SentryTracer createTracer({ + bool? sampled, + }) { + final context = SentryTransactionContext( + 'name', + 'op', + sampled: sampled, + ); + return SentryTracer(context, MockHub()); + } } diff --git a/dio/test/mocks/mock_transport.dart b/dio/test/mocks/mock_transport.dart index 85a2c625b1..5b9b13de03 100644 --- a/dio/test/mocks/mock_transport.dart +++ b/dio/test/mocks/mock_transport.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:sentry/sentry.dart'; - import 'no_such_method_provider.dart'; class MockTransport with NoSuchMethodProvider implements Transport { diff --git a/flutter/test/file_system_transport_test.dart b/flutter/test/file_system_transport_test.dart index 680deb073d..f458fb2203 100644 --- a/flutter/test/file_system_transport_test.dart +++ b/flutter/test/file_system_transport_test.dart @@ -96,8 +96,9 @@ void main() { } class Fixture { + final options = SentryOptions(dsn: ''); + FileSystemTransport getSut(MethodChannel channel) { - final options = SentryOptions(dsn: ''); return FileSystemTransport(channel, options); } } diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 537916f84c..c7452aede6 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.0.16 from annotations +// Mocks generated by Mockito 5.1.0 from annotations // in sentry_flutter/example/ios/.symlinks/plugins/sentry_flutter/test/mocks.dart. // Do not manually edit this file. @@ -17,6 +17,7 @@ import 'package:sentry_flutter/src/sentry_native_channel.dart' as _i11; import 'mocks.dart' as _i12; +// ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references @@ -59,8 +60,6 @@ class MockTransport extends _i1.Mock implements _i6.Transport { (super.noSuchMethod(Invocation.method(#send, [envelope]), returnValue: Future<_i3.SentryId?>.value()) as _i7.Future<_i3.SentryId?>); - @override - String toString() => super.toString(); } /// A class which mocks [NoOpSentrySpan]. @@ -125,8 +124,6 @@ class MockNoOpSentrySpan extends _i1.Mock implements _i2.NoOpSentrySpan { _i3.SentryTraceHeader toSentryTrace() => (super.noSuchMethod(Invocation.method(#toSentryTrace, []), returnValue: _FakeSentryTraceHeader_3()) as _i3.SentryTraceHeader); - @override - String toString() => super.toString(); } /// A class which mocks [MethodChannel]. @@ -168,8 +165,6 @@ class MockMethodChannel extends _i1.Mock implements _i9.MethodChannel { _i7.Future Function(_i4.MethodCall)? handler) => super.noSuchMethod(Invocation.method(#setMethodCallHandler, [handler]), returnValueForMissingStub: null); - @override - String toString() => super.toString(); } /// A class which mocks [SentryNative]. @@ -212,8 +207,6 @@ class MockSentryNative extends _i1.Mock implements _i10.SentryNative { @override void reset() => super.noSuchMethod(Invocation.method(#reset, []), returnValueForMissingStub: null); - @override - String toString() => super.toString(); } /// A class which mocks [Hub]. @@ -375,6 +368,4 @@ class MockHub extends _i1.Mock implements _i6.Hub { super.noSuchMethod( Invocation.method(#setSpanContext, [throwable, span, transaction]), returnValueForMissingStub: null); - @override - String toString() => super.toString(); }