-
-
Notifications
You must be signed in to change notification settings - Fork 472
feat(extend-app-start): [3/4] Eagerly create the extended app start transaction #5608
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/app-start-extension-android
Are you sure you want to change the base?
Changes from all commits
d996425
51e4969
4c1f0d7
48bb3a1
9fa64fd
38f83e1
55decba
f0a85b0
66cd87d
571416e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,8 @@ public final class ActivityLifecycleIntegration | |
| static final String APP_START_COLD = "app.start.cold"; | ||
| static final String TTID_OP = "ui.load.initial_display"; | ||
| static final String TTFD_OP = "ui.load.full_display"; | ||
| static final String APP_START_EXTENDED_OP = "app.start.extended"; | ||
| static final String APP_START_EXTENDED_DESC = "Extended App Start"; | ||
| static final long TTFD_TIMEOUT_MILLIS = 25000; | ||
| // If a headless app start and the following activity's ui.load are more than this far apart, they | ||
| // are treated as unrelated and not connected into the same trace. | ||
|
|
@@ -139,7 +141,10 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions | |
| application.registerActivityLifecycleCallbacks(this); | ||
|
|
||
| if (performanceEnabled && this.options.isEnableStandaloneAppStartTracing()) { | ||
| AppStartMetrics.getInstance().setHeadlessAppStartListener(this::onHeadlessAppStart); | ||
| final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance(); | ||
| metrics.setHeadlessAppStartListener(this::onHeadlessAppStart); | ||
| // Enables Sentry.extendAppStart(). Standalone-only, since it is only registered here. | ||
| metrics.getAppStartExtension().setExtendAppStartListener(this::onExtendAppStartRequested); | ||
| addIntegrationToSdkVersion("StandaloneAppStart"); | ||
| } | ||
|
|
||
|
|
@@ -154,7 +159,9 @@ private boolean isPerformanceEnabled(final @NotNull SentryAndroidOptions options | |
| @Override | ||
| public void close() throws IOException { | ||
| application.unregisterActivityLifecycleCallbacks(this); | ||
| AppStartMetrics.getInstance().setHeadlessAppStartListener(null); | ||
| final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance(); | ||
| metrics.setHeadlessAppStartListener(null); | ||
| metrics.getAppStartExtension().setExtendAppStartListener(null); | ||
|
|
||
| if (options != null) { | ||
| options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration removed."); | ||
|
|
@@ -259,17 +266,27 @@ private void startTracing(final @NotNull Activity activity) { | |
| transactionOptions.setAppStartTransaction(appStartSamplingDecision != null); | ||
| setSpanOrigin(transactionOptions); | ||
|
|
||
| // An extend-app-start transaction (Sentry.extendAppStart) is already open. Reuse its trace | ||
| // for this ui.load instead of creating a second app.start. It also stores an app-start | ||
| // trace id, so the headless-start check below is guarded with !extensionActive to avoid | ||
| // mistaking it for a finished headless start. | ||
| final boolean extensionActive = | ||
| AppStartMetrics.getInstance().getAppStartExtension().isActive(); | ||
|
|
||
| final @Nullable SentryId storedAppStartTraceId = | ||
| AppStartMetrics.getInstance().getAppStartTraceId(); | ||
| final boolean isFollowingHeadlessAppStart = (storedAppStartTraceId != null); | ||
| final boolean isFollowingHeadlessAppStart = | ||
| !extensionActive && (storedAppStartTraceId != null); | ||
|
|
||
| final boolean isAppStart = | ||
| !(firstActivityCreated || appStartTime == null || coldStart == null); | ||
| // Foreground starts create app.start first; ui.load then shares its trace. | ||
| // Foreground starts create app.start first; ui.load then shares its trace. When the app | ||
| // start is being extended, the eager app.start txn already exists, so we continue it. | ||
| final boolean createStandaloneAppStart = | ||
| isAppStart | ||
| && options.isEnableStandaloneAppStartTracing() | ||
| && !isFollowingHeadlessAppStart; | ||
| && !isFollowingHeadlessAppStart | ||
| && !extensionActive; | ||
|
|
||
| if (createStandaloneAppStart) { | ||
| final TransactionOptions appStartTransactionOptions = new TransactionOptions(); | ||
|
|
@@ -300,15 +317,25 @@ private void startTracing(final @NotNull Activity activity) { | |
| continueSentryTrace = appStartTransaction.toSentryTrace().getValue(); | ||
| final @Nullable BaggageHeader baggageHeader = appStartTransaction.toBaggageHeader(null); | ||
| continueBaggage = baggageHeader == null ? null : baggageHeader.getValue(); | ||
| } else if (isFollowingHeadlessAppStart | ||
| && isWithinAppStartContinuationWindow(ttidStartTime)) { | ||
| } else if (extensionActive | ||
| || (isFollowingHeadlessAppStart && isWithinAppStartContinuationWindow(ttidStartTime))) { | ||
| // Continue the eager extension's app.start trace, or an earlier headless app.start. | ||
| continueSentryTrace = AppStartMetrics.getInstance().getAppStartSentryTraceHeader(); | ||
| continueBaggage = AppStartMetrics.getInstance().getAppStartBaggageHeader(); | ||
| } else { | ||
| continueSentryTrace = null; | ||
| continueBaggage = null; | ||
| } | ||
|
|
||
| if (extensionActive) { | ||
| // Attach the screen (this first activity) so the eager app.start matches the foreground | ||
| // standalone app.start and the event processor treats it as a foreground (not headless) | ||
| // start. | ||
| AppStartMetrics.getInstance() | ||
| .getAppStartExtension() | ||
| .setData(APP_START_SCREEN_DATA, activityName); | ||
| } | ||
|
|
||
| final @Nullable TransactionContext continuedContext = | ||
| continueSentryTrace == null | ||
| ? null | ||
|
|
@@ -967,6 +994,9 @@ private void finishAppStartSpan(final @Nullable SentryDate endDate) { | |
| if (appStartTransaction != null && !appStartTransaction.isFinished()) { | ||
| appStartTransaction.finish(SpanStatus.OK, appStartEndTime); | ||
| } | ||
| // Finish the eager extended transaction at the natural first-frame end. waitForChildren keeps | ||
| // it open until the extended span finishes; no-op if the app start was not extended. | ||
| AppStartMetrics.getInstance().getAppStartExtension().finishTransaction(appStartEndTime); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -994,17 +1024,51 @@ private void onHeadlessAppStart() { | |
| return; | ||
| } | ||
|
|
||
| // Extended headless start: finish the existing eager txn at the headless end instead of | ||
| // creating a second one. | ||
| if (metrics.getAppStartExtension().isActive()) { | ||
| metrics.getAppStartExtension().finishTransaction(endTime); | ||
| return; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extended headless omits end timeLow Severity When headless startup reuses an active extended eager transaction, Additional Locations (1)Reviewed by Cursor Bugbot for commit 66cd87d. Configure here. |
||
| } | ||
|
Comment on lines
+1029
to
+1032
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: An extended headless app start fails to set Suggested FixAfter the call to Prompt for AI AgentDid we get this right? ๐ / ๐ to inform future reviews. |
||
|
|
||
| final @NotNull ITransaction transaction = | ||
| createStandaloneAppStartTransaction(startTime, null, false); | ||
| // Persist the end time so a later activity can decide whether its ui.load is close enough in | ||
| // time to continue this trace. | ||
| metrics.setAppStartEndTime(endTime); | ||
|
|
||
| transaction.finish(SpanStatus.OK, endTime); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Headless path duplicates finished extensionMedium Severity
Reviewed by Cursor Bugbot for commit 66cd87d. Configure here. |
||
| } | ||
|
|
||
| /** | ||
| * Creates the standalone {@code app.start} transaction (not bound to the scope) and persists its | ||
| * trace headers so a later {@code ui.load} can share the same trace. Shared by the headless path | ||
| * and the eager extension path. When {@code holdOpenForExtension} is true, the transaction waits | ||
| * for its children and gets a deadline so it stays open until the extended span finishes. | ||
| */ | ||
| private @NotNull ITransaction createStandaloneAppStartTransaction( | ||
| final @NotNull SentryDate startTime, | ||
| final @Nullable TracesSamplingDecision samplingDecision, | ||
| final boolean holdOpenForExtension) { | ||
| final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance(); | ||
|
|
||
| final TransactionOptions txnOptions = new TransactionOptions(); | ||
| txnOptions.setBindToScope(false); | ||
| txnOptions.setStartTimestamp(startTime); | ||
| txnOptions.setOrigin(APP_START_TRACE_ORIGIN); | ||
| txnOptions.setAppStartTransaction(samplingDecision != null); | ||
| if (holdOpenForExtension) { | ||
| txnOptions.setWaitForChildren(true); | ||
| final long deadlineTimeoutMillis = options.getDeadlineTimeout(); | ||
| txnOptions.setDeadlineTimeout(deadlineTimeoutMillis <= 0 ? null : deadlineTimeoutMillis); | ||
| } | ||
|
|
||
| final @NotNull TransactionContext txnContext = | ||
| new TransactionContext( | ||
| STANDALONE_APP_START_NAME, | ||
| TransactionNameSource.COMPONENT, | ||
| STANDALONE_APP_START_OP, | ||
| null); | ||
| samplingDecision); | ||
|
|
||
| final @NotNull ITransaction transaction = scopes.startTransaction(txnContext, txnOptions); | ||
| final @Nullable String appStartReason = metrics.getAppStartReason(); | ||
|
|
@@ -1016,10 +1080,55 @@ private void onHeadlessAppStart() { | |
| metrics.setAppStartSentryTraceHeader(transaction.toSentryTrace().getValue()); | ||
| final @Nullable BaggageHeader baggageHeader = transaction.toBaggageHeader(null); | ||
| metrics.setAppStartBaggageHeader(baggageHeader == null ? null : baggageHeader.getValue()); | ||
| // Persist the end time so a later activity can decide whether its ui.load is close enough in | ||
| // time to continue this trace. | ||
| metrics.setAppStartEndTime(endTime); | ||
| return transaction; | ||
| } | ||
|
|
||
| transaction.finish(SpanStatus.OK, endTime); | ||
| /** | ||
| * Handles {@code Sentry.extendAppStart()}: eagerly creates the standalone app.start transaction | ||
| * and the extended child span (we have scopes here), then hands both to the {@link | ||
| * AppStartExtension}, which owns them. The transaction is held open ({@code waitForChildren}) | ||
| * until the user calls {@code Sentry.finishExtendedAppStart()} or the deadline forces it. | ||
| * Standalone-only: this is only registered as a listener when standalone app start tracing is | ||
| * enabled. | ||
| */ | ||
| private @Nullable AppStartExtension.ExtendedAppStart onExtendAppStartRequested() { | ||
| if (scopes == null | ||
| || options == null | ||
| || !performanceEnabled | ||
| || !options.isEnableStandaloneAppStartTracing()) { | ||
| return null; | ||
| } | ||
| final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance(); | ||
|
|
||
| // The earliest known start of this app start (process start when perf-v2 is available, else SDK | ||
| // init). It is available before the first activity because SentryPerformanceProvider sets it. | ||
| final @NotNull TimeSpan appStartTimeSpan = | ||
| metrics.getAppStartTimeSpan().hasStarted() | ||
| ? metrics.getAppStartTimeSpan() | ||
| : metrics.getSdkInitTimeSpan(); | ||
| final @Nullable SentryDate startTime = appStartTimeSpan.getStartTimestamp(); | ||
| if (startTime == null) { | ||
| return null; | ||
| } | ||
|
|
||
| // The app start txn inherits the sampling decision from app start profiling, then clears it so | ||
| // it doesn't leak to the later ui.load. | ||
| final @Nullable TracesSamplingDecision samplingDecision = metrics.getAppStartSamplingDecision(); | ||
| metrics.setAppStartSamplingDecision(null); | ||
|
|
||
| final @NotNull ITransaction transaction = | ||
| createStandaloneAppStartTransaction(startTime, samplingDecision, true); | ||
|
|
||
| final SpanOptions spanOptions = new SpanOptions(); | ||
| setSpanOrigin(spanOptions); | ||
| final @NotNull ISpan extendedSpan = | ||
| transaction.startChild( | ||
| APP_START_EXTENDED_OP, | ||
| APP_START_EXTENDED_DESC, | ||
| AndroidDateUtils.getCurrentSentryDateTime(), | ||
| Instrumenter.SENTRY, | ||
| spanOptions); | ||
|
|
||
| return new AppStartExtension.ExtendedAppStart(transaction, extendedSpan); | ||
| } | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extended headless vitals dropped after screen
Medium Severity
Attaching
app.vitals.start.screento the eager extendedapp.starttransaction makes the processor classify it as non-headless, whileshouldSendStartMeasurementsstill requiresappLaunchedInForeground. Headless extended starts that later open an activity can skip cold/warm app-start measurements entirely.Additional Locations (1)
sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java#L95-L101Reviewed by Cursor Bugbot for commit 571416e. Configure here.