Skip to content

Commit dd933d4

Browse files
authored
Record dropped spans in client reports (#2154)
* Record dropped spans * Changelog * Naming * Update CHANGELOG.md * Send dropped event as well for rate limit and network error * Update * Dart analyze * Fix test * Improve comments * improvements * Apply same logic of beforeSend to event processor * Fix test * Formatting * Comments * Rename mock
1 parent 0e12dac commit dd933d4

16 files changed

+368
-91
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Record dropped spans in client reports ([#2154](https://github.com/getsentry/sentry-dart/pull/2154))
78
- Add memory usage to contexts ([#2133](https://github.com/getsentry/sentry-dart/pull/2133))
89
- Only for Linux/Windows applications, as iOS/Android/macOS use native SDKs
910

dart/lib/src/client_reports/client_report_recorder.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ class ClientReportRecorder {
1313
final ClockProvider _clock;
1414
final Map<_QuantityKey, int> _quantities = {};
1515

16-
void recordLostEvent(
17-
final DiscardReason reason, final DataCategory category) {
16+
void recordLostEvent(final DiscardReason reason, final DataCategory category,
17+
{int count = 1}) {
1818
final key = _QuantityKey(reason, category);
1919
var current = _quantities[key] ?? 0;
20-
_quantities[key] = current + 1;
20+
_quantities[key] = current + count;
2121
}
2222

2323
ClientReport? flush() {

dart/lib/src/client_reports/discarded_event.dart

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ extension _DataCategoryExtension on DataCategory {
5454
return 'session';
5555
case DataCategory.transaction:
5656
return 'transaction';
57+
case DataCategory.span:
58+
return 'span';
5759
case DataCategory.attachment:
5860
return 'attachment';
5961
case DataCategory.security:

dart/lib/src/client_reports/noop_client_report_recorder.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ class NoOpClientReportRecorder implements ClientReportRecorder {
1515
}
1616

1717
@override
18-
void recordLostEvent(DiscardReason reason, DataCategory category) {}
18+
void recordLostEvent(DiscardReason reason, DataCategory category,
19+
{int count = 1}) {}
1920
}

dart/lib/src/hub.dart

+5
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,11 @@ class Hub {
542542
DiscardReason.sampleRate,
543543
DataCategory.transaction,
544544
);
545+
_options.recorder.recordLostEvent(
546+
DiscardReason.sampleRate,
547+
DataCategory.span,
548+
count: transaction.spans.length + 1,
549+
);
545550
_options.logger(
546551
SentryLevel.warning,
547552
'Transaction ${transaction.eventId} was dropped due to sampling decision.',

dart/lib/src/sentry_client.dart

+56-21
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ class SentryClient {
8484
Hint? hint,
8585
}) async {
8686
if (_sampleRate()) {
87-
_recordLostEvent(event, DiscardReason.sampleRate);
87+
_options.recorder
88+
.recordLostEvent(DiscardReason.sampleRate, _getCategory(event));
8889
_options.logger(
8990
SentryLevel.debug,
9091
'Event ${event.eventId.toString()} was dropped due to sampling decision.',
@@ -403,7 +404,9 @@ class SentryClient {
403404
SentryEvent event,
404405
Hint hint,
405406
) async {
406-
SentryEvent? eventOrTransaction = event;
407+
SentryEvent? processedEvent = event;
408+
final spanCountBeforeCallback =
409+
event is SentryTransaction ? event.spans.length : 0;
407410

408411
final beforeSend = _options.beforeSend;
409412
final beforeSendTransaction = _options.beforeSendTransaction;
@@ -412,18 +415,18 @@ class SentryClient {
412415
try {
413416
if (event is SentryTransaction && beforeSendTransaction != null) {
414417
beforeSendName = 'beforeSendTransaction';
415-
final e = beforeSendTransaction(event);
416-
if (e is Future<SentryTransaction?>) {
417-
eventOrTransaction = await e;
418+
final callbackResult = beforeSendTransaction(event);
419+
if (callbackResult is Future<SentryTransaction?>) {
420+
processedEvent = await callbackResult;
418421
} else {
419-
eventOrTransaction = e;
422+
processedEvent = callbackResult;
420423
}
421424
} else if (beforeSend != null) {
422-
final e = beforeSend(event, hint);
423-
if (e is Future<SentryEvent?>) {
424-
eventOrTransaction = await e;
425+
final callbackResult = beforeSend(event, hint);
426+
if (callbackResult is Future<SentryEvent?>) {
427+
processedEvent = await callbackResult;
425428
} else {
426-
eventOrTransaction = e;
429+
processedEvent = callbackResult;
427430
}
428431
}
429432
} catch (exception, stackTrace) {
@@ -438,15 +441,30 @@ class SentryClient {
438441
}
439442
}
440443

441-
if (eventOrTransaction == null) {
442-
_recordLostEvent(event, DiscardReason.beforeSend);
444+
final discardReason = DiscardReason.beforeSend;
445+
if (processedEvent == null) {
446+
_options.recorder.recordLostEvent(discardReason, _getCategory(event));
447+
if (event is SentryTransaction) {
448+
// We dropped the whole transaction, the dropped count includes all child spans + 1 root span
449+
_options.recorder.recordLostEvent(discardReason, DataCategory.span,
450+
count: spanCountBeforeCallback + 1);
451+
}
443452
_options.logger(
444453
SentryLevel.debug,
445454
'${event.runtimeType} was dropped by $beforeSendName callback',
446455
);
456+
} else if (event is SentryTransaction &&
457+
processedEvent is SentryTransaction) {
458+
// If beforeSend removed only some spans we still report them as dropped
459+
final spanCountAfterCallback = processedEvent.spans.length;
460+
final droppedSpanCount = spanCountBeforeCallback - spanCountAfterCallback;
461+
if (droppedSpanCount > 0) {
462+
_options.recorder.recordLostEvent(discardReason, DataCategory.span,
463+
count: droppedSpanCount);
464+
}
447465
}
448466

449-
return eventOrTransaction;
467+
return processedEvent;
450468
}
451469

452470
Future<SentryEvent?> _runEventProcessors(
@@ -455,6 +473,9 @@ class SentryClient {
455473
required List<EventProcessor> eventProcessors,
456474
}) async {
457475
SentryEvent? processedEvent = event;
476+
int spanCountBeforeEventProcessors =
477+
event is SentryTransaction ? event.spans.length : 0;
478+
458479
for (final processor in eventProcessors) {
459480
try {
460481
final e = processor.apply(processedEvent!, hint);
@@ -474,12 +495,29 @@ class SentryClient {
474495
rethrow;
475496
}
476497
}
498+
499+
final discardReason = DiscardReason.eventProcessor;
477500
if (processedEvent == null) {
478-
_recordLostEvent(event, DiscardReason.eventProcessor);
501+
_options.recorder.recordLostEvent(discardReason, _getCategory(event));
502+
if (event is SentryTransaction) {
503+
// We dropped the whole transaction, the dropped count includes all child spans + 1 root span
504+
_options.recorder.recordLostEvent(discardReason, DataCategory.span,
505+
count: spanCountBeforeEventProcessors + 1);
506+
}
479507
_options.logger(SentryLevel.debug, 'Event was dropped by a processor');
480-
break;
508+
} else if (event is SentryTransaction &&
509+
processedEvent is SentryTransaction) {
510+
// If event processor removed only some spans we still report them as dropped
511+
final spanCountAfterEventProcessors = processedEvent.spans.length;
512+
final droppedSpanCount =
513+
spanCountBeforeEventProcessors - spanCountAfterEventProcessors;
514+
if (droppedSpanCount > 0) {
515+
_options.recorder.recordLostEvent(discardReason, DataCategory.span,
516+
count: droppedSpanCount);
517+
}
481518
}
482519
}
520+
483521
return processedEvent;
484522
}
485523

@@ -490,14 +528,11 @@ class SentryClient {
490528
return false;
491529
}
492530

493-
void _recordLostEvent(SentryEvent event, DiscardReason reason) {
494-
DataCategory category;
531+
DataCategory _getCategory(SentryEvent event) {
495532
if (event is SentryTransaction) {
496-
category = DataCategory.transaction;
497-
} else {
498-
category = DataCategory.error;
533+
return DataCategory.transaction;
499534
}
500-
_options.recorder.recordLostEvent(reason, category);
535+
return DataCategory.error;
501536
}
502537

503538
Future<SentryId?> _attachClientReportsAndSend(SentryEnvelope envelope) {

dart/lib/src/sentry_envelope_item.dart

+20-12
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import 'sentry_user_feedback.dart';
1212

1313
/// Item holding header information and JSON encoded data.
1414
class SentryEnvelopeItem {
15-
SentryEnvelopeItem(this.header, this.dataFactory);
15+
/// The original, non-encoded object, used when direct access to the source data is needed.
16+
Object? originalObject;
17+
18+
SentryEnvelopeItem(this.header, this.dataFactory, {this.originalObject});
1619

1720
/// Creates a [SentryEnvelopeItem] which sends [SentryTransaction].
1821
factory SentryEnvelopeItem.fromTransaction(SentryTransaction transaction) {
@@ -24,7 +27,8 @@ class SentryEnvelopeItem {
2427
cachedItem.getDataLength,
2528
contentType: 'application/json',
2629
);
27-
return SentryEnvelopeItem(header, cachedItem.getData);
30+
return SentryEnvelopeItem(header, cachedItem.getData,
31+
originalObject: transaction);
2832
}
2933

3034
factory SentryEnvelopeItem.fromAttachment(SentryAttachment attachment) {
@@ -37,7 +41,8 @@ class SentryEnvelopeItem {
3741
fileName: attachment.filename,
3842
attachmentType: attachment.attachmentType,
3943
);
40-
return SentryEnvelopeItem(header, cachedItem.getData);
44+
return SentryEnvelopeItem(header, cachedItem.getData,
45+
originalObject: attachment);
4146
}
4247

4348
/// Create a [SentryEnvelopeItem] which sends [SentryUserFeedback].
@@ -50,7 +55,8 @@ class SentryEnvelopeItem {
5055
cachedItem.getDataLength,
5156
contentType: 'application/json',
5257
);
53-
return SentryEnvelopeItem(header, cachedItem.getData);
58+
return SentryEnvelopeItem(header, cachedItem.getData,
59+
originalObject: feedback);
5460
}
5561

5662
/// Create a [SentryEnvelopeItem] which holds the [SentryEvent] data.
@@ -59,13 +65,13 @@ class SentryEnvelopeItem {
5965
_CachedItem(() async => utf8JsonEncoder.convert(event.toJson()));
6066

6167
return SentryEnvelopeItem(
62-
SentryEnvelopeItemHeader(
63-
SentryItemType.event,
64-
cachedItem.getDataLength,
65-
contentType: 'application/json',
66-
),
67-
cachedItem.getData,
68-
);
68+
SentryEnvelopeItemHeader(
69+
SentryItemType.event,
70+
cachedItem.getDataLength,
71+
contentType: 'application/json',
72+
),
73+
cachedItem.getData,
74+
originalObject: event);
6975
}
7076

7177
/// Create a [SentryEnvelopeItem] which holds the [ClientReport] data.
@@ -80,6 +86,7 @@ class SentryEnvelopeItem {
8086
contentType: 'application/json',
8187
),
8288
cachedItem.getData,
89+
originalObject: clientReport,
8390
);
8491
}
8592

@@ -102,7 +109,8 @@ class SentryEnvelopeItem {
102109
cachedItem.getDataLength,
103110
contentType: 'application/octet-stream',
104111
);
105-
return SentryEnvelopeItem(header, cachedItem.getData);
112+
return SentryEnvelopeItem(header, cachedItem.getData,
113+
originalObject: buckets);
106114
}
107115

108116
/// Header with info about type and length of data in bytes.

dart/lib/src/transport/data_category.dart

+21-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,28 @@ enum DataCategory {
55
error,
66
session,
77
transaction,
8+
span,
89
attachment,
910
security,
1011
metricBucket,
11-
unknown
12+
unknown;
13+
14+
static DataCategory fromItemType(String itemType) {
15+
switch (itemType) {
16+
case 'event':
17+
return DataCategory.error;
18+
case 'session':
19+
return DataCategory.session;
20+
case 'attachment':
21+
return DataCategory.attachment;
22+
case 'transaction':
23+
return DataCategory.transaction;
24+
// The envelope item type used for metrics is statsd,
25+
// whereas the client report category is metric_bucket
26+
case 'statsd':
27+
return DataCategory.metricBucket;
28+
default:
29+
return DataCategory.unknown;
30+
}
31+
}
1232
}

dart/lib/src/transport/rate_limiter.dart

+12-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1+
import '../../sentry.dart';
12
import '../transport/rate_limit_parser.dart';
2-
import '../sentry_options.dart';
3-
import '../sentry_envelope.dart';
4-
import '../sentry_envelope_item.dart';
53
import 'rate_limit.dart';
64
import 'data_category.dart';
75
import '../client_reports/discard_reason.dart';
@@ -25,8 +23,17 @@ class RateLimiter {
2523

2624
_options.recorder.recordLostEvent(
2725
DiscardReason.rateLimitBackoff,
28-
_categoryFromItemType(item.header.type),
26+
DataCategory.fromItemType(item.header.type),
2927
);
28+
29+
final originalObject = item.originalObject;
30+
if (originalObject is SentryTransaction) {
31+
_options.recorder.recordLostEvent(
32+
DiscardReason.rateLimitBackoff,
33+
DataCategory.span,
34+
count: originalObject.spans.length + 1,
35+
);
36+
}
3037
}
3138
}
3239

@@ -80,7 +87,7 @@ class RateLimiter {
8087
// Private
8188

8289
bool _isRetryAfter(String itemType) {
83-
final dataCategory = _categoryFromItemType(itemType);
90+
final dataCategory = DataCategory.fromItemType(itemType);
8491
final currentDate = DateTime.fromMillisecondsSinceEpoch(
8592
_options.clock().millisecondsSinceEpoch);
8693

@@ -106,25 +113,6 @@ class RateLimiter {
106113
return false;
107114
}
108115

109-
DataCategory _categoryFromItemType(String itemType) {
110-
switch (itemType) {
111-
case 'event':
112-
return DataCategory.error;
113-
case 'session':
114-
return DataCategory.session;
115-
case 'attachment':
116-
return DataCategory.attachment;
117-
case 'transaction':
118-
return DataCategory.transaction;
119-
// The envelope item type used for metrics is statsd,
120-
// whereas the client report category is metric_bucket
121-
case 'statsd':
122-
return DataCategory.metricBucket;
123-
default:
124-
return DataCategory.unknown;
125-
}
126-
}
127-
128116
void _applyRetryAfterOnlyIfLonger(DataCategory dataCategory, DateTime date) {
129117
final oldDate = _rateLimitedUntil[dataCategory];
130118

dart/lib/src/utils/transport_utils.dart

+15-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,21 @@ class TransportUtils {
1919
}
2020

2121
if (response.statusCode >= 400 && response.statusCode != 429) {
22-
options.recorder
23-
.recordLostEvent(DiscardReason.networkError, DataCategory.error);
22+
for (final item in envelope.items) {
23+
options.recorder.recordLostEvent(
24+
DiscardReason.networkError,
25+
DataCategory.fromItemType(item.header.type),
26+
);
27+
28+
final originalObject = item.originalObject;
29+
if (originalObject is SentryTransaction) {
30+
options.recorder.recordLostEvent(
31+
DiscardReason.networkError,
32+
DataCategory.span,
33+
count: originalObject.spans.length + 1,
34+
);
35+
}
36+
}
2437
}
2538
} else {
2639
options.logger(

0 commit comments

Comments
 (0)