Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a43e535
ref: Extract getAppStartMeasurement into SentryAppStartMeasurementPro…
philipphofmann Mar 9, 2026
2d02876
ref: Remove resetAppStartMeasurementRead from SentryTracer
philipphofmann Mar 9, 2026
ba85ba9
ref: Clean up app start measurement provider API
philipphofmann Mar 10, 2026
ec54453
fix: Remove leftover profilerReferenceID in concurrency test
philipphofmann Mar 10, 2026
4d7bfa8
fix: Fix compilation errors in app start measurement provider tests
philipphofmann Mar 10, 2026
87017a5
feat: Add enableStandaloneAppStartTracing option
philipphofmann Mar 5, 2026
b31cbf5
feat: Implement standalone app start transaction
philipphofmann Mar 5, 2026
2cad1fb
ref: Pass standalone flag to sentryBuildAppStartSpans
philipphofmann Mar 5, 2026
244ad48
ref: Extract isStandaloneAppStartTransaction method
philipphofmann Mar 5, 2026
6427ad5
ref: Check origin in isStandaloneAppStartTransaction
philipphofmann Mar 5, 2026
683fc13
ref: Move standalone app start check to Swift helper
philipphofmann Mar 5, 2026
81c03f0
ref: Use SentrySpan protocol in app start spans
philipphofmann Mar 5, 2026
51dc82b
fix linter
philipphofmann Mar 5, 2026
d4918cb
test: Add standalone app start span building tests
philipphofmann Mar 9, 2026
db3ef7e
feat: Guard standalone app start transaction on SDK enabled
philipphofmann Mar 9, 2026
8211d93
ref: Pass app start measurement via tracer configuration
philipphofmann Mar 9, 2026
89686ca
test: Assert span names and operations in standalone test
philipphofmann Mar 9, 2026
985fdd0
test: Improve standalone app start global measurement test
philipphofmann Mar 9, 2026
78e9ab3
fix: Mark app start measurement as read in standalone path
philipphofmann Mar 9, 2026
df35dee
feat: Add changelog entry for standalone app start tracing
philipphofmann Mar 10, 2026
e0b4ea4
ref: Clarify markAsRead comment as safeguard
philipphofmann Mar 10, 2026
8106a64
fix: Remove trailing commas unsupported by CI Swift version
philipphofmann Mar 10, 2026
661d309
ref: Move SDK enabled guard to top of handle method
philipphofmann Mar 10, 2026
462b028
ref: Log error for unknown app start type
philipphofmann Mar 10, 2026
0c44cc8
ref: Reference SentryAppStartMeasurementProvider in comment
philipphofmann Mar 10, 2026
868d726
ref: Log error for unknown app start type in span builder
philipphofmann Mar 10, 2026
a0c07a6
ref: Simplify unknown app start type log message
philipphofmann Mar 10, 2026
fc55e12
ref: Simplify unknown app start type log in Swift
philipphofmann Mar 10, 2026
a1e88d1
ref: Extract AppStartMeasurementHandler into own file
philipphofmann Mar 10, 2026
529e78a
test: Add tests for AppStartMeasurementHandler
philipphofmann Mar 10, 2026
ed652f5
ref: Split sentryBuildAppStartSpans into two methods
philipphofmann Mar 10, 2026
2799506
test: Add integration tests for standalone app start
philipphofmann Mar 10, 2026
6a7e677
test: Add missing import for _SentryPrivate
philipphofmann Mar 10, 2026
8ba3510
test: Replace clearTestState with targeted cleanup
philipphofmann Mar 10, 2026
5106df6
test: Add span validation for standalone app start
philipphofmann Mar 10, 2026
58d04f0
fix: Fix flaky tests by restoring clearTestState and using serialized…
philipphofmann Mar 10, 2026
d11bf49
feat: Track standalone app start tracing in enabled features
philipphofmann Mar 10, 2026
c7d58ca
ref: address PR review comments
philipphofmann Mar 10, 2026
15b9564
fix: Add visionOS to platform guards in clearTestState
philipphofmann Mar 10, 2026
027eef2
build: update public API after adding experimental option
philipphofmann Mar 10, 2026
4740d03
Merge branch 'main' into feat/standalone-app-start-tracing
philipphofmann Mar 12, 2026
98fbbed
changelog
philipphofmann Mar 12, 2026
8b3d6f2
ref: Remove forward declarations in SentryBuildAppStartSpans
philipphofmann Mar 12, 2026
4819275
ref: Move isStandaloneAppStartTransaction to UIKit section
philipphofmann Mar 12, 2026
d0d815e
ref: Rename AppStartMeasurementHandler to AppStartReportingStrategy
philipphofmann Mar 12, 2026
f525749
fix: Address review feedback for standalone app start tracing
philipphofmann Mar 12, 2026
9d19a2a
test: Use targeted cleanup instead of blanket clearTestState
philipphofmann Mar 12, 2026
973cc2f
test: Fix test conventions from develop-docs review
philipphofmann Mar 12, 2026
3ac8825
test: Add integration test for standalone app start tracing
philipphofmann Mar 12, 2026
ade227c
test: Add coverage for standalone app start edge cases
philipphofmann Mar 12, 2026
a243fa7
fix: Remove standalone tracing from sample, fix endif comment
philipphofmann Mar 12, 2026
00e0d76
Merge branch 'main' into feat/standalone-app-start-tracing
philipphofmann Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Add standalone app start tracing as an experimental option (#7660), enable it via `options.experimental.enableStandaloneAppStartTracing = true`

## 9.7.0

### Features
Expand Down
7 changes: 7 additions & 0 deletions Sources/Sentry/SentryAppStartMeasurementProvider.m
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ + (nullable SentryAppStartMeasurement *)appStartMeasurementForOperation:(NSStrin
return measurement;
}

+ (void)markAsRead
{
@synchronized(appStartMeasurementLock) {
appStartMeasurementRead = YES;
}
}

+ (void)reset
{
@synchronized(appStartMeasurementLock) {
Expand Down
66 changes: 49 additions & 17 deletions Sources/Sentry/SentryBuildAppStartSpans.m
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
#import "SentryAppStartMeasurement.h"
#import "SentryLogC.h"
#import "SentrySpanContext+Private.h"
#import "SentrySpanId.h"
#import "SentrySpanInternal.h"
#import "SentrySpanOperation.h"
#import "SentrySwift.h"
#import "SentryTraceOrigin.h"
#import "SentryTracer.h"
#import <SentryBuildAppStartSpans.h>

#if SENTRY_HAS_UIKIT

id<SentrySpan>
# pragma mark - Private

static id<SentrySpan>
sentryBuildAppStartSpan(
SentryTracer *tracer, SentrySpanId *parentId, NSString *operation, NSString *description)
{
Expand All @@ -22,14 +26,19 @@
origin:SentryTraceOriginAutoAppStart
sampled:tracer.sampled];

// Pass nil for the framesTracker because app start spans are created during launch,
// before the frames tracker is available.
return [[SentrySpanInternal alloc] initWithTracer:tracer context:context framesTracker:nil];
}

NSArray<id<SentrySpan>> *
sentryBuildAppStartSpans(
SentryTracer *tracer, SentryAppStartMeasurement *_Nullable appStartMeasurement)
/**
* Internal helper that builds the app start child spans. When @c isStandalone is @c YES the
* intermediate grouping span is omitted and children are parented directly to the tracer.
*/
static NSArray<id<SentrySpan>> *
sentryBuildAppStartSpansInternal(SentryTracer *tracer,
SentryAppStartMeasurement *_Nullable appStartMeasurement, BOOL isStandalone)
{

if (appStartMeasurement == nil) {
return @[];
}
Expand All @@ -39,14 +48,15 @@

switch (appStartMeasurement.type) {
case SentryAppStartTypeCold:
operation = @"app.start.cold";
operation = SentrySpanOperationAppStartCold;
type = @"Cold Start";
break;
case SentryAppStartTypeWarm:
operation = @"app.start.warm";
operation = SentrySpanOperationAppStartWarm;
type = @"Warm Start";
break;
default:
SENTRY_LOG_ERROR(@"Unknown app start type, can't build app start spans");
return @[];
}

Expand All @@ -55,45 +65,67 @@
NSDate *appStartEndTimestamp = [appStartMeasurement.appStartTimestamp
dateByAddingTimeInterval:appStartMeasurement.duration];

id<SentrySpan> appStartSpan = sentryBuildAppStartSpan(tracer, tracer.spanId, operation, type);
[appStartSpan setStartTimestamp:appStartMeasurement.appStartTimestamp];
[appStartSpan setTimestamp:appStartEndTimestamp];

[appStartSpans addObject:appStartSpan];
SentrySpanId *appStartSpanParentId;
if (isStandalone) {
appStartSpanParentId = tracer.spanId;
} else {
id<SentrySpan> appStartSpan
= sentryBuildAppStartSpan(tracer, tracer.spanId, operation, type);
[appStartSpan setStartTimestamp:appStartMeasurement.appStartTimestamp];
[appStartSpan setTimestamp:appStartEndTimestamp];
[appStartSpans addObject:appStartSpan];
appStartSpanParentId = appStartSpan.spanId;
}

if (!appStartMeasurement.isPreWarmed) {
id<SentrySpan> premainSpan
= sentryBuildAppStartSpan(tracer, appStartSpan.spanId, operation, @"Pre Runtime Init");
= sentryBuildAppStartSpan(tracer, appStartSpanParentId, operation, @"Pre Runtime Init");
[premainSpan setStartTimestamp:appStartMeasurement.appStartTimestamp];
[premainSpan setTimestamp:appStartMeasurement.runtimeInitTimestamp];
[appStartSpans addObject:premainSpan];

id<SentrySpan> runtimeInitSpan = sentryBuildAppStartSpan(
tracer, appStartSpan.spanId, operation, @"Runtime Init to Pre Main Initializers");
tracer, appStartSpanParentId, operation, @"Runtime Init to Pre Main Initializers");
[runtimeInitSpan setStartTimestamp:appStartMeasurement.runtimeInitTimestamp];
[runtimeInitSpan setTimestamp:appStartMeasurement.moduleInitializationTimestamp];
[appStartSpans addObject:runtimeInitSpan];
}

id<SentrySpan> appInitSpan
= sentryBuildAppStartSpan(tracer, appStartSpan.spanId, operation, @"UIKit Init");
= sentryBuildAppStartSpan(tracer, appStartSpanParentId, operation, @"UIKit Init");
[appInitSpan setStartTimestamp:appStartMeasurement.moduleInitializationTimestamp];
[appInitSpan setTimestamp:appStartMeasurement.sdkStartTimestamp];
[appStartSpans addObject:appInitSpan];

id<SentrySpan> didFinishLaunching
= sentryBuildAppStartSpan(tracer, appStartSpan.spanId, operation, @"Application Init");
= sentryBuildAppStartSpan(tracer, appStartSpanParentId, operation, @"Application Init");
[didFinishLaunching setStartTimestamp:appStartMeasurement.sdkStartTimestamp];
[didFinishLaunching setTimestamp:appStartMeasurement.didFinishLaunchingTimestamp];
[appStartSpans addObject:didFinishLaunching];

id<SentrySpan> frameRenderSpan
= sentryBuildAppStartSpan(tracer, appStartSpan.spanId, operation, @"Initial Frame Render");
= sentryBuildAppStartSpan(tracer, appStartSpanParentId, operation, @"Initial Frame Render");
[frameRenderSpan setStartTimestamp:appStartMeasurement.didFinishLaunchingTimestamp];
[frameRenderSpan setTimestamp:appStartEndTimestamp];
[appStartSpans addObject:frameRenderSpan];

return appStartSpans;
}

# pragma mark - Public

NSArray<id<SentrySpan>> *
sentryBuildAppStartSpans(
SentryTracer *tracer, SentryAppStartMeasurement *_Nullable appStartMeasurement)
{
return sentryBuildAppStartSpansInternal(tracer, appStartMeasurement, NO);
}

NSArray<id<SentrySpan>> *
sentryBuildStandaloneAppStartSpans(
SentryTracer *tracer, SentryAppStartMeasurement *_Nullable appStartMeasurement)
{
return sentryBuildAppStartSpansInternal(tracer, appStartMeasurement, YES);
}

#endif // SENTRY_HAS_UIKIT
3 changes: 3 additions & 0 deletions Sources/Sentry/SentrySpanOperation.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

NSString *const SentrySpanOperationUiAction = @"ui.action";
NSString *const SentrySpanOperationUiActionClick = @"ui.action.click";
NSString *const SentrySpanOperationAppStartCold = @"app.start.cold";
NSString *const SentrySpanOperationAppStartWarm = @"app.start.warm";

NSString *const SentrySpanOperationUiLoad = @"ui.load";
NSString *const SentrySpanOperationUiLoadInitialDisplay = @"ui.load.initial_display";
NSString *const SentrySpanOperationUiLoadFullDisplay = @"ui.load.full_display";
30 changes: 26 additions & 4 deletions Sources/Sentry/SentryTracer.m
Original file line number Diff line number Diff line change
Expand Up @@ -596,9 +596,22 @@ - (BOOL)finishTracer:(SentrySpanStatus)unfinishedSpansFinishStatus shouldCleanUp
[super finishWithStatus:_finishStatus];
}
#if SENTRY_HAS_UIKIT
appStartMeasurement =
[SentryAppStartMeasurementProvider appStartMeasurementForOperation:self.operation
startTimestamp:self.startTimestamp];
// Standalone app start transactions carry their measurement directly in the configuration,
// bypassing the global static in SentryAppStartMeasurementProvider. The main advantage is
// avoiding a race condition between the app start tracker producing the measurement and
// the first UIViewController transaction consuming it. We don't change the existing
// UIViewController/AppStart path below because it's bulletproof and we'll likely remove
// it once standalone app start tracing is stable.
if ([self isStandaloneAppStartTransaction] && _configuration.appStartMeasurement != nil) {
appStartMeasurement = _configuration.appStartMeasurement;
// Safeguard: this shouldn't normally happen, but mark as read so no UIViewController
// transaction picks up the global static too.
[SentryAppStartMeasurementProvider markAsRead];
} else {
appStartMeasurement =
[SentryAppStartMeasurementProvider appStartMeasurementForOperation:self.operation
startTimestamp:self.startTimestamp];
}

if (appStartMeasurement != nil) {
[self updateStartTime:appStartMeasurement.appStartTimestamp];
Expand Down Expand Up @@ -706,7 +719,9 @@ - (SentryTransaction *)toTransaction
#if SENTRY_HAS_UIKIT
[self addFrameStatistics];

NSArray<id<SentrySpan>> *appStartSpans = sentryBuildAppStartSpans(self, appStartMeasurement);
NSArray<id<SentrySpan>> *appStartSpans = [self isStandaloneAppStartTransaction]
? sentryBuildStandaloneAppStartSpans(self, appStartMeasurement)
: sentryBuildAppStartSpans(self, appStartMeasurement);
capacity = _children.count + appStartSpans.count;
#else
capacity = _children.count;
Expand Down Expand Up @@ -758,6 +773,13 @@ - (SentryTransaction *)toTransaction

#if SENTRY_HAS_UIKIT

- (BOOL)isStandaloneAppStartTransaction
{
return [StandaloneAppStartTransactionHelper
isStandaloneAppStartTransactionWithOperation:self.operation
origin:self.origin];
}

- (void)addAppStartMeasurements:(SentryTransaction *)transaction
{
if (appStartMeasurement != nil && appStartMeasurement.type != SentryAppStartTypeUnknown) {
Expand Down
7 changes: 7 additions & 0 deletions Sources/Sentry/include/SentryAppStartMeasurementProvider.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ NS_ASSUME_NONNULL_BEGIN
startTimestamp:
(nullable NSDate *)startTimestamp;

/**
* Marks the app start measurement as read so subsequent calls to
* @c appStartMeasurementForOperation:startTimestamp: return @c nil.
* Used by standalone app start transactions that carry their own measurement.
*/
+ (void)markAsRead;

/**
* Internal. Only needed for testing.
*/
Expand Down
30 changes: 30 additions & 0 deletions Sources/Sentry/include/SentryBuildAppStartSpans.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,39 @@ NS_ASSUME_NONNULL_BEGIN

#if SENTRY_HAS_UIKIT

/**
* Builds app start spans for a UIViewController transaction. An intermediate grouping span
* ("Cold Start" / "Warm Start") is inserted as parent for the phase spans:
*
* @code
* UIViewController (op: ui.load) ← tracer
* └─ Cold Start (op: app.start.cold) ← grouping span
* β”œβ”€ Pre Runtime Init
* β”œβ”€ Runtime Init to Pre Main Initializers
* β”œβ”€ UIKit Init
* β”œβ”€ Application Init
* └─ Initial Frame Render
* @endcode
*/
NSArray<id<SentrySpan>> *sentryBuildAppStartSpans(
SentryTracer *tracer, SentryAppStartMeasurement *_Nullable appStartMeasurement);

/**
* Builds app start spans for a standalone app start transaction. Phase spans are parented
* directly to the tracer (no intermediate grouping span):
*
* @code
* App Start Cold (op: app.start.cold) ← tracer
* β”œβ”€ Pre Runtime Init
* β”œβ”€ Runtime Init to Pre Main Initializers
* β”œβ”€ UIKit Init
* β”œβ”€ Application Init
* └─ Initial Frame Render
* @endcode
*/
NSArray<id<SentrySpan>> *sentryBuildStandaloneAppStartSpans(
SentryTracer *tracer, SentryAppStartMeasurement *_Nullable appStartMeasurement);

#endif // SENTRY_HAS_UIKIT

NS_ASSUME_NONNULL_END
1 change: 1 addition & 0 deletions Sources/Sentry/include/SentryPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
#import "SentryTraceHeader.h"
#import "SentryTraceOrigin.h"
#import "SentryTraceProfiler.h"
#import "SentryTracerConfiguration.h"
#import "SentryUIViewControllerSwizzlingHelper.h"
#import "SentryUncaughtNSExceptions.h"
#import "SentryWatchdogTerminationBreadcrumbProcessor.h"
3 changes: 3 additions & 0 deletions Sources/Sentry/include/SentrySpanOperation.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ SENTRY_EXTERN NSString *const SentrySpanOperationNetworkRequestOperation;
SENTRY_EXTERN NSString *const SentrySpanOperationUiAction;
SENTRY_EXTERN NSString *const SentrySpanOperationUiActionClick;

SENTRY_EXTERN NSString *const SentrySpanOperationAppStartCold;
SENTRY_EXTERN NSString *const SentrySpanOperationAppStartWarm;

SENTRY_EXTERN NSString *const SentrySpanOperationUiLoad;

SENTRY_EXTERN NSString *const SentrySpanOperationUiLoadInitialDisplay;
Expand Down
12 changes: 12 additions & 0 deletions Sources/Sentry/include/SentryTracerConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

NS_ASSUME_NONNULL_BEGIN

@class SentryAppStartMeasurement;
@class SentryDispatchQueueWrapper;
@class SentryProfileOptions;
@class SentrySamplerDecision;
Expand Down Expand Up @@ -52,6 +53,17 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic) NSTimeInterval idleTimeout;

#if SENTRY_HAS_UIKIT
/**
* The app start measurement to attach to this tracer.
* When set, the tracer uses this instead of reading from the global static in
* @c SentryAppStartMeasurementProvider.
*
* Default is nil.
*/
@property (nonatomic, strong, nullable) SentryAppStartMeasurement *appStartMeasurement;
#endif // SENTRY_HAS_UIKIT

+ (SentryTracerConfiguration *)configurationWithBlock:
(void (^)(SentryTracerConfiguration *configuration))block;

Expand Down
3 changes: 3 additions & 0 deletions Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ import Foundation
if options.experimental.enableMetrics {
features.append("metrics")
}
if options.experimental.enableStandaloneAppStartTracing {
features.append("standaloneAppStartTracing")
}

return features
}
Expand Down
Loading
Loading