Skip to content

feat: Standalone app start tracing#7660

Draft
philipphofmann wants to merge 40 commits intomainfrom
feat/standalone-app-start-tracing
Draft

feat: Standalone app start tracing#7660
philipphofmann wants to merge 40 commits intomainfrom
feat/standalone-app-start-tracing

Conversation

@philipphofmann
Copy link
Member

@philipphofmann philipphofmann commented Mar 10, 2026

📜 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.

Based on #7655.
Fixes #6883.

💡 Motivation and Context

The current approach stores the app start measurement in a global static that gets consumed by the first ui.load transaction. This is fragile — the measurement can be lost or consumed by the wrong transaction. A standalone transaction gives us full control over timing and avoids race conditions.

💚 How did you test it?

  • Unit tests for standalone span building (no grouping span, correct parent IDs)
  • Unit tests for tracer configuration measurement path
  • Unit tests verifying markAsRead prevents double consumption
  • Enabled in sample app via options.experimental.enableStandaloneAppStartTracing = true
  • End-to-end regression test: Ran the iOS-Swift sample app on a clean simulator with enableStandaloneAppStartTracing = false on this branch and compared against main. Both produce identical transaction structure: 26 spans, same operations, same hierarchy (Cold Start grouping span with 5 child phase spans under the ui.load transaction). No regression.
Regression test details

Method

  1. Added a unique regression-test scope tag to identify each run
  2. Erased the simulator between runs for clean cold starts
  3. Built and launched iOS-Swift on each branch, waited ~15s, stopped the app
  4. Compared transactions via Sentry MCP (search_events + get_trace_details)

Results

Property Feature Branch Main Branch
Tag regression-test-feature-branch regression-test-main-branch
Trace ID 3ff1c4e3c3754ab18ece26d7e0cf722a 3edc353029d34c9292f5ea85df4851ac
Total spans 26 26
App start spans 6 (Cold Start + 5 phases) 6 (Cold Start + 5 phases)
Transaction op ui.load ui.load

Full span comparison (sorted by duration desc)

Span Description Operation Feature Branch Main Branch
ErrorsViewController ui.load 1152ms 1091ms
ErrorsViewController initial display ui.load.initial_display 967ms 887ms
ErrorsViewController full display ui.load.full_display 967ms 920ms
Cold Start app.start.cold 967ms 887ms
Pre Runtime Init app.start.cold 468ms 531ms
POST http://localhost:8969/stream http.client 189ms 206ms
UIKit Init app.start.cold 188ms 102ms
Initial Frame Render app.start.cold 171ms 113ms
Application Init app.start.cold 137ms 138ms
loadView ui.load 4ms 5ms
Runtime Init to Pre Main Initializers app.start.cold 2ms 2ms
QmU-DD-itF-view-M6c-EX-0C6.nib file.read 0.5ms 0.6ms
Info.plist file.read 0.4ms 0.4ms
Info-8.0+.plist file.read 0.3ms 0.3ms
data.data (533 bytes) file.write 0.3ms 0.4ms
Sim.Pencil.plist file.read 0.3ms 0.2ms
layoutSubViews ui.load 0.3ms 0.3ms
Sim.Touch.plist file.read 0.3ms 0.2ms
viewDidAppear ui.load 0.2ms 0.2ms
viewDidLoad ui.load 0.2ms 0.2ms
layoutSubViews ui.load 0.07ms 0.08ms
viewWillLayoutSubviews ui.load 0.03ms 0.03ms
viewWillAppear ui.load 0.02ms 0.02ms
viewWillLayoutSubviews ui.load 0.02ms 0.02ms
viewDidLayoutSubviews ui.load 0.02ms 0.02ms
viewDidLayoutSubviews ui.load 0.02ms 0.02ms

Sentry links

Reproducible prompt

Run an end-to-end regression test for app start tracing. Compare the current branch
(with enableStandaloneAppStartTracing = false) against main to verify no regression
in the UIViewController app start transaction.

Steps:
1. In SentrySDKWrapper.swift, set enableStandaloneAppStartTracing = false and add a
   unique scope tag: scope.setTag(value: "regression-test-feature-branch", key: "regression-test")
2. Erase the iOS simulator (xcrun simctl shutdown <ID> && xcrun simctl erase <ID>),
   boot it, build and run iOS-Swift scheme, wait 15 seconds, stop the app
3. Revert sample app changes, stash, checkout main
4. In SentrySDKWrapper.swift on main, add scope tag:
   scope.setTag(value: "regression-test-main-branch", key: "regression-test")
5. Erase the simulator again, boot, build and run iOS-Swift, wait 15 seconds, stop
6. Revert main changes, checkout back to feature branch, pop stash
7. Use Sentry MCP search_events to find transactions with tag regression-test for
   each branch in project 5428557 (sentry-sdks org)
8. Use get_trace_details and search_events to get all spans for both traces
9. Compare: span count, span operations, span descriptions, hierarchy structure.
   Durations will differ (expected). Everything else should match.
10. Update the PR description "How did you test it?" section with the comparison.

📝 Checklist

You have to check all boxes before merging:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (feedback) Implement shake gesture detection by antonis in #7579
  • (SPM) Add package traits for UI framework opt-out and macOS CLI sample by philprime in #7578
  • Standalone app start tracing by philipphofmann in #7660
  • Use full flamegraph for metrickit app hangs by noahsmartin in #7185

Bug Fixes 🐛

  • Capture instance and class method [NSApplication \_crashOnException] exceptions by denrase in #7510
  • Capture transactions during launch profiling window by philipphofmann in #7602

Internal Changes 🔧

Deps

  • Bump fastlane-plugin-sentry from 2.1.1 to 2.2.0 by dependabot in #7626
  • Bump mikepenz/action-junit-report from 6.3.0 to 6.3.1 by dependabot in #7630
  • Bump actions/setup-node from 6.2.0 to 6.3.0 by dependabot in #7627
  • Bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.23.0 to 2.23.2 by dependabot in #7628
  • Bump getsentry/craft from 2.23.0 to 2.23.2 by dependabot in #7629
  • Bump ruby/setup-ruby from 1.288.0 to 1.290.0 by dependabot in #7631

Samples

  • Restructure iOS-SwiftUI-Widgets sample by philprime in #7658
  • Restructure iOS15-SwiftUI sample by philprime in #7657
  • Restructure watchOS-Swift sample by philprime in #7614
  • Restructure visionOS-Swift sample by philprime in #7616
  • Restructure visionOS-SwiftUI-SPM sample by philprime in #7615

Other

  • (ci) Adds a Nightly Job with unit tests for less frequent devices by itaybre in #7632
  • (test-samples) Restructure SwiftUITestSample and SwiftUICrashTest by philprime in #7612
  • Fix watchOS tests and add them to nightly job by itaybre in #7633
  • Add crash when SentryInitializeForGettingSubclassesNotCalled is… by noahsmartin in #7637
  • Fix flaky HangTracker deallocated test by noahsmartin in #7639

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 027eef2

@codecov
Copy link

codecov bot commented Mar 10, 2026

Codecov Report

❌ Patch coverage is 98.76543% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 85.347%. Comparing base (2c7a266) to head (027eef2).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
.../AppStartTracking/AppStartMeasurementHandler.swift 97.368% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##              main     #7660       +/-   ##
=============================================
+ Coverage   85.307%   85.347%   +0.039%     
=============================================
  Files          485       486        +1     
  Lines        28838     28903       +65     
  Branches     12523     12551       +28     
=============================================
+ Hits         24601     24668       +67     
+ Misses        4190      4187        -3     
- Partials        47        48        +1     
Files with missing lines Coverage Δ
Sources/Sentry/SentryAppStartMeasurementProvider.m 92.307% <100.000%> (+0.415%) ⬆️
Sources/Sentry/SentryBuildAppStartSpans.m 100.000% <100.000%> (ø)
Sources/Sentry/SentryTracer.m 98.053% <100.000%> (+0.789%) ⬆️
...es/Swift/Helper/SentryEnabledFeaturesBuilder.swift 100.000% <100.000%> (ø)
...tions/AppStartTracking/SentryAppStartTracker.swift 93.209% <100.000%> (-0.501%) ⬇️
Sources/Swift/SentryDependencyContainer.swift 97.196% <100.000%> (+0.013%) ⬆️
Sources/Swift/SentryExperimentalOptions.swift 83.333% <100.000%> (+3.333%) ⬆️
.../AppStartTracking/AppStartMeasurementHandler.swift 97.368% <97.368%> (ø)

... and 4 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2c7a266...027eef2. Read the comment docs.

@philipphofmann philipphofmann added the ready-to-merge Use this label to trigger all PR workflows label Mar 10, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1219.83 ms 1239.50 ms 19.67 ms
Size 24.14 KiB 1.12 MiB 1.10 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
b0c71c8 1206.29 ms 1242.13 ms 35.84 ms
e701dc8 1215.89 ms 1254.06 ms 38.17 ms
88592c1 1221.80 ms 1259.98 ms 38.18 ms
1b74fd4 1219.43 ms 1250.17 ms 30.73 ms
6108e4c 1212.31 ms 1251.80 ms 39.49 ms
56c05ae 1218.02 ms 1253.30 ms 35.28 ms
19f3e6b 1231.17 ms 1257.02 ms 25.85 ms
2d1826b 1216.96 ms 1254.05 ms 37.09 ms
d68691e 1221.48 ms 1248.13 ms 26.65 ms
c424b6a 1220.38 ms 1248.18 ms 27.80 ms

App size

Revision Plain With Sentry Diff
b0c71c8 24.14 KiB 1.08 MiB 1.06 MiB
e701dc8 24.14 KiB 1.06 MiB 1.04 MiB
88592c1 24.14 KiB 1.12 MiB 1.10 MiB
1b74fd4 24.14 KiB 1.10 MiB 1.08 MiB
6108e4c 24.14 KiB 1.11 MiB 1.09 MiB
56c05ae 24.14 KiB 1.10 MiB 1.08 MiB
19f3e6b 24.14 KiB 1.10 MiB 1.08 MiB
2d1826b 24.14 KiB 1.12 MiB 1.09 MiB
d68691e 24.14 KiB 1.12 MiB 1.09 MiB
c424b6a 24.14 KiB 1.06 MiB 1.04 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
0d2ca60 1229.17 ms 1261.09 ms 31.91 ms

App size

Revision Plain With Sentry Diff
8b737d8 24.14 KiB 1.12 MiB 1.10 MiB
0d2ca60 24.14 KiB 1.12 MiB 1.10 MiB

Copy link
Member Author

@philipphofmann philipphofmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review for Claude Code.

@sentry
Copy link

sentry bot commented Mar 10, 2026

Sentry Build Distribution

App Version Configuration
App 9.6.0 (1) Release

@sentry
Copy link

sentry bot commented Mar 10, 2026

Sentry Build Distribution

App Version Configuration
App 9.6.0 (1) Release

@sentry
Copy link

sentry bot commented Mar 10, 2026

Sentry Build Distribution

App Version Configuration
App 9.6.0 (1) Release

@sentry
Copy link

sentry bot commented Mar 10, 2026

Sentry Build Distribution

App Version Configuration
App 9.6.0 (1) Release

Base automatically changed from ref/extract-app-start-measurement-provider to main March 11, 2026 10:44
…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
- Move public functions to top of SentryBuildAppStartSpans.m
- Expand SentryTracer comment on race condition avoidance
- Use string literals instead of constants in tests
- Assert specific debug image properties in test
- Add span tree examples to header doc comments

Agent transcript: https://claudescope.sentry.dev/share/4RQyqJ6qxJ3uLrCpOW-w1LjUpmtGerVFCS6DIZgKRXE
The platform guards for app start cleanup in clearTestState were
missing os(visionOS), causing appStartMeasurementRead to never
reset between tests on visionOS. This made tests that depend on
app start measurement data fail when run after other tests that
consume the measurement.

Agent transcript: https://claudescope.sentry.dev/share/XRVuiHsfZrr_PPrwGDvT9RkHPcp6AxGP1tIDaq2H4CU
@philipphofmann philipphofmann force-pushed the feat/standalone-app-start-tracing branch from 5378661 to 027eef2 Compare March 11, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Use this label to trigger all PR workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Send standalone app start transactions

1 participant