Skip to content

Commit 3280187

Browse files
authored
Merge 2823497 into 6dff1c9
2 parents 6dff1c9 + 2823497 commit 3280187

6 files changed

Lines changed: 170 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Emits a transaction named `App Start` with op `app.start`, carrying the existing app start measurements and phase spans (`process.load`, `contentprovider.load`, `application.load`, activity lifecycle spans) as direct children of the root
1010
- The standalone transaction shares the same `traceId` as the first `ui.load` activity transaction so they remain linked in the trace view
1111
- Also covers non-activity starts (broadcast receivers, services, content providers)
12+
- On Android 15+ (API 35), the standalone transaction reports why the OS started the process via `app.vitals.start.reason` trace data (e.g. `launcher`, `broadcast`, `service`, `content_provider`), derived from `ApplicationStartInfo.getReason()` ([#5552](https://github.com/getsentry/sentry-java/pull/5552))
1213

1314
### Improvements
1415

sentry-android-core/api/sentry-android-core.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
746746
public fun getAppStartContinuousProfiler ()Lio/sentry/IContinuousProfiler;
747747
public fun getAppStartEndTime ()Lio/sentry/SentryDate;
748748
public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler;
749+
public fun getAppStartReason ()Ljava/lang/String;
749750
public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision;
750751
public fun getAppStartSentryTraceHeader ()Ljava/lang/String;
751752
public fun getAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
@@ -780,6 +781,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
780781
public fun setAppStartSentryTraceHeader (Ljava/lang/String;)V
781782
public fun setAppStartTraceId (Lio/sentry/protocol/SentryId;)V
782783
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V
784+
public fun setCachedStartInfo (Landroid/app/ApplicationStartInfo;)V
783785
public fun setClassLoadedUptimeMs (J)V
784786
public fun setHeadlessAppStartListener (Lio/sentry/android/core/performance/AppStartMetrics$HeadlessAppStartListener;)V
785787
public fun shouldSendStartMeasurements ()Z

sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public final class ActivityLifecycleIntegration
7272
static final long APP_START_TO_UI_LOAD_CONTINUATION_MAX_GAP_NANOS = TimeUnit.MINUTES.toNanos(1);
7373
private static final String TRACE_ORIGIN = "auto.ui.activity";
7474
static final String APP_START_SCREEN_DATA = "app.vitals.start.screen";
75+
static final String APP_START_REASON_DATA = "app.vitals.start.reason";
7576
static final String APP_START_TRACE_ORIGIN = "auto.app.start";
7677

7778
private final @NotNull Application application;
@@ -286,6 +287,10 @@ private void startTracing(final @NotNull Activity activity) {
286287
appStartSamplingDecision),
287288
appStartTransactionOptions);
288289
appStartTransaction.setData(APP_START_SCREEN_DATA, activityName);
290+
final @Nullable String appStartReason = AppStartMetrics.getInstance().getAppStartReason();
291+
if (appStartReason != null) {
292+
appStartTransaction.setData(APP_START_REASON_DATA, appStartReason);
293+
}
289294
}
290295

291296
// Continue either the foreground app.start above or an earlier headless app.start.
@@ -1002,6 +1007,10 @@ private void onHeadlessAppStart() {
10021007
null);
10031008

10041009
final @NotNull ITransaction transaction = scopes.startTransaction(txnContext, txnOptions);
1010+
final @Nullable String appStartReason = metrics.getAppStartReason();
1011+
if (appStartReason != null) {
1012+
transaction.setData(APP_START_REASON_DATA, appStartReason);
1013+
}
10051014
metrics.setAppStartTraceId(transaction.getSpanContext().getTraceId());
10061015
// Persist trace headers so a later ui.load can share traceId and sampleRand.
10071016
metrics.setAppStartSentryTraceHeader(transaction.toSentryTrace().getValue());

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,45 @@ public void setAppStartType(final @NotNull AppStartType appStartType) {
166166
return appStartType;
167167
}
168168

169+
/**
170+
* The reason the OS started the process, mapped from {@link ApplicationStartInfo#getReason()}.
171+
* Only available on API 35+ (when {@link #cachedStartInfo} was resolved); returns {@code null}
172+
* otherwise or for an unmapped reason.
173+
*/
174+
public @Nullable String getAppStartReason() {
175+
if (cachedStartInfo == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
176+
return null;
177+
}
178+
switch (cachedStartInfo.getReason()) {
179+
case ApplicationStartInfo.START_REASON_ALARM:
180+
return "alarm";
181+
case ApplicationStartInfo.START_REASON_BACKUP:
182+
return "backup";
183+
case ApplicationStartInfo.START_REASON_BOOT_COMPLETE:
184+
return "boot_complete";
185+
case ApplicationStartInfo.START_REASON_BROADCAST:
186+
return "broadcast";
187+
case ApplicationStartInfo.START_REASON_CONTENT_PROVIDER:
188+
return "content_provider";
189+
case ApplicationStartInfo.START_REASON_JOB:
190+
return "job";
191+
case ApplicationStartInfo.START_REASON_LAUNCHER:
192+
return "launcher";
193+
case ApplicationStartInfo.START_REASON_LAUNCHER_RECENTS:
194+
return "launcher_recents";
195+
case ApplicationStartInfo.START_REASON_PUSH:
196+
return "push";
197+
case ApplicationStartInfo.START_REASON_SERVICE:
198+
return "service";
199+
case ApplicationStartInfo.START_REASON_START_ACTIVITY:
200+
return "start_activity";
201+
case ApplicationStartInfo.START_REASON_OTHER:
202+
return "other";
203+
default:
204+
return null;
205+
}
206+
}
207+
169208
public boolean isAppLaunchedInForeground() {
170209
return appLaunchedInForeground.getValue();
171210
}
@@ -372,6 +411,12 @@ public void setClassLoadedUptimeMs(final long classLoadedUptimeMs) {
372411
CLASS_LOADED_UPTIME_MS = classLoadedUptimeMs;
373412
}
374413

414+
@TestOnly
415+
@ApiStatus.Internal
416+
public void setCachedStartInfo(final @Nullable ApplicationStartInfo cachedStartInfo) {
417+
this.cachedStartInfo = cachedStartInfo;
418+
}
419+
375420
/**
376421
* Called by instrumentation
377422
*

sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.app.Activity
44
import android.app.ActivityManager
55
import android.app.ActivityManager.RunningAppProcessInfo
66
import android.app.Application
7+
import android.app.ApplicationStartInfo
78
import android.content.Context
89
import android.os.Build
910
import android.os.Bundle
@@ -275,6 +276,76 @@ class ActivityLifecycleIntegrationTest {
275276
)
276277
}
277278

279+
@Test
280+
@Config(sdk = [Build.VERSION_CODES.VANILLA_ICE_CREAM])
281+
fun `Standalone app start transaction carries app start reason when available`() {
282+
val sut =
283+
fixture.getSut {
284+
it.tracesSampleRate = 1.0
285+
it.isEnableStandaloneAppStartTracing = true
286+
}
287+
sut.register(fixture.scopes, fixture.options)
288+
289+
setAppStartTime()
290+
val startInfo =
291+
mock<ApplicationStartInfo>().apply {
292+
whenever(reason).thenReturn(ApplicationStartInfo.START_REASON_LAUNCHER)
293+
}
294+
AppStartMetrics.getInstance().setCachedStartInfo(startInfo)
295+
296+
val activity = mock<Activity>()
297+
sut.onActivityCreated(activity, fixture.bundle)
298+
299+
val appStartTransaction =
300+
fixture.createdTransactions.single {
301+
it.spanContext.operation == ActivityLifecycleIntegration.STANDALONE_APP_START_OP
302+
}
303+
assertEquals("launcher", appStartTransaction.getData("app.vitals.start.reason"))
304+
}
305+
306+
@Test
307+
fun `Standalone app start transaction has no app start reason when unavailable`() {
308+
val sut =
309+
fixture.getSut {
310+
it.tracesSampleRate = 1.0
311+
it.isEnableStandaloneAppStartTracing = true
312+
}
313+
sut.register(fixture.scopes, fixture.options)
314+
315+
setAppStartTime()
316+
317+
val activity = mock<Activity>()
318+
sut.onActivityCreated(activity, fixture.bundle)
319+
320+
val appStartTransaction =
321+
fixture.createdTransactions.single {
322+
it.spanContext.operation == ActivityLifecycleIntegration.STANDALONE_APP_START_OP
323+
}
324+
assertNull(appStartTransaction.getData("app.vitals.start.reason"))
325+
}
326+
327+
@Test
328+
@Config(sdk = [Build.VERSION_CODES.VANILLA_ICE_CREAM])
329+
fun `Headless standalone app start transaction carries app start reason when available`() {
330+
val sut =
331+
fixture.getSut {
332+
it.tracesSampleRate = 1.0
333+
it.isEnableStandaloneAppStartTracing = true
334+
}
335+
sut.register(fixture.scopes, fixture.options)
336+
prepareHeadlessAppStart(appStartType = AppStartType.COLD)
337+
val startInfo =
338+
mock<ApplicationStartInfo>().apply {
339+
whenever(reason).thenReturn(ApplicationStartInfo.START_REASON_BROADCAST)
340+
}
341+
AppStartMetrics.getInstance().setCachedStartInfo(startInfo)
342+
343+
driveHeadlessAppStart()
344+
345+
val transaction = fixture.createdTransactions.single()
346+
assertEquals("broadcast", transaction.getData("app.vitals.start.reason"))
347+
}
348+
278349
@Test
279350
fun `HeadlessAppStartListener is registered when standalone flag is on and performance enabled`() {
280351
val sut =

sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTestApi35.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import java.util.concurrent.atomic.AtomicInteger
1515
import kotlin.test.Test
1616
import kotlin.test.assertEquals
1717
import kotlin.test.assertFalse
18+
import kotlin.test.assertNull
1819
import org.junit.Before
1920
import org.junit.runner.RunWith
2021
import org.mockito.kotlin.mock
@@ -207,6 +208,47 @@ class AppStartMetricsTestApi35 {
207208
assertEquals(1, listenerCalls.get())
208209
}
209210

211+
@Test
212+
fun `getAppStartReason maps ApplicationStartInfo reason to string on API 35`() {
213+
val mockStartInfo = mock<ApplicationStartInfo>()
214+
whenever(mockStartInfo.startupState).thenReturn(ApplicationStartInfo.STARTUP_STATE_STARTED)
215+
whenever(mockStartInfo.startType).thenReturn(ApplicationStartInfo.START_TYPE_COLD)
216+
whenever(mockStartInfo.reason).thenReturn(ApplicationStartInfo.START_REASON_BROADCAST)
217+
SentryShadowActivityManager.setHistoricalProcessStartReasons(listOf(mockStartInfo))
218+
val metrics = AppStartMetrics.getInstance()
219+
220+
val app = ApplicationProvider.getApplicationContext<Application>()
221+
metrics.registerLifecycleCallbacks(app)
222+
223+
assertEquals("broadcast", metrics.appStartReason)
224+
}
225+
226+
@Test
227+
fun `getAppStartReason returns null when no ApplicationStartInfo is available`() {
228+
SentryShadowActivityManager.setHistoricalProcessStartReasons(emptyList())
229+
val metrics = AppStartMetrics.getInstance()
230+
231+
val app = ApplicationProvider.getApplicationContext<Application>()
232+
metrics.registerLifecycleCallbacks(app)
233+
234+
assertNull(metrics.appStartReason)
235+
}
236+
237+
@Test
238+
fun `getAppStartReason returns null for an unmapped reason`() {
239+
val mockStartInfo = mock<ApplicationStartInfo>()
240+
whenever(mockStartInfo.startupState).thenReturn(ApplicationStartInfo.STARTUP_STATE_STARTED)
241+
whenever(mockStartInfo.startType).thenReturn(ApplicationStartInfo.START_TYPE_COLD)
242+
whenever(mockStartInfo.reason).thenReturn(Int.MAX_VALUE)
243+
SentryShadowActivityManager.setHistoricalProcessStartReasons(listOf(mockStartInfo))
244+
val metrics = AppStartMetrics.getInstance()
245+
246+
val app = ApplicationProvider.getApplicationContext<Application>()
247+
metrics.registerLifecycleCallbacks(app)
248+
249+
assertNull(metrics.appStartReason)
250+
}
251+
210252
private fun waitForMainLooperIdle() {
211253
Handler(Looper.getMainLooper()).post {}
212254
Shadows.shadowOf(Looper.getMainLooper()).idle()

0 commit comments

Comments
 (0)