feat: Standalone app start tracing#7660
Conversation
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Internal Changes 🔧Samples
Other
🤖 This preview updates automatically when you update the PR. |
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7660 +/- ##
=============================================
- Coverage 86.192% 86.188% -0.005%
=============================================
Files 487 488 +1
Lines 29651 29793 +142
Branches 12894 12950 +56
=============================================
+ Hits 25557 25678 +121
- Misses 4046 4065 +19
- Partials 48 50 +2
... and 6 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 29d546e | 1224.06 ms | 1257.05 ms | 32.98 ms |
| e56c394 | 1229.37 ms | 1245.86 ms | 16.49 ms |
| a7c42d9 | 1217.25 ms | 1253.98 ms | 36.73 ms |
| 787537a | 1218.35 ms | 1251.72 ms | 33.38 ms |
| 7814719 | 1220.98 ms | 1254.14 ms | 33.16 ms |
| 1155f0c | 1222.41 ms | 1251.18 ms | 28.76 ms |
| 6b08499 | 1231.61 ms | 1241.90 ms | 10.29 ms |
| 9b154ad | 1218.94 ms | 1253.60 ms | 34.66 ms |
| bdd8e0e | 1233.35 ms | 1266.96 ms | 33.60 ms |
| 670aba7 | 1219.98 ms | 1257.20 ms | 37.23 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 29d546e | 24.14 KiB | 1.15 MiB | 1.13 MiB |
| e56c394 | 24.14 KiB | 1.16 MiB | 1.13 MiB |
| a7c42d9 | 24.14 KiB | 1.15 MiB | 1.13 MiB |
| 787537a | 24.14 KiB | 1.15 MiB | 1.12 MiB |
| 7814719 | 24.14 KiB | 1.15 MiB | 1.13 MiB |
| 1155f0c | 24.14 KiB | 1.16 MiB | 1.13 MiB |
| 6b08499 | 24.14 KiB | 1.15 MiB | 1.13 MiB |
| 9b154ad | 24.14 KiB | 1.16 MiB | 1.13 MiB |
| bdd8e0e | 24.14 KiB | 1.16 MiB | 1.13 MiB |
| 670aba7 | 24.14 KiB | 1.16 MiB | 1.13 MiB |
Previous results on branch: feat/standalone-app-start-tracing
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 8b737d8 | 1222.43 ms | 1252.96 ms | 30.53 ms |
| c1f77f9 | 1220.31 ms | 1242.54 ms | 22.23 ms |
| 0d2ca60 | 1229.17 ms | 1261.09 ms | 31.91 ms |
| 50bee10 | 1228.36 ms | 1267.13 ms | 38.77 ms |
| edbdb50 | 1217.06 ms | 1249.92 ms | 32.86 ms |
| 92fb0d9 | 1198.41 ms | 1215.74 ms | 17.33 ms |
| 9175efb | 1219.83 ms | 1239.50 ms | 19.67 ms |
| e03b996 | 1222.64 ms | 1254.96 ms | 32.32 ms |
| 89b00b8 | 1218.33 ms | 1255.14 ms | 36.81 ms |
| ad22b9d | 1214.31 ms | 1243.60 ms | 29.30 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 8b737d8 | 24.14 KiB | 1.12 MiB | 1.10 MiB |
| c1f77f9 | 24.14 KiB | 1.13 MiB | 1.10 MiB |
| 0d2ca60 | 24.14 KiB | 1.12 MiB | 1.10 MiB |
| 50bee10 | 24.14 KiB | 1.16 MiB | 1.13 MiB |
| edbdb50 | 24.14 KiB | 1.12 MiB | 1.10 MiB |
| 92fb0d9 | 24.14 KiB | 1.13 MiB | 1.10 MiB |
| 9175efb | 24.14 KiB | 1.12 MiB | 1.10 MiB |
| e03b996 | 24.14 KiB | 1.16 MiB | 1.14 MiB |
| 89b00b8 | 24.14 KiB | 1.16 MiB | 1.13 MiB |
| ad22b9d | 24.14 KiB | 1.13 MiB | 1.10 MiB |
philipphofmann
left a comment
There was a problem hiding this comment.
Review for Claude Code.
Sentry Build Distribution
|
Sentry Build Distribution
|
📲 Install BuildsiOS
|
Sentry Build Distribution
|
…vider Move the app start measurement retrieval logic from SentryTracer into a standalone SentryAppStartMeasurementProvider class to decouple it from the tracer and prepare for a future Swift rewrite. Agent transcript: https://claudescope.sentry.dev/share/-ySeYAQr0t_-TzVJYh7grS4nvSpo6f1NKAIEUc4__60
Call SentryAppStartMeasurementProvider.reset() directly from ClearTestState and the provider tests instead of routing through SentryTracer's forwarding method. Agent transcript: https://claudescope.sentry.dev/share/L1InLC1w4Xm6HgEp2bHk-sJ2wtr9fRfQ08BpwGE4JSs
Remove unused profilerReferenceID parameter, add header docs, and fix stale TSan suppression. Agent transcript: https://claudescope.sentry.dev/share/YCbJaiw4eo47tyRGdPdNfyWa3VZueptJtPoevH1S97A
Fix trailing closure ambiguity with DispatchWorkItem and remove non-existent profilerReferenceID parameter. Agent transcript: https://claudescope.sentry.dev/share/boilLz21_S-f8uu_dcXvext-dAi2MAY8xkbK2zEafW4
Add experimental option to send standalone app start transactions instead of attaching app start data to the first UIViewController transaction. Uses a strategy pattern via AppStartMeasurementHandler protocol. The standalone handler is currently a no-op placeholder; actual transaction logic will be added in a follow-up. Refs: #6883 Agent transcript: https://claudescope.sentry.dev/share/KZR39vRrsVDqgpm56ONILZFlu1u_gpBhAnK7QrpyOH0
Create a real tracer with app.start.cold/warm operation that reuses the existing tracer pipeline for span building, measurements, context, debug images, and profiling. - Add SentrySpanOperationAppStartCold/Warm constants - Relax getAppStartMeasurement to accept app start ops - Skip intermediate root span for standalone transactions - Enable option in iOS sample app for validation Refs: #6883 Agent transcript: https://claudescope.sentry.dev/share/UaCuhStnxjQCwKH46hOsPqgeHsckPh15A0Flqw8JOPc
Move the standalone app start detection from SentryBuildAppStartSpans to SentryTracer and pass it as a BOOL parameter. Use span operation constants instead of string literals. Agent transcript: https://claudescope.sentry.dev/share/QyuEyImC81GP83tCrqUbp939ejQ3-3y_0TcwnvcejsA
Deduplicate the app start operation check in SentryTracer into a single method reused in toTransaction and getAppStartMeasurement. Agent transcript: https://claudescope.sentry.dev/share/bffTUQ5TAcp6oTyLqR8SSSjLLG4pSUsZ8l7TB7qaq1s
Also verify the trace origin is auto.app_start to more precisely identify standalone app start transactions. Agent transcript: https://claudescope.sentry.dev/share/HhoCIcB8Nh0dEd16RtFdllS_ZIxK393KCEPaOUiPp5s
Add StandaloneAppStartTransactionHelper to centralize the logic for identifying standalone app start transactions next to the code that creates them. Agent transcript: https://claudescope.sentry.dev/share/th338Y1YxxM5471m3BNGV5Az6QKNTNQzAyhCyZ9MmuM
Avoids race conditions with global static by passing the measurement directly to the tracer. Agent transcript: https://claudescope.sentry.dev/share/5dFraoe7qfSofcTRtjfi59JzjPmHtWPyBK9YLzp61_g
Cover nil config measurement, nil measurement input, unknown start type, enhanced integration assertions, and default feature flag behavior. Agent transcript: https://claudescope.sentry.dev/share/DHthmP4l-WXLLOSec8mR7LkoGRxh5KdqGZSeF3UaXik
Remove enableStandaloneAppStartTracing from sample app since the feature isn't ready for general use yet. Restore visionOS in #endif comment to match the opening platform guard. Agent transcript: https://claudescope.sentry.dev/share/vz0QO06DfJpC3-St_cH3Br5qoqAuE2iHNNDFfK-M7uQ
This comment was marked as outdated.
This comment was marked as outdated.
|
|
||
| NSString *const SentrySpanOperationUiAction = @"ui.action"; | ||
| NSString *const SentrySpanOperationUiActionClick = @"ui.action.click"; | ||
| NSString *const SentrySpanOperationAppStartCold = @"app.start.cold"; |
There was a problem hiding this comment.
we've made a decision with @noahsmartin that we want to switch the span operation to just app.start, and make the distinction of the type by introducing these new attributes (as mentioned here):
app.vitals.start.type — app start type (cold/warm)
app.vitals.start.value - app start value in ms
this would make it cleaner as well as allow us to sample this specific spans/transactions in a custom TracesSampler callback without introducing a new sample rate.
There was a problem hiding this comment.
side note: for now we will additionally send app.vitals.start.cold/warm.value attributes on top of the app.vitals.start.value and app.vitals.start.type combination (getsentry/sentry-conventions#318).
This is mainly due to migrations because relay needs to manually backfill those attributes and existing transactions dont have them (see PR). So the mobile vitals dashboard will work with app.vitals.start.cold/warm.value for now. After ~90 days (which should be around end of July) we can fully migrate to app.vitals.start.value and app.vitals.start.type.
Depending on when this PR finishes we might not need the support for app.vitals.start.cold/warm.value :)
…andalone-app-start-tracing
…andalone-app-start-tracing
…andalone-app-start-tracing
The assertion checked "app.vitals.start.warm.value" which is a span data key only set in standalone mode. The ui.load transaction uses "app_start_warm" as its measurement key.
|
|
||
| let hub = SentrySDKInternal.currentHub() | ||
| let tracer = hub.startTransaction( | ||
| with: context, | ||
| bindToScope: false, | ||
| customSamplingContext: [:], | ||
| configuration: configuration | ||
| ) | ||
| tracer.origin = SentryTraceOriginAutoAppStart | ||
|
|
||
| tracer.finish() |
There was a problem hiding this comment.
side note: if UI is rendered as part of app launch, the transaction should additionally have the app.vitals.start.screen attribute
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 39f4d4c. Configure here.
SentryAppStartMeasurementProvider is only available on UIKit platforms. Without the guard, macOS builds fail.
Use a named ObjC category (SwiftPrivate) with rawNameSource: to give a distinct selector, replacing the standalone C functions and eliminating SentryTransactionContextFactory.h.

📜 Description
Send app start data as a standalone transaction instead of attaching it to the first UIViewController transaction.

The measurement is passed directly via
SentryTracerConfiguration, avoiding the global static and its race conditions.We intentionally don't add public user-facing docs for this because this is more of a proof of concept for #6883 that we still need to iterate on before we want a larger audience to try it. The Sentry product doesn't handle these standalone app start transactions yet.
💡 Motivation and Context
Fixes #6883
💚 How did you test it?
enableStandaloneAppStartTracing = falseon this branch and compared againstmain. Both produce identical transaction structure (same operations, same hierarchy:Cold Startgrouping span with 5 child phase spans under theui.loadtransaction). No regression.Regression test run 2 — 2026-03-12T10:42Z
Method
regression-testscope tag to identify each runsearch_events+get_trace_details)Results
regression-test-feature-branch-2regression-test-main-branch-22279393e2bdd4126a29efeda00dd946b7a1bb60029c842fea30b0e289e507b81Cold Start+ 5 phases)Cold Start+ 5 phases)ui.loadui.load¹ The extra span is an unrelated
profileLaunchfile.write, not an app start span.Full span comparison (sorted by duration desc)
ui.loadui.load.initial_displayui.load.full_displayapp.start.coldapp.start.coldhttp.clientapp.start.coldapp.start.coldapp.start.coldui.loadapp.start.coldfile.readfile.writefile.readfile.readfile.readfile.readui.loadui.loadui.loadui.loadui.loadui.loadui.loadui.loadui.loadSentry links
Regression test run 1 — 2026-03-12T09:20Z
Method
regression-testscope tag to identify each runsearch_events+get_trace_details)Results
regression-test-feature-branchregression-test-main-branch3ff1c4e3c3754ab18ece26d7e0cf722a3edc353029d34c9292f5ea85df4851acCold Start+ 5 phases)Cold Start+ 5 phases)ui.loadui.loadFull span comparison (sorted by duration desc)
ui.loadui.load.initial_displayui.load.full_displayapp.start.coldapp.start.coldhttp.clientapp.start.coldapp.start.coldapp.start.coldui.loadapp.start.coldfile.readfile.readfile.readfile.writefile.readui.loadfile.readui.loadui.loadui.loadui.loadui.loadui.loadui.loadui.loadSentry links
Reproducible prompt
📝 Checklist
You have to check all boxes before merging:
sendDefaultPIIis enabled.