Skip to content

Commit a9e53be

Browse files
runningcodeclaudemarkushi
authored
fix(android): Read the app start type once per event processor pass (#6006)
* fix(android): Read the app start type once per event processor pass PerformanceAndroidEventProcessor read AppStartMetrics.getAppStartType() three separate times while building a single transaction: once for the measurement key, once to decide whether to attach the cold-start breakdown children, and once for contexts.app.start_type, which is also what is now stamped on the trace context and every child span as app.vitals.start.type. The field is written on the main thread (activity lifecycle callbacks, ApplicationStartInfo) while the processor runs on whichever thread finished the transaction, which can be the Sentry timer thread for idle and deadline timeouts. Backgrounding the app flips the type to WARM in onActivityDestroyed, so the reads can disagree and ship a transaction whose measurement says app_start_cold while every span says app.vitals.start.type = warm. Snapshot the type once at the top of process() and pass it down, and make the field volatile so the read is not a data race. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * changelog --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Markus Hintersteiner <markus.hintersteiner@sentry.io>
1 parent a6bbe0c commit a9e53be

4 files changed

Lines changed: 75 additions & 12 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
- Add `Session.State.Unhandled` for unhandled errors that do not terminate the process ([#5919](https://github.com/getsentry/sentry-java/pull/5919))
88

9-
109
### Improvements
1110

1211
- Emit a single `ui.compose` span per `SentryTraced` on initial composition instead of one on every recomposition, and set the origin on `ui.render` spans ([#6051](https://github.com/getsentry/sentry-java/pull/6051))
@@ -22,6 +21,7 @@
2221
- `Sentry.pushScope`, `Sentry.pushIsolationScope` and `Sentry.popScope` remain no-ops when `globalHubMode` is enabled
2322
- Drop the `profiler_id` from transactions and spans when no Perfetto profile covers them, e.g. when Android's `ProfilingManager` rate limits the profiling request ([#6015](https://github.com/getsentry/sentry-java/pull/6015))
2423
- Prevent events from being dropped when feature flags are added while an event is being captured ([#5989](https://github.com/getsentry/sentry-java/pull/5989))
24+
- Report a consistent app start type across the app start measurement, `contexts.app` and the `app.start` span attributes ([#6006](https://github.com/getsentry/sentry-java/pull/6006))
2525

2626
### Internal
2727

‎sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
8686
}
8787

8888
final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
89+
// The app start type can flip (e.g. the last activity being destroyed marks the next start
90+
// as warm) while this runs on the timer thread that finished the transaction. Read it once so
91+
// the measurement key, contexts.app and the span attributes below cannot disagree.
92+
final @NotNull AppStartMetrics.AppStartType appStartType = appStartMetrics.getAppStartType();
8993
// the app start measurement is only sent once and only if the transaction has
9094
// the app.start span, which is automatically created by the SDK.
9195
if (hasAppStartSpan(transaction)) {
@@ -141,14 +145,14 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
141145
(float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName());
142146

143147
final String appStartKey =
144-
appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.COLD
148+
appStartType == AppStartMetrics.AppStartType.COLD
145149
? MeasurementValue.KEY_APP_START_COLD
146150
: MeasurementValue.KEY_APP_START_WARM;
147151

148152
transaction.getMeasurements().put(appStartKey, value);
149153
}
150154

151-
attachAppStartSpans(appStartMetrics, transaction);
155+
attachAppStartSpans(appStartMetrics, appStartType, transaction);
152156
appStartMetrics.onAppStartSpansSent();
153157
}
154158
}
@@ -158,14 +162,12 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
158162
appContext = new App();
159163
transaction.getContexts().setApp(appContext);
160164
}
161-
final String appStartType =
162-
appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.COLD
163-
? "cold"
164-
: "warm";
165-
appContext.setStartType(appStartType);
165+
final String appStartTypeName =
166+
appStartType == AppStartMetrics.AppStartType.COLD ? "cold" : "warm";
167+
appContext.setStartType(appStartTypeName);
166168

167169
if (isStandaloneAppStartTxn) {
168-
setAppStartVitals(transaction, appStartType);
170+
setAppStartVitals(transaction, appStartTypeName);
169171
}
170172
}
171173

@@ -293,10 +295,12 @@ private boolean hasAppStartSpan(final @NotNull SentryTransaction txn) {
293295
}
294296

295297
private void attachAppStartSpans(
296-
final @NotNull AppStartMetrics appStartMetrics, final @NotNull SentryTransaction txn) {
298+
final @NotNull AppStartMetrics appStartMetrics,
299+
final @NotNull AppStartMetrics.AppStartType appStartType,
300+
final @NotNull SentryTransaction txn) {
297301

298302
// We include process init, content providers and application.onCreate spans only on cold start
299-
if (appStartMetrics.getAppStartType() != AppStartMetrics.AppStartType.COLD) {
303+
if (appStartType != AppStartMetrics.AppStartType.COLD) {
300304
return;
301305
}
302306

‎sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ public enum AppStartType {
6868
public static final @NotNull AutoClosableReentrantLock staticLock =
6969
new AutoClosableReentrantLock();
7070

71-
private @NotNull AppStartType appStartType = AppStartType.UNKNOWN;
71+
// Written on the main thread (activity lifecycle, ApplicationStartInfo) and read on whichever
72+
// thread finishes an app start transaction, which can be the Sentry timer thread.
73+
private volatile @NotNull AppStartType appStartType = AppStartType.UNKNOWN;
7274
private @Nullable volatile Boolean appLaunchedInForeground;
7375
private volatile long firstIdle = -1;
7476

‎sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,63 @@ class PerformanceAndroidEventProcessorTest {
205205
}
206206
}
207207

208+
@Test
209+
fun `cold standalone app start reports the same type on measurement, contexts and spans`() {
210+
val sut = fixture.getSut(enablePerformanceV2 = true)
211+
AppStartMetrics.getInstance().apply {
212+
appStartType = AppStartType.COLD
213+
isAppLaunchedInForeground = true
214+
classLoadedUptimeMs = 50
215+
appStartTimeSpan.apply {
216+
setStartedAt(1)
217+
setStoppedAt(100)
218+
}
219+
applicationOnCreateTimeSpan.apply {
220+
setStartedAt(10)
221+
description = "com.example.App.onCreate"
222+
setStoppedAt(42)
223+
}
224+
}
225+
226+
var tr = createStandaloneAppStartTransaction(appStartScreen = "Activity")
227+
tr = sut.process(tr, Hint())
228+
229+
assertThat(tr.measurements).containsKey(MeasurementValue.KEY_APP_START_COLD)
230+
assertThat(tr.measurements).doesNotContainKey(MeasurementValue.KEY_APP_START_WARM)
231+
assertThat(tr.contexts.app!!.startType).isEqualTo("cold")
232+
assertThat(tr.contexts.trace!!.data[APP_START_TYPE_DATA]).isEqualTo("cold")
233+
// cold starts attach the process init / Application.onCreate breakdown children
234+
assertThat(tr.spans).isNotEmpty()
235+
for (span in tr.spans) {
236+
assertThat(span.data?.get(APP_START_TYPE_DATA)).isEqualTo("cold")
237+
}
238+
}
239+
240+
@Test
241+
fun `warm standalone app start reports the same type on measurement, contexts and spans`() {
242+
val sut = fixture.getSut(enablePerformanceV2 = true)
243+
AppStartMetrics.getInstance().apply {
244+
appStartType = AppStartType.WARM
245+
isAppLaunchedInForeground = true
246+
classLoadedUptimeMs = 50
247+
appStartTimeSpan.apply {
248+
setStartedAt(1)
249+
setStoppedAt(100)
250+
}
251+
}
252+
253+
var tr = createStandaloneAppStartTransaction(appStartScreen = "Activity")
254+
tr = sut.process(tr, Hint())
255+
256+
assertThat(tr.measurements).containsKey(MeasurementValue.KEY_APP_START_WARM)
257+
assertThat(tr.measurements).doesNotContainKey(MeasurementValue.KEY_APP_START_COLD)
258+
assertThat(tr.contexts.app!!.startType).isEqualTo("warm")
259+
assertThat(tr.contexts.trace!!.data[APP_START_TYPE_DATA]).isEqualTo("warm")
260+
for (span in tr.spans) {
261+
assertThat(span.data?.get(APP_START_TYPE_DATA)).isEqualTo("warm")
262+
}
263+
}
264+
208265
@Test
209266
fun `standalone app start sets screen and type on user spans under the extended span`() {
210267
val sut = fixture.getSut()

0 commit comments

Comments
 (0)