diff --git a/CHANGELOG.md b/CHANGELOG.md index 763129d137..db299dfd93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Add `DioException` response data to error breadcrumb ([#3164](https://github.com/getsentry/sentry-dart/pull/3164)) - Bumped `dio` min verion to `5.2.0` +- Log a warning when dropping envelope items ([#3165](https://github.com/getsentry/sentry-dart/pull/3165)) ### Dependencies diff --git a/metrics/metrics-ios.yml b/metrics/metrics-ios.yml index 92cf114fa4..119455c8b6 100644 --- a/metrics/metrics-ios.yml +++ b/metrics/metrics-ios.yml @@ -11,4 +11,4 @@ startupTimeTest: binarySizeTest: diffMin: 1400 KiB - diffMax: 1800 KiB + diffMax: 1825 KiB diff --git a/packages/dart/lib/src/transport/rate_limiter.dart b/packages/dart/lib/src/transport/rate_limiter.dart index fa0f7a9018..1cba8eb1ca 100644 --- a/packages/dart/lib/src/transport/rate_limiter.dart +++ b/packages/dart/lib/src/transport/rate_limiter.dart @@ -25,6 +25,9 @@ class RateLimiter { DiscardReason.rateLimitBackoff, DataCategory.fromItemType(item.header.type), ); + _logDebugWarning( + 'Envelope item of type "${item.header.type}" was dropped due to rate limiting.', + ); final originalObject = item.originalObject; if (originalObject is SentryTransaction) { @@ -48,6 +51,9 @@ class RateLimiter { // no reason to continue if (toSend.isEmpty) { + _logDebugWarning( + 'Envelope was dropped due to rate limiting.', + ); return null; } @@ -121,4 +127,17 @@ class RateLimiter { _rateLimitedUntil[dataCategory] = date; } } + + // Enable debug mode to log warning messages + void _logDebugWarning(String message) { + var debug = _options.debug; + if (!debug) { + // Surface the log even if debug is disabled + _options.debug = true; + } + _options.log(SentryLevel.warning, message); + if (debug != _options.debug) { + _options.debug = debug; + } + } } diff --git a/packages/dart/test/protocol/rate_limiter_test.dart b/packages/dart/test/protocol/rate_limiter_test.dart index 9a6079810b..a4845f6430 100644 --- a/packages/dart/test/protocol/rate_limiter_test.dart +++ b/packages/dart/test/protocol/rate_limiter_test.dart @@ -319,6 +319,116 @@ void main() { expect(DataCategory.fromItemType('unknown'), DataCategory.unknown); }); }); + + group('RateLimiter logging', () { + test('logs warning for dropped item and full envelope', () { + final options = defaultTestOptions(); + options.debug = false; + options.diagnosticLevel = SentryLevel.warning; + + final logCalls = <_LogCall>[]; + void mockLogger( + SentryLevel level, + String message, { + String? logger, + Object? exception, + StackTrace? stackTrace, + }) { + logCalls.add(_LogCall(level, message)); + } + + options.log = mockLogger; + + final rateLimiter = RateLimiter(options); + + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + final envelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + // Apply rate limit for error (event) + rateLimiter.updateRetryAfterLimits( + '1:error:key, 5:error:organization', null, 1); + + // Filter should drop the entire envelope + final result = rateLimiter.filter(envelope); + expect(result, isNull); + + // Expect 2 warning logs: item dropped + all items dropped + expect(logCalls.length, 2); + + final itemLog = logCalls[0]; + expect(itemLog.level, SentryLevel.warning); + expect( + itemLog.message, + contains( + 'Envelope item of type "event" was dropped due to rate limiting'), + ); + + final fullDropLog = logCalls[1]; + expect(fullDropLog.level, SentryLevel.warning); + expect( + fullDropLog.message, + contains('Envelope was dropped due to rate limiting'), + ); + + expect(options.debug, isFalse); + }); + + test('logs warning for each dropped item only when some items are sent', + () { + final options = defaultTestOptions(); + options.debug = false; + options.diagnosticLevel = SentryLevel.warning; + + final logCalls = <_LogCall>[]; + void mockLogger( + SentryLevel level, + String message, { + String? logger, + Object? exception, + StackTrace? stackTrace, + }) { + logCalls.add(_LogCall(level, message)); + } + + options.log = mockLogger; + + final rateLimiter = RateLimiter(options); + + // One event (error) and one transaction + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + final transaction = fixture.getTransaction(); + final transactionItem = SentryEnvelopeItem.fromTransaction(transaction); + + final envelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem, transactionItem], + ); + + // Apply rate limit only for errors so the transaction can still be sent + rateLimiter.updateRetryAfterLimits('60:error:key', null, 1); + + final result = rateLimiter.filter(envelope); + expect(result, isNotNull); + expect(result!.items.length, 1); + expect(result.items.first.header.type, 'transaction'); + + // Expect only 1 warning log: per-item drop (no summary) + expect(logCalls.length, 1); + + final itemLog = logCalls.first; + expect(itemLog.level, SentryLevel.warning); + expect( + itemLog.message, + contains( + 'Envelope item of type "event" was dropped due to rate limiting'), + ); + + expect(options.debug, isFalse); + }); + }); } class Fixture { @@ -348,3 +458,10 @@ class Fixture { return SentryTransaction(tracer); } } + +class _LogCall { + final SentryLevel level; + final String message; + + _LogCall(this.level, this.message); +} diff --git a/packages/flutter/lib/src/native/cocoa/binding.dart b/packages/flutter/lib/src/native/cocoa/binding.dart index 789070cf68..310e7ed426 100644 --- a/packages/flutter/lib/src/native/cocoa/binding.dart +++ b/packages/flutter/lib/src/native/cocoa/binding.dart @@ -26,6 +26,7 @@ external ffi.Pointer _SentryCocoa_wrapBlockingBlock_xtuoz7( @ffi.Native< ffi.Pointer Function( ffi.Pointer, ffi.Pointer)>() +// ignore: unused_element external ffi.Pointer _SentryCocoa_protocolTrampoline_1mbt9g9( ffi.Pointer target, ffi.Pointer arg0,