diff --git a/CHANGELOG.md b/CHANGELOG.md index 77024f99d78..dc00388b896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Record dropped spans (#4172) + ### Fixes - Session replay crash when writing the replay (#4186) diff --git a/SentryTestUtils/Invocations.swift b/SentryTestUtils/Invocations.swift index 60c3f14ea30..0791d691421 100644 --- a/SentryTestUtils/Invocations.swift +++ b/SentryTestUtils/Invocations.swift @@ -41,6 +41,15 @@ public class Invocations { } } + public func get(_ index: Int) -> T? { + return queue.sync { + guard self._invocations.indices.contains(index) else { + return nil + } + return self._invocations[index] + } + } + public func record(_ invocation: T) { queue.async { self._invocations.append(invocation) diff --git a/SentryTestUtils/TestClient.swift b/SentryTestUtils/TestClient.swift index fe3875af8a5..df10551fc01 100644 --- a/SentryTestUtils/TestClient.swift +++ b/SentryTestUtils/TestClient.swift @@ -127,6 +127,11 @@ public class TestClient: SentryClient { recordLostEvents.record((category, reason)) } + public var recordLostEventsWithQauntity = Invocations<(category: SentryDataCategory, reason: SentryDiscardReason, quantity: UInt)>() + public override func recordLostEvent(_ category: SentryDataCategory, reason: SentryDiscardReason, quantity: UInt) { + recordLostEventsWithQauntity.record((category, reason, quantity)) + } + public var flushInvocations = Invocations() public override func flush(timeout: TimeInterval) { flushInvocations.record(timeout) diff --git a/SentryTestUtils/TestTransport.swift b/SentryTestUtils/TestTransport.swift index 03ed7359e6a..9930713f699 100644 --- a/SentryTestUtils/TestTransport.swift +++ b/SentryTestUtils/TestTransport.swift @@ -13,6 +13,11 @@ public class TestTransport: NSObject, Transport { recordLostEvents.record((category, reason)) } + public var recordLostEventsWithCount = Invocations<(category: SentryDataCategory, reason: SentryDiscardReason, quantity: UInt)>() + public func recordLostEvent(_ category: SentryDataCategory, reason: SentryDiscardReason, quantity: UInt) { + recordLostEventsWithCount.record((category, reason, quantity)) + } + public var flushInvocations = Invocations() public func flush(_ timeout: TimeInterval) -> SentryFlushResult { flushInvocations.record(timeout) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index a02f38f7d15..4e6c60daf4a 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -571,6 +571,13 @@ - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason [self.transportAdapter recordLostEvent:category reason:reason]; } +- (void)recordLostEvent:(SentryDataCategory)category + reason:(SentryDiscardReason)reason + quantity:(NSUInteger)quantity +{ + [self.transportAdapter recordLostEvent:category reason:reason quantity:quantity]; +} + - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event withScope:(SentryScope *)scope alwaysAttachStacktrace:(BOOL)alwaysAttachStacktrace @@ -719,13 +726,35 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event event.user.ipAddress = @"{{auto}}"; } + BOOL eventIsATransaction + = event.type != nil && [event.type isEqualToString:SentryEnvelopeItemTypeTransaction]; + BOOL eventIsATransactionClass + = eventIsATransaction && [event isKindOfClass:[SentryTransaction class]]; + + NSUInteger currentSpanCount; + if (eventIsATransactionClass) { + SentryTransaction *transaction = (SentryTransaction *)event; + currentSpanCount = transaction.spans.count; + } else { + currentSpanCount = 0; + } + event = [self callEventProcessors:event]; if (event == nil) { [self recordLost:eventIsNotATransaction reason:kSentryDiscardReasonEventProcessor]; + if (eventIsATransaction) { + // We dropped the whole transaction, the dropped count includes all child spans + 1 root + // span + [self recordLostSpanWithReason:kSentryDiscardReasonEventProcessor + quantity:currentSpanCount + 1]; + } + } else { + if (eventIsATransactionClass) { + [self recordPartiallyDroppedSpans:(SentryTransaction *)event + withReason:kSentryDiscardReasonEventProcessor + withCurrentSpanCount:¤tSpanCount]; + } } - - BOOL eventIsATransaction - = event.type != nil && [event.type isEqualToString:SentryEnvelopeItemTypeTransaction]; if (event != nil && eventIsATransaction && self.options.beforeSendSpan != nil) { SentryTransaction *transaction = (SentryTransaction *)event; NSMutableArray> *processedSpans = [NSMutableArray array]; @@ -735,15 +764,31 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event [processedSpans addObject:processedSpan]; } } - transaction.spans = processedSpans; + + if (eventIsATransactionClass) { + [self recordPartiallyDroppedSpans:transaction + withReason:kSentryDiscardReasonBeforeSend + withCurrentSpanCount:¤tSpanCount]; + } } if (event != nil && nil != self.options.beforeSend) { event = self.options.beforeSend(event); - if (event == nil) { [self recordLost:eventIsNotATransaction reason:kSentryDiscardReasonBeforeSend]; + if (eventIsATransaction) { + // We dropped the whole transaction, the dropped count includes all child spans + 1 + // root span + [self recordLostSpanWithReason:kSentryDiscardReasonBeforeSend + quantity:currentSpanCount + 1]; + } + } else { + if (eventIsATransactionClass) { + [self recordPartiallyDroppedSpans:(SentryTransaction *)event + withReason:kSentryDiscardReasonBeforeSend + withCurrentSpanCount:¤tSpanCount]; + } } } @@ -758,6 +803,19 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event return event; } +- (void)recordPartiallyDroppedSpans:(SentryTransaction *)transaction + withReason:(SentryDiscardReason)reason + withCurrentSpanCount:(NSUInteger *)currentSpanCount +{ + // If some spans got removed we still report them as dropped + NSUInteger spanCountAfter = transaction.spans.count; + NSUInteger droppedSpanCount = *currentSpanCount - spanCountAfter; + if (droppedSpanCount > 0) { + [self recordLostSpanWithReason:reason quantity:droppedSpanCount]; + } + *currentSpanCount = spanCountAfter; +} + - (BOOL)isSampled:(NSNumber *)sampleRate { if (sampleRate == nil) { @@ -971,6 +1029,11 @@ - (void)recordLost:(BOOL)eventIsNotATransaction reason:(SentryDiscardReason)reas } } +- (void)recordLostSpanWithReason:(SentryDiscardReason)reason quantity:(NSUInteger)quantity +{ + [self recordLostEvent:kSentryDataCategorySpan reason:reason quantity:quantity]; +} + - (void)addAttachmentProcessor:(id)attachmentProcessor { [self.attachmentProcessors addObject:attachmentProcessor]; diff --git a/Sources/Sentry/SentryDataCategoryMapper.m b/Sources/Sentry/SentryDataCategoryMapper.m index 0749c13762d..58d5a655eec 100644 --- a/Sources/Sentry/SentryDataCategoryMapper.m +++ b/Sources/Sentry/SentryDataCategoryMapper.m @@ -14,6 +14,7 @@ NSString *const kSentryDataCategoryNameProfileChunk = @"profile_chunk"; NSString *const kSentryDataCategoryNameReplay = @"replay"; NSString *const kSentryDataCategoryNameMetricBucket = @"metric_bucket"; +NSString *const kSentryDataCategoryNameSpan = @"span"; NSString *const kSentryDataCategoryNameUnknown = @"unknown"; NS_ASSUME_NONNULL_BEGIN @@ -47,6 +48,7 @@ if ([itemType isEqualToString:SentryEnvelopeItemTypeStatsd]) { return kSentryDataCategoryMetricBucket; } + return kSentryDataCategoryDefault; } @@ -96,6 +98,9 @@ if ([value isEqualToString:kSentryDataCategoryNameMetricBucket]) { return kSentryDataCategoryMetricBucket; } + if ([value isEqualToString:kSentryDataCategoryNameSpan]) { + return kSentryDataCategorySpan; + } return kSentryDataCategoryUnknown; } @@ -132,6 +137,8 @@ return kSentryDataCategoryNameUnknown; case kSentryDataCategoryReplay: return kSentryDataCategoryNameReplay; + case kSentryDataCategorySpan: + return kSentryDataCategoryNameSpan; } } diff --git a/Sources/Sentry/SentryEnvelopeRateLimit.m b/Sources/Sentry/SentryEnvelopeRateLimit.m index 29841e7abed..dd44806a601 100644 --- a/Sources/Sentry/SentryEnvelopeRateLimit.m +++ b/Sources/Sentry/SentryEnvelopeRateLimit.m @@ -59,7 +59,7 @@ - (SentryEnvelope *)removeRateLimitedItems:(SentryEnvelope *)envelope = sentryDataCategoryForEnvelopItemType(item.header.type); if ([self.rateLimits isRateLimitActive:rateLimitCategory]) { [itemsToDrop addObject:item]; - [self.delegate envelopeItemDropped:rateLimitCategory]; + [self.delegate envelopeItemDropped:item withCategory:rateLimitCategory]; } } diff --git a/Sources/Sentry/SentryFileManager.m b/Sources/Sentry/SentryFileManager.m index ad9497d2c91..3793f1ea810 100644 --- a/Sources/Sentry/SentryFileManager.m +++ b/Sources/Sentry/SentryFileManager.m @@ -365,7 +365,7 @@ - (void)handleEnvelopesLimit continue; } - [_delegate envelopeItemDeleted:rateLimitCategory]; + [_delegate envelopeItemDeleted:item withCategory:rateLimitCategory]; } [self removeFileAtPath:envelopeFilePath]; diff --git a/Sources/Sentry/SentryHttpTransport.m b/Sources/Sentry/SentryHttpTransport.m index 2d133079706..612ec264ca0 100644 --- a/Sources/Sentry/SentryHttpTransport.m +++ b/Sources/Sentry/SentryHttpTransport.m @@ -1,5 +1,6 @@ #import "SentryHttpTransport.h" #import "SentryClientReport.h" +#import "SentryDataCategory.h" #import "SentryDataCategoryMapper.h" #import "SentryDependencyContainer.h" #import "SentryDiscardReasonMapper.h" @@ -139,6 +140,13 @@ - (void)sendEnvelope:(SentryEnvelope *)envelope } - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason)reason +{ + [self recordLostEvent:category reason:reason quantity:1]; +} + +- (void)recordLostEvent:(SentryDataCategory)category + reason:(SentryDiscardReason)reason + quantity:(NSUInteger)quantity { if (!self.options.sendClientReports) { return; @@ -149,7 +157,6 @@ - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason @synchronized(self.discardedEvents) { SentryDiscardedEvent *event = self.discardedEvents[key]; - NSUInteger quantity = 1; if (event != nil) { quantity = event.quantity + 1; } @@ -225,17 +232,21 @@ - (SentryFlushResult)flush:(NSTimeInterval)timeout /** * SentryEnvelopeRateLimitDelegate. */ -- (void)envelopeItemDropped:(SentryDataCategory)dataCategory +- (void)envelopeItemDropped:(SentryEnvelopeItem *)envelopeItem + withCategory:(SentryDataCategory)dataCategory; { [self recordLostEvent:dataCategory reason:kSentryDiscardReasonRateLimitBackoff]; + [self recordLostSpans:envelopeItem reason:kSentryDiscardReasonRateLimitBackoff]; } /** * SentryFileManagerDelegate. */ -- (void)envelopeItemDeleted:(SentryDataCategory)dataCategory +- (void)envelopeItemDeleted:(SentryEnvelopeItem *)envelopeItem + withCategory:(SentryDataCategory)dataCategory { [self recordLostEvent:dataCategory reason:kSentryDiscardReasonCacheOverflow]; + [self recordLostSpans:envelopeItem reason:kSentryDiscardReasonCacheOverflow]; } #pragma mark private methods @@ -389,6 +400,20 @@ - (void)recordLostEventFor:(NSArray *)items } SentryDataCategory category = sentryDataCategoryForEnvelopItemType(itemType); [self recordLostEvent:category reason:kSentryDiscardReasonNetworkError]; + [self recordLostSpans:item reason:kSentryDiscardReasonNetworkError]; + } +} + +- (void)recordLostSpans:(SentryEnvelopeItem *)envelopeItem reason:(SentryDiscardReason)reason +{ + if ([SentryEnvelopeItemTypeTransaction isEqualToString:envelopeItem.header.type]) { + NSDictionary *transactionJson = + [SentrySerialization deserializeEventEnvelopeItem:envelopeItem.data]; + if (transactionJson == nil) { + return; + } + NSArray *spans = transactionJson[@"spans"] ?: [NSArray array]; + [self recordLostEvent:kSentryDataCategorySpan reason:reason quantity:spans.count + 1]; } } diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index f39b7543eba..e7a3c14baac 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -285,6 +285,9 @@ - (void)captureTransaction:(SentryTransaction *)transaction if (decision != kSentrySampleDecisionYes) { [self.client recordLostEvent:kSentryDataCategoryTransaction reason:kSentryDiscardReasonSampleRate]; + [self.client recordLostEvent:kSentryDataCategorySpan + reason:kSentryDiscardReasonSampleRate + quantity:transaction.spans.count + 1]; return; } diff --git a/Sources/Sentry/SentrySpotlightTransport.m b/Sources/Sentry/SentrySpotlightTransport.m index 7f528bed8fb..303ef5a83a8 100644 --- a/Sources/Sentry/SentrySpotlightTransport.m +++ b/Sources/Sentry/SentrySpotlightTransport.m @@ -96,11 +96,19 @@ - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason // Empty on purpose } +- (void)recordLostEvent:(SentryDataCategory)category + reason:(SentryDiscardReason)reason + quantity:(NSUInteger)quantity +{ + // Empty on purpose +} + #if defined(TEST) || defined(TESTCI) || defined(DEBUG) - (void)setStartFlushCallback:(nonnull void (^)(void))callback { // Empty on purpose } + #endif // defined(TEST) || defined(TESTCI) || defined(DEBUG) @end diff --git a/Sources/Sentry/SentryTransportAdapter.m b/Sources/Sentry/SentryTransportAdapter.m index 06c14609ee9..1e65b46abc4 100644 --- a/Sources/Sentry/SentryTransportAdapter.m +++ b/Sources/Sentry/SentryTransportAdapter.m @@ -102,6 +102,15 @@ - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason } } +- (void)recordLostEvent:(SentryDataCategory)category + reason:(SentryDiscardReason)reason + quantity:(NSUInteger)quantity +{ + for (id transport in self.transports) { + [transport recordLostEvent:category reason:reason quantity:quantity]; + } +} + - (void)flush:(NSTimeInterval)timeout { for (id transport in self.transports) { diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index 5bd2d6f3387..356eac4a2ea 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -57,6 +57,9 @@ SentryClient () - (void)captureEnvelope:(SentryEnvelope *)envelope; - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason)reason; +- (void)recordLostEvent:(SentryDataCategory)category + reason:(SentryDiscardReason)reason + quantity:(NSUInteger)quantity; - (void)addAttachmentProcessor:(id)attachmentProcessor; - (void)removeAttachmentProcessor:(id)attachmentProcessor; diff --git a/Sources/Sentry/include/SentryDataCategory.h b/Sources/Sentry/include/SentryDataCategory.h index 7d4891c8487..3a384add2cb 100644 --- a/Sources/Sentry/include/SentryDataCategory.h +++ b/Sources/Sentry/include/SentryDataCategory.h @@ -17,5 +17,6 @@ typedef NS_ENUM(NSUInteger, SentryDataCategory) { kSentryDataCategoryMetricBucket = 8, kSentryDataCategoryReplay = 9, kSentryDataCategoryProfileChunk = 10, - kSentryDataCategoryUnknown = 11 + kSentryDataCategorySpan = 11, + kSentryDataCategoryUnknown = 12, }; diff --git a/Sources/Sentry/include/SentryDataCategoryMapper.h b/Sources/Sentry/include/SentryDataCategoryMapper.h index 6b918dcee99..677996907e2 100644 --- a/Sources/Sentry/include/SentryDataCategoryMapper.h +++ b/Sources/Sentry/include/SentryDataCategoryMapper.h @@ -14,6 +14,7 @@ FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameProfile; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameProfileChunk; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameReplay; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameMetricBucket; +FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameSpan; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUnknown; SentryDataCategory sentryDataCategoryForNSUInteger(NSUInteger value); diff --git a/Sources/Sentry/include/SentryEnvelopeRateLimit.h b/Sources/Sentry/include/SentryEnvelopeRateLimit.h index ca885a0d520..283892427d5 100644 --- a/Sources/Sentry/include/SentryEnvelopeRateLimit.h +++ b/Sources/Sentry/include/SentryEnvelopeRateLimit.h @@ -3,7 +3,7 @@ @protocol SentryEnvelopeRateLimitDelegate; -@class SentryEnvelope; +@class SentryEnvelope, SentryEnvelopeItem; NS_ASSUME_NONNULL_BEGIN @@ -23,7 +23,8 @@ NS_SWIFT_NAME(EnvelopeRateLimit) @protocol SentryEnvelopeRateLimitDelegate -- (void)envelopeItemDropped:(SentryDataCategory)dataCategory; +- (void)envelopeItemDropped:(SentryEnvelopeItem *)envelopeItem + withCategory:(SentryDataCategory)dataCategory; @end diff --git a/Sources/Sentry/include/SentryFileManager.h b/Sources/Sentry/include/SentryFileManager.h index 531e13db156..698d9e5d57a 100644 --- a/Sources/Sentry/include/SentryFileManager.h +++ b/Sources/Sentry/include/SentryFileManager.h @@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN @class SentryDispatchQueueWrapper; @class SentryEvent; @class SentryEnvelope; +@class SentryEnvelopeItem; @class SentryFileContents; @class SentryOptions; @class SentrySession; @@ -133,7 +134,8 @@ SENTRY_EXTERN void removeAppLaunchProfilingConfigFile(void); @protocol SentryFileManagerDelegate -- (void)envelopeItemDeleted:(SentryDataCategory)dataCategory; +- (void)envelopeItemDeleted:(SentryEnvelopeItem *)envelopeItem + withCategory:(SentryDataCategory)dataCategory; @end diff --git a/Sources/Sentry/include/SentryTransport.h b/Sources/Sentry/include/SentryTransport.h index 340562d4d15..c3cdcd1bd09 100644 --- a/Sources/Sentry/include/SentryTransport.h +++ b/Sources/Sentry/include/SentryTransport.h @@ -19,6 +19,10 @@ NS_SWIFT_NAME(Transport) - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason)reason; +- (void)recordLostEvent:(SentryDataCategory)category + reason:(SentryDiscardReason)reason + quantity:(NSUInteger)quantity; + - (SentryFlushResult)flush:(NSTimeInterval)timeout; #if defined(TEST) || defined(TESTCI) || defined(DEBUG) diff --git a/Sources/Sentry/include/SentryTransportAdapter.h b/Sources/Sentry/include/SentryTransportAdapter.h index d0a00f28062..81c7f36fc44 100644 --- a/Sources/Sentry/include/SentryTransportAdapter.h +++ b/Sources/Sentry/include/SentryTransportAdapter.h @@ -45,6 +45,10 @@ SENTRY_NO_INIT - (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason)reason; +- (void)recordLostEvent:(SentryDataCategory)category + reason:(SentryDiscardReason)reason + quantity:(NSUInteger)quantity; + - (void)flush:(NSTimeInterval)timeout; @end diff --git a/Tests/SentryTests/Helper/TestFileManagerDelegate.swift b/Tests/SentryTests/Helper/TestFileManagerDelegate.swift index d17969f670b..41391936720 100644 --- a/Tests/SentryTests/Helper/TestFileManagerDelegate.swift +++ b/Tests/SentryTests/Helper/TestFileManagerDelegate.swift @@ -4,7 +4,7 @@ import SentryTestUtils class TestFileManagerDelegate: NSObject, SentryFileManagerDelegate { var envelopeItemsDeleted = Invocations() - func envelopeItemDeleted(_ dataCategory: SentryDataCategory) { + func envelopeItemDeleted(_ envelopeItem: SentryEnvelopeItem, with dataCategory: SentryDataCategory) { envelopeItemsDeleted.record(dataCategory) } } diff --git a/Tests/SentryTests/Networking/RateLimits/TestEnvelopeRateLimitDelegate.swift b/Tests/SentryTests/Networking/RateLimits/TestEnvelopeRateLimitDelegate.swift index 44a62c327f1..392628fc827 100644 --- a/Tests/SentryTests/Networking/RateLimits/TestEnvelopeRateLimitDelegate.swift +++ b/Tests/SentryTests/Networking/RateLimits/TestEnvelopeRateLimitDelegate.swift @@ -4,7 +4,7 @@ import SentryTestUtils class TestEnvelopeRateLimitDelegate: NSObject, SentryEnvelopeRateLimitDelegate { var envelopeItemsDropped = Invocations() - func envelopeItemDropped(_ dataCategory: SentryDataCategory) { + func envelopeItemDropped(_ envelopeItem: SentryEnvelopeItem, with dataCategory: SentryDataCategory) { envelopeItemsDropped.record(dataCategory) } } diff --git a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift index 59436e560b0..cb0b6d6099b 100644 --- a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift +++ b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift @@ -27,9 +27,10 @@ class SentryDataCategoryMapperTests: XCTestCase { XCTAssertEqual(sentryDataCategoryForNSUInteger(8), .metricBucket) XCTAssertEqual(sentryDataCategoryForNSUInteger(9), .replay) XCTAssertEqual(sentryDataCategoryForNSUInteger(10), .profileChunk) - XCTAssertEqual(sentryDataCategoryForNSUInteger(11), .unknown) + XCTAssertEqual(sentryDataCategoryForNSUInteger(11), .span) + XCTAssertEqual(sentryDataCategoryForNSUInteger(12), .unknown) - XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(11), "Failed to map unknown category number to case .unknown") + XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(13), "Failed to map unknown category number to case .unknown") } func testMapStringToCategory() { @@ -44,6 +45,7 @@ class SentryDataCategoryMapperTests: XCTestCase { XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameProfileChunk), .profileChunk) XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameMetricBucket), .metricBucket) XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameReplay), .replay) + XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameSpan), .span) XCTAssertEqual(sentryDataCategoryForString(kSentryDataCategoryNameUnknown), .unknown) XCTAssertEqual(.unknown, sentryDataCategoryForString("gdfagdfsa"), "Failed to map unknown category name to case .unknown") @@ -61,6 +63,7 @@ class SentryDataCategoryMapperTests: XCTestCase { XCTAssertEqual(nameForSentryDataCategory(.profileChunk), kSentryDataCategoryNameProfileChunk) XCTAssertEqual(nameForSentryDataCategory(.metricBucket), kSentryDataCategoryNameMetricBucket) XCTAssertEqual(nameForSentryDataCategory(.replay), kSentryDataCategoryNameReplay) + XCTAssertEqual(nameForSentryDataCategory(.span), kSentryDataCategoryNameSpan) XCTAssertEqual(nameForSentryDataCategory(.unknown), kSentryDataCategoryNameUnknown) } } diff --git a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift index 62366e822d9..86e9e254bc3 100644 --- a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift +++ b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift @@ -2,6 +2,7 @@ import SentryTestUtils import XCTest +// swiftlint:disable file_length class SentryHttpTransportTests: XCTestCase { private static let dsnAsString = TestConstants.dsnAsString(username: "SentryHttpTransportTests") @@ -10,7 +11,11 @@ class SentryHttpTransportTests: XCTestCase { let event: Event let eventEnvelope: SentryEnvelope let eventRequest: SentryNSURLRequest + let transaction: Transaction + let transactionEnvelope: SentryEnvelope + let transactionRequest: SentryNSURLRequest let attachmentEnvelopeItem: SentryEnvelopeItem + let transactionEnvelopeItem: SentryEnvelopeItem let eventWithAttachmentRequest: SentryNSURLRequest let eventWithSessionEnvelope: SentryEnvelope let eventWithSessionRequest: SentryNSURLRequest @@ -53,15 +58,31 @@ class SentryHttpTransportTests: XCTestCase { event = Event() event.message = SentryMessage(formatted: "Some message") + let tracer = SentryTracer(transactionContext: TransactionContext(name: "SomeTransaction", operation: "SomeOperation"), hub: nil) + transaction = Transaction( + trace: tracer, + children: [ + tracer.startChild(operation: "child1"), + tracer.startChild(operation: "child2"), + tracer.startChild(operation: "child3") + ] + ) + eventRequest = buildRequest(SentryEnvelope(event: event)) + transactionRequest = buildRequest(SentryEnvelope(event: transaction)) attachmentEnvelopeItem = SentryEnvelopeItem(attachment: TestData.dataAttachment, maxAttachmentSize: 5 * 1_024 * 1_024)! - + transactionEnvelopeItem = SentryEnvelopeItem(event: transaction) + eventEnvelope = SentryEnvelope(id: event.eventId, items: [SentryEnvelopeItem(event: event), attachmentEnvelopeItem]) // We are comparing byte data and the `sentAt` header is also set in the transport, so we also need them here in the expected envelope. eventEnvelope.header.sentAt = SentryDependencyContainer.sharedInstance().dateProvider.date() eventWithAttachmentRequest = buildRequest(eventEnvelope) - + + transactionEnvelope = SentryEnvelope(id: transaction.eventId, items: [SentryEnvelopeItem(event: transaction), attachmentEnvelopeItem]) + // We are comparing byte data and the `sentAt` header is also set in the transport, so we also need them here in the expected envelope. + transactionEnvelope.header.sentAt = SentryDependencyContainer.sharedInstance().dateProvider.date() + session = SentrySession(releaseName: "2.0.1", distinctId: "some-id") sessionEnvelope = SentryEnvelope(id: nil, singleItem: SentryEnvelopeItem(session: session)) sessionEnvelope.header.sentAt = SentryDependencyContainer.sharedInstance().dateProvider.date() @@ -530,6 +551,35 @@ class SentryHttpTransportTests: XCTestCase { XCTAssertEqual(clientReportRequest.httpBody, actualEventRequest?.httpBody, "Client report not sent.") } + func testTransactionRateLimited_RecordsLostSpans() { + let clientReport = SentryClientReport( + discardedEvents: [ + SentryDiscardedEvent(reason: .rateLimitBackoff, category: .transaction, quantity: 1), + SentryDiscardedEvent(reason: .rateLimitBackoff, category: .span, quantity: 4) + ] + ) + + let clientReportEnvelopeItems = [ + fixture.attachmentEnvelopeItem, + SentryEnvelopeItem(clientReport: clientReport) + ] + + let clientReportEnvelope = SentryEnvelope(id: fixture.transaction.eventId, items: clientReportEnvelopeItems) + clientReportEnvelope.header.sentAt = SentryDependencyContainer.sharedInstance().dateProvider.date() + let clientReportRequest = SentryHttpTransportTests.buildRequest(clientReportEnvelope) + + givenRateLimitResponse(forCategory: "transaction") + + sut.send(envelope: fixture.transactionEnvelope) + waitForAllRequests() + + sut.send(envelope: fixture.transactionEnvelope) + waitForAllRequests() + + let actualEventRequest = fixture.requestManager.requests.last + XCTAssertEqual(clientReportRequest.httpBody, actualEventRequest?.httpBody, "Client report not sent.") + } + func testCacheFull_RecordsLostEvent() { givenNoInternetConnection() for _ in 0...fixture.options.maxCacheItems { @@ -547,6 +597,26 @@ class SentryHttpTransportTests: XCTestCase { XCTAssertEqual(1, deletedError?.quantity) XCTAssertEqual(1, attachment?.quantity) } + + func testCacheFull_RecordsLostSpans() { + givenNoInternetConnection() + for _ in 0...fixture.options.maxCacheItems { + sut.send(envelope: fixture.transactionEnvelope) + } + + waitForAllRequests() + + let dict = Dynamic(sut).discardedEvents.asDictionary as? [String: SentryDiscardedEvent] + XCTAssertNotNil(dict) + XCTAssertEqual(3, dict?.count) + + let transaction = dict?["transaction:cache_overflow"] + let span = dict?["span:cache_overflow"] + let attachment = dict?["attachment:cache_overflow"] + XCTAssertEqual(1, transaction?.quantity) + XCTAssertEqual(4, span?.quantity) + XCTAssertEqual(1, attachment?.quantity) + } func testSendEnvelopesConcurrent() { fixture.requestManager.responseDelay = 0.0001 @@ -623,6 +693,29 @@ class SentryHttpTransportTests: XCTestCase { assertRequestsSent(requestCount: 1) } + func testBuildingRequestFails_RecordsLostSpans() { + sendTransaction() + + fixture.requestBuilder.shouldFailWithError = true + sendTransaction() + + let dict = Dynamic(sut).discardedEvents.asDictionary as? [String: SentryDiscardedEvent] + XCTAssertNotNil(dict) + XCTAssertEqual(3, dict?.count) + + let transaction = dict?["transaction:network_error"] + XCTAssertEqual(1, transaction?.quantity) + + let span = dict?["span:network_error"] + XCTAssertEqual(4, span?.quantity) + + let attachment = dict?["attachment:network_error"] + XCTAssertEqual(1, attachment?.quantity) + + assertEnvelopesStored(envelopeCount: 0) + assertRequestsSent(requestCount: 1) + } + func testBuildingRequestFails_ClientReportNotRecordedAsLostEvent() { fixture.requestBuilder.shouldFailWithError = true sendEvent() @@ -931,6 +1024,15 @@ class SentryHttpTransportTests: XCTestCase { private func sendEventAsync() { sut.send(envelope: fixture.eventEnvelope) } + + private func sendTransaction() { + sendTransactionAsync() + waitForAllRequests() + } + + private func sendTransactionAsync() { + sut.send(envelope: fixture.transactionEnvelope) + } private func sendEnvelope(envelope: SentryEnvelope = TestConstants.envelope) { sut.send(envelope: envelope) @@ -981,3 +1083,4 @@ class SentryHttpTransportTests: XCTestCase { XCTAssertEqual(0, dict?.count) } } +// swiftlint:enable file_length diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 95dd82cf004..b0b632721bf 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1260,6 +1260,181 @@ class SentryClientTest: XCTestCase { assertLostEventRecorded(category: .transaction, reason: .eventProcessor) } + + func testRecordEventProcessorDroppingTransaction() { + SentryGlobalEventProcessor.shared().add { _ in return nil } + + let transaction = Transaction( + trace: fixture.trace, + children: [ + fixture.trace.startChild(operation: "child1"), + fixture.trace.startChild(operation: "child2"), + fixture.trace.startChild(operation: "child3") + ] + ) + + fixture.getSut().capture(event: transaction) + + assertLostEventWithCountRecorded(category: .span, reason: .eventProcessor, quantity: 4) + } + + func testRecordEventProcessorDroppingPartiallySpans() { + SentryGlobalEventProcessor.shared().add { event in + if let transaction = event as? Transaction { + transaction.spans = transaction.spans.filter { + $0.operation != "child2" + } + return transaction + } else { + return event + } + } + + let transaction = Transaction( + trace: fixture.trace, + children: [ + fixture.trace.startChild(operation: "child1"), + fixture.trace.startChild(operation: "child2"), + fixture.trace.startChild(operation: "child3") + ] + ) + + fixture.getSut().capture(event: transaction) + + assertLostEventWithCountRecorded(category: .span, reason: .eventProcessor, quantity: 1) + } + + func testRecordBeforeSendSpanDroppingPartiallySpans() { + let transaction = Transaction( + trace: fixture.trace, + children: [ + fixture.trace.startChild(operation: "child1"), + fixture.trace.startChild(operation: "child2"), + fixture.trace.startChild(operation: "child3") + ] + ) + + let numberOfSpansDropped: UInt = 2 + var dropped: UInt = 0 + fixture.getSut(configureOptions: { options in + options.beforeSendSpan = { span in + if dropped < numberOfSpansDropped { + dropped++ + return nil + } else { + return span + } + } + }).capture(event: transaction) + + assertLostEventWithCountRecorded(category: .span, reason: .beforeSend, quantity: numberOfSpansDropped) + } + + func testRecordBeforeSendDroppingTransaction() { + let transaction = Transaction( + trace: fixture.trace, + children: [ + fixture.trace.startChild(operation: "child1"), + fixture.trace.startChild(operation: "child2"), + fixture.trace.startChild(operation: "child3") + ] + ) + + fixture.getSut(configureOptions: { options in + options.beforeSend = { _ in + return nil + } + }).capture(event: transaction) + + assertLostEventWithCountRecorded(category: .span, reason: .beforeSend, quantity: 4) + } + + func testRecordBeforeSendCorrectlyRecordsPartiallyDroppedSpans() { + let transaction = Transaction( + trace: fixture.trace, + children: [ + fixture.trace.startChild(operation: "child1"), + fixture.trace.startChild(operation: "child2"), + fixture.trace.startChild(operation: "child3") + ] + ) + + fixture.getSut(configureOptions: { options in + options.beforeSend = { event in + if let transaction = event as? Transaction { + transaction.spans = transaction.spans.filter { + $0.operation != "child2" + } + return transaction + } else { + return event + } + } + }).capture(event: transaction) + + // transaction has 3 span children and we dropped 1 of them + assertLostEventWithCountRecorded(category: .span, reason: .beforeSend, quantity: 1) + } + + func testCombinedPartiallyDroppedSpans() { + + SentryGlobalEventProcessor.shared().add { event in + if let transaction = event as? Transaction { + transaction.spans = transaction.spans.filter { + $0.operation != "child1" + } + return transaction + } else { + return event + } + } + + let transaction = Transaction( + trace: fixture.trace, + children: [ + fixture.trace.startChild(operation: "child1"), + fixture.trace.startChild(operation: "child2"), + fixture.trace.startChild(operation: "child3") + ] + ) + + fixture.getSut(configureOptions: { options in + options.beforeSend = { event in + if let transaction = event as? Transaction { + transaction.spans = transaction.spans.filter { + $0.operation != "child2" + } + return transaction + } else { + return event + } + } + options.beforeSendSpan = { span in + if span.operation == "child3" { + return nil + } else { + return span + } + } + }).capture(event: transaction) + + XCTAssertEqual(3, fixture.transport.recordLostEventsWithCount.count) + + // span dropped by event processor + XCTAssertEqual(fixture.transport.recordLostEventsWithCount.get(0)?.category, SentryDataCategory.span) + XCTAssertEqual(fixture.transport.recordLostEventsWithCount.get(0)?.reason, SentryDiscardReason.eventProcessor) + XCTAssertEqual(fixture.transport.recordLostEventsWithCount.get(0)?.quantity, 1) + + // span dropped by beforeSendSpan + XCTAssertEqual(fixture.transport.recordLostEventsWithCount.get(1)?.category, SentryDataCategory.span) + XCTAssertEqual(fixture.transport.recordLostEventsWithCount.get(1)?.reason, SentryDiscardReason.beforeSend) + XCTAssertEqual(fixture.transport.recordLostEventsWithCount.get(1)?.quantity, 1) + + // span dropped by beforeSend + XCTAssertEqual(fixture.transport.recordLostEventsWithCount.get(2)?.category, SentryDataCategory.span) + XCTAssertEqual(fixture.transport.recordLostEventsWithCount.get(2)?.reason, SentryDiscardReason.beforeSend) + XCTAssertEqual(fixture.transport.recordLostEventsWithCount.get(2)?.quantity, 1) + } func testNoDsn_UserFeedbackNotSent() { let sut = fixture.getSutWithNoDsn() @@ -1846,6 +2021,14 @@ private extension SentryClientTest { XCTAssertEqual(reason, lostEvent?.reason) } + private func assertLostEventWithCountRecorded(category: SentryDataCategory, reason: SentryDiscardReason, quantity: UInt) { + XCTAssertEqual(1, fixture.transport.recordLostEventsWithCount.count) + let lostEvent = fixture.transport.recordLostEventsWithCount.first + XCTAssertEqual(category, lostEvent?.category) + XCTAssertEqual(reason, lostEvent?.reason) + XCTAssertEqual(quantity, lostEvent?.quantity) + } + private enum TestError: Error { case invalidTest case testIsFailing diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index e34d409f7cf..a9b78a30fd3 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -385,6 +385,27 @@ class SentryHubTests: XCTestCase { XCTAssertEqual(.sampleRate, lostEvent?.reason) } + func testCaptureSampledTransaction_RecordsLostSpans() throws { + let transaction = sut.startTransaction(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation, sampled: .no)) + let trans = Dynamic(transaction).toTransaction().asAnyObject + + if let tracer = transaction as? SentryTracer { + (trans as? Transaction)?.spans = [ + tracer.startChild(operation: "child1"), + tracer.startChild(operation: "child2"), + tracer.startChild(operation: "child3") + ] + } + + sut.capture(try XCTUnwrap(trans as? Transaction), with: Scope()) + + XCTAssertEqual(1, fixture.client.recordLostEventsWithQauntity.count) + let lostEvent = fixture.client.recordLostEventsWithQauntity.first + XCTAssertEqual(.span, lostEvent?.category) + XCTAssertEqual(.sampleRate, lostEvent?.reason) + XCTAssertEqual(4, lostEvent?.quantity) + } + func testCaptureMessageWithScope() { fixture.getSut().capture(message: fixture.message, scope: fixture.scope)