diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index 427590ceb4..61f1ca4299 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -96,6 +96,7 @@ @implementation SentryTracer { dispatch_block_t _idleTimeoutBlock; NSMutableArray> *_children; BOOL _startTimeChanged; + BOOL _timeout; NSObject *_idleTimeoutLock; #if SENTRY_HAS_UIKIT @@ -148,6 +149,8 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti self.wasFinishCalled = NO; _measurements = [[NSMutableDictionary alloc] init]; self.finishStatus = kSentrySpanStatusUndefined; + self.finishMustBeCalled = NO; + _timeout = YES; if (_configuration.timerFactory == nil) { _configuration.timerFactory = [[SentryNSTimerFactory alloc] init]; @@ -289,6 +292,8 @@ - (void)startDeadlineTimer - (void)deadlineTimerFired { SENTRY_LOG_DEBUG(@"Sentry tracer deadline fired"); + _timeout = YES; + @synchronized(self) { // This try to minimize a race condition with a proper call to `finishInternal`. if (self.isFinished) { @@ -303,7 +308,8 @@ - (void)deadlineTimerFired } } - [self finishWithStatus:kSentrySpanStatusDeadlineExceeded]; + _finishStatus = kSentrySpanStatusDeadlineExceeded; + [self finishInternal]; } - (void)cancelDeadlineTimer @@ -581,6 +587,12 @@ - (void)finishInternal } }]; + if (self.finishMustBeCalled && !self.wasFinishCalled) { + SENTRY_LOG_DEBUG( + @"Not capturing transaction because finish was not called before timing out."); + return; + } + @synchronized(_children) { if (_configuration.idleTimeout > 0.0 && _children.count == 0) { SENTRY_LOG_DEBUG(@"Was waiting for timeout for UI event trace but it had no children, " diff --git a/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m b/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m index c7d051e15b..c8eeb1aa24 100644 --- a/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m +++ b/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m @@ -125,6 +125,12 @@ - (void)createTransaction:(UIViewController *)controller SENTRY_LOG_DEBUG(@"Started span with id %@ to track view controller %@.", spanId.sentrySpanIdString, name); + id vcSpan = [self.tracker getSpan:spanId]; + if ([vcSpan isKindOfClass:SentryTracer.class]) { + SentryTracer *vcTracer = (SentryTracer *)vcSpan; + vcTracer.finishMustBeCalled = YES; + } + // Use the target itself to store the spanId to avoid using a global mapper. objc_setAssociatedObject(controller, &SENTRY_UI_PERFORMANCE_TRACKER_SPAN_ID, spanId, OBJC_ASSOCIATION_RETAIN_NONATOMIC); diff --git a/Sources/Sentry/include/SentryTracer.h b/Sources/Sentry/include/SentryTracer.h index 33dfd76ccb..d2e40a4d72 100644 --- a/Sources/Sentry/include/SentryTracer.h +++ b/Sources/Sentry/include/SentryTracer.h @@ -43,6 +43,13 @@ static const NSTimeInterval SENTRY_AUTO_TRANSACTION_MAX_DURATION = 500.0; @property (nullable, nonatomic, copy) BOOL (^shouldIgnoreWaitForChildrenCallback)(id); +/** + * This flag indicates whether the trace should be captured when the timeout triggers. + * If Yes, this tracer will be discarced in case the timeout triggers. + * Default @c NO + */ +@property (nonatomic) BOOL finishMustBeCalled; + /** * All the spans that where created with this tracer but rootSpan. */ diff --git a/Tests/SentryTests/Transaction/SentryTracerTests.swift b/Tests/SentryTests/Transaction/SentryTracerTests.swift index 105c47712f..44f3808805 100644 --- a/Tests/SentryTests/Transaction/SentryTracerTests.swift +++ b/Tests/SentryTests/Transaction/SentryTracerTests.swift @@ -1292,6 +1292,15 @@ class SentryTracerTests: XCTestCase { } #endif + func testFinishShouldBeCalled_Timeout_NotCaptured() { + fixture.dispatchQueue.blockBeforeMainBlock = { true } + + let sut = fixture.getSut() + sut.finishMustBeCalled = true + fixture.timerFactory.fire() + assertTransactionNotCaptured(sut) + } + @available(*, deprecated) func testSetExtra_ForwardsToSetData() { let sut = fixture.getSut()