diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d1973402e..0c80fb1dc4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Enhance the UIViewController breadcrumbs with more data (#1945) - feat: Add extra app start span (#1952) - Add enableAutoBreadcrumbTracking option (#1958) +- Automatic nest spans with the UI life cycle (#1959) - Upload frame rendering timestamps to correlate to sampled backtraces (#1910) ### Fixes diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift index 099a64601f3..6bbd1f0cb46 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift @@ -19,5 +19,4 @@ class LoremIpsumViewController: UIViewController { } } } - } diff --git a/Sources/Sentry/SentryPerformanceTracker.m b/Sources/Sentry/SentryPerformanceTracker.m index f7a8e37eab2..2ddd4218f4e 100644 --- a/Sources/Sentry/SentryPerformanceTracker.m +++ b/Sources/Sentry/SentryPerformanceTracker.m @@ -13,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN @interface -SentryPerformanceTracker () +SentryPerformanceTracker () @property (nonatomic, strong) NSMutableDictionary> *spans; @property (nonatomic, strong) NSMutableArray> *activeSpanStack; @@ -67,6 +67,10 @@ - (SentrySpanId *)startSpanWithName:(NSString *)name operation:(NSString *)opera bindToScope:bindToScope waitForChildren:YES customSamplingContext:@ {}]; + + if ([newSpan isKindOfClass:[SentryTracer class]]) { + [(SentryTracer *)newSpan setDelegate:self]; + } }]; } @@ -179,6 +183,13 @@ - (BOOL)isSpanAlive:(SentrySpanId *)spanId } } +- (nullable id)activeSpanForTracer:(SentryTracer *)tracer +{ + @synchronized(self.activeSpanStack) { + return [self.activeSpanStack lastObject]; + } +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index 31e3ab526be..5f549941bf6 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -38,7 +38,6 @@ SentryTracer () @property (nonatomic, strong) SentrySpan *rootSpan; -@property (nonatomic, strong) NSMutableArray> *children; @property (nonatomic, strong) SentryHub *hub; @property (nonatomic) SentrySpanStatus finishStatus; @property (nonatomic) BOOL isWaitingForChildren; @@ -54,6 +53,7 @@ @implementation SentryTracer { NSMutableDictionary *_tags; NSMutableDictionary *_data; dispatch_block_t _idleTimeoutBlock; + NSMutableArray> *_children; #if SENTRY_HAS_UIKIT BOOL _startTimeChanged; @@ -122,7 +122,7 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti if (self = [super init]) { self.rootSpan = [[SentrySpan alloc] initWithTransaction:self context:transactionContext]; self.name = transactionContext.name; - self.children = [[NSMutableArray alloc] init]; + _children = [[NSMutableArray alloc] init]; self.hub = hub; self.isWaitingForChildren = NO; _waitForChildren = waitForChildren; @@ -200,15 +200,33 @@ - (void)cancelIdleTimeout } } +- (id)getActiveSpan +{ + id span; + + if (self.delegate) { + @synchronized(_children) { + span = [self.delegate activeSpanForTracer:self]; + if (span == nil || span == self || ![_children containsObject:span]) { + span = _rootSpan; + } + } + } else { + span = _rootSpan; + } + + return span; +} + - (id)startChildWithOperation:(NSString *)operation { - return [_rootSpan startChildWithOperation:operation]; + return [[self getActiveSpan] startChildWithOperation:operation]; } - (id)startChildWithOperation:(NSString *)operation description:(nullable NSString *)description { - return [_rootSpan startChildWithOperation:operation description:description]; + return [[self getActiveSpan] startChildWithOperation:operation description:description]; } - (id)startChildWithParentId:(SentrySpanId *)parentId @@ -226,7 +244,7 @@ - (void)cancelIdleTimeout context.spanDescription = description; SentrySpan *child = [[SentrySpan alloc] initWithTransaction:self context:context]; - @synchronized(self.children) { + @synchronized(_children) { [_children addObject:child]; } @@ -304,6 +322,11 @@ - (BOOL)isFinished return self.rootSpan.isFinished; } +- (NSArray> *)children +{ + return [_children copy]; +} + - (void)setDataValue:(nullable id)value forKey:(NSString *)key { @synchronized(_data) { diff --git a/Sources/Sentry/include/SentryTracer.h b/Sources/Sentry/include/SentryTracer.h index b0d69c4478e..cb6735e0733 100644 --- a/Sources/Sentry/include/SentryTracer.h +++ b/Sources/Sentry/include/SentryTracer.h @@ -4,10 +4,20 @@ NS_ASSUME_NONNULL_BEGIN @class SentryHub, SentryTransactionContext, SentryTraceHeader, SentryTraceContext, - SentryDispatchQueueWrapper; + SentryDispatchQueueWrapper, SentryTracer; static NSTimeInterval const SentryTracerDefaultTimeout = 3.0; +@protocol SentryTracerDelegate + +/** + * Return the active span of given tracer. + * This function is used to determine which span will be used to create a new child. + */ +- (nullable id)activeSpanForTracer:(SentryTracer *)tracer; + +@end + @interface SentryTracer : NSObject /** @@ -64,6 +74,11 @@ static NSTimeInterval const SentryTracerDefaultTimeout = 3.0; */ @property (nonatomic, readonly) NSArray> *children; +/* + * A delegate that provides extra information for the transaction. + */ +@property (nullable, nonatomic, weak) id delegate; + /** * Init a SentryTracer with given transaction context and hub and set other fields by default * diff --git a/Tests/SentryTests/Performance/SentryTracerTests.swift b/Tests/SentryTests/Performance/SentryTracerTests.swift index efacab0d9b0..76c58472018 100644 --- a/Tests/SentryTests/Performance/SentryTracerTests.swift +++ b/Tests/SentryTests/Performance/SentryTracerTests.swift @@ -2,6 +2,15 @@ import XCTest class SentryTracerTests: XCTestCase { + private class TracerDelegate: SentryTracerDelegate { + + var activeSpan: Span? + + func activeSpan(for tracer: SentryTracer) -> Span? { + return activeSpan + } + } + private class Fixture { let client: TestClient let hub: TestHub @@ -372,6 +381,53 @@ class SentryTracerTests: XCTestCase { assertAppStartsSpanAdded(transaction: transaction, startType: "Cold Start", operation: fixture.appStartColdOperation, appStartMeasurement: appStartMeasurement) } + func test_startChildWithDelegate() { + let delegate = TracerDelegate() + + let sut = fixture.getSut() + sut.delegate = delegate + + let child = sut.startChild(operation: fixture.transactionOperation) + + delegate.activeSpan = child + + let secondChild = sut.startChild(operation: fixture.transactionOperation) + + XCTAssertEqual(secondChild.context.parentSpanId, child.context.spanId) + } + + func test_startChildWithDelegate_ActiveNotChild() { + let delegate = TracerDelegate() + + let sut = fixture.getSut() + sut.delegate = delegate + + delegate.activeSpan = SentryTracer(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation), hub: nil) + + let child = sut.startChild(operation: fixture.transactionOperation) + + let secondChild = sut.startChild(operation: fixture.transactionOperation) + + XCTAssertEqual(secondChild.context.parentSpanId, sut.context.spanId) + XCTAssertEqual(secondChild.context.parentSpanId, child.context.parentSpanId) + } + + func test_startChildWithDelegate_SelfIsActive() { + let delegate = TracerDelegate() + + let sut = fixture.getSut() + sut.delegate = delegate + + delegate.activeSpan = sut + + let child = sut.startChild(operation: fixture.transactionOperation) + + let secondChild = sut.startChild(operation: fixture.transactionOperation) + + XCTAssertEqual(secondChild.context.parentSpanId, sut.context.spanId) + XCTAssertEqual(secondChild.context.parentSpanId, child.context.parentSpanId) + } + func testAddWarmAppStartMeasurement_PutOnNextAutoUITransaction() { let appStartMeasurement = fixture.getAppStartMeasurement(type: .warm) SentrySDK.setAppStartMeasurement(appStartMeasurement)