tags = new HashMap<>();
+ tags.put("start.reason", getReasonLabel(startInfo.getReason()));
+ tags.put("start.type", getStartupTypeLabel(startInfo.getStartType()));
+ tags.put("start.launch_mode", getLaunchModeLabel(startInfo.getLaunchMode()));
+ return tags;
+ }
+
+ @RequiresApi(api = 35)
+ private @NotNull String getStartupTypeLabel(final int startType) {
+ switch (startType) {
+ case ApplicationStartInfo.START_TYPE_COLD:
+ return "cold";
+ case ApplicationStartInfo.START_TYPE_WARM:
+ return "warm";
+ case ApplicationStartInfo.START_TYPE_HOT:
+ return "hot";
+ default:
+ return "unknown";
+ }
+ }
+
+ @RequiresApi(api = 35)
+ private @NotNull String getLaunchModeLabel(final int launchMode) {
+ switch (launchMode) {
+ case ApplicationStartInfo.LAUNCH_MODE_STANDARD:
+ return "standard";
+ case ApplicationStartInfo.LAUNCH_MODE_SINGLE_TOP:
+ return "single_top";
+ case ApplicationStartInfo.LAUNCH_MODE_SINGLE_INSTANCE:
+ return "single_instance";
+ case ApplicationStartInfo.LAUNCH_MODE_SINGLE_TASK:
+ return "single_task";
+ case ApplicationStartInfo.LAUNCH_MODE_SINGLE_INSTANCE_PER_TASK:
+ return "single_instance_per_task";
+ default:
+ return "unknown";
+ }
+ }
+
+ @RequiresApi(api = 35)
+ private @NotNull String getReasonLabel(final int reason) {
+ switch (reason) {
+ case ApplicationStartInfo.START_REASON_ALARM:
+ return "alarm";
+ case ApplicationStartInfo.START_REASON_BACKUP:
+ return "backup";
+ case ApplicationStartInfo.START_REASON_BOOT_COMPLETE:
+ return "boot_complete";
+ case ApplicationStartInfo.START_REASON_BROADCAST:
+ return "broadcast";
+ case ApplicationStartInfo.START_REASON_CONTENT_PROVIDER:
+ return "content_provider";
+ case ApplicationStartInfo.START_REASON_JOB:
+ return "job";
+ case ApplicationStartInfo.START_REASON_LAUNCHER:
+ return "launcher";
+ case ApplicationStartInfo.START_REASON_OTHER:
+ return "other";
+ case ApplicationStartInfo.START_REASON_PUSH:
+ return "push";
+ case ApplicationStartInfo.START_REASON_SERVICE:
+ return "service";
+ case ApplicationStartInfo.START_REASON_START_ACTIVITY:
+ return "start_activity";
+ default:
+ return "unknown";
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ try (final ISentryLifecycleToken ignored = startLock.acquire()) {
+ isClosed = true;
+ }
+ }
+}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationStartInfoMetricsProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationStartInfoMetricsProcessor.java
new file mode 100644
index 0000000000..6fe63032e1
--- /dev/null
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationStartInfoMetricsProcessor.java
@@ -0,0 +1,37 @@
+package io.sentry.android.core;
+
+import android.app.ApplicationStartInfo;
+import androidx.annotation.RequiresApi;
+import io.sentry.IScopes;
+import io.sentry.metrics.SentryMetricsParameters;
+import java.util.Map;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Processor that emits counter metrics from ApplicationStartInfo data.
+ *
+ * This processor emits an app.launch counter metric with attributes for start reason, start
+ * type, and launch mode.
+ *
+ *
Requires API level 35 (Android 15) or higher.
+ */
+@RequiresApi(api = 35)
+final class ApplicationStartInfoMetricsProcessor implements IApplicationStartInfoProcessor {
+
+ private final @NotNull SentryAndroidOptions options;
+
+ ApplicationStartInfoMetricsProcessor(final @NotNull SentryAndroidOptions options) {
+ this.options = options;
+ }
+
+ @Override
+ public void process(
+ final @NotNull ApplicationStartInfo startInfo,
+ final @NotNull Map tags,
+ final @NotNull IScopes scopes) {
+
+ @SuppressWarnings("unchecked")
+ final Map attributes = (Map) (Map, ?>) tags;
+ scopes.metrics().count("app.launch", 1.0, null, SentryMetricsParameters.create(attributes));
+ }
+}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationStartInfoTracingProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationStartInfoTracingProcessor.java
new file mode 100644
index 0000000000..5b7dcf08f5
--- /dev/null
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationStartInfoTracingProcessor.java
@@ -0,0 +1,301 @@
+package io.sentry.android.core;
+
+import android.app.ApplicationStartInfo;
+import android.os.SystemClock;
+import androidx.annotation.RequiresApi;
+import io.sentry.IScopes;
+import io.sentry.SentryDate;
+import io.sentry.SentryNanotimeDate;
+import io.sentry.SpanContext;
+import io.sentry.SpanDataConvention;
+import io.sentry.SpanId;
+import io.sentry.SpanStatus;
+import io.sentry.TracesSamplingDecision;
+import io.sentry.android.core.internal.util.AndroidThreadChecker;
+import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
+import io.sentry.android.core.performance.AppStartMetrics;
+import io.sentry.android.core.performance.TimeSpan;
+import io.sentry.protocol.SentryId;
+import io.sentry.protocol.SentrySpan;
+import io.sentry.protocol.SentryTransaction;
+import io.sentry.protocol.TransactionInfo;
+import io.sentry.protocol.TransactionNameSource;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Processor that generates app.start transactions from ApplicationStartInfo data.
+ *
+ * This processor creates a transaction with spans representing the app startup timeline,
+ * including bind_application, content provider initialization, application onCreate, activity
+ * lifecycle, TTID, and TTFD.
+ *
+ *
Requires API level 35 (Android 15) or higher.
+ */
+@RequiresApi(api = 35)
+final class ApplicationStartInfoTracingProcessor implements IApplicationStartInfoProcessor {
+
+ private final @NotNull SentryAndroidOptions options;
+
+ ApplicationStartInfoTracingProcessor(final @NotNull SentryAndroidOptions options) {
+ this.options = options;
+ }
+
+ @Override
+ public void process(
+ final @NotNull ApplicationStartInfo startInfo,
+ final @NotNull Map tags,
+ final @NotNull IScopes scopes) {
+
+ final long currentUnixMs = System.currentTimeMillis();
+ final long currentRealtimeMs = SystemClock.elapsedRealtime();
+ final long unixTimeOffsetMs = currentUnixMs - currentRealtimeMs;
+
+ final long startRealtimeMs = getStartTimestampMs(startInfo);
+ final long ttidRealtimeMs = getFirstFrameTimestampMs(startInfo);
+ final long ttfdRealtimeMs = getFullyDrawnTimestampMs(startInfo);
+ final long bindApplicationRealtimeMs = getBindApplicationTimestampMs(startInfo);
+
+ final SentryDate startDate = dateFromUnixTime(unixTimeOffsetMs + startRealtimeMs);
+ final long endTimestamp = ttidRealtimeMs > 0 ? ttidRealtimeMs : ttfdRealtimeMs;
+ final SentryDate endDate =
+ endTimestamp > 0
+ ? dateFromUnixTime(unixTimeOffsetMs + endTimestamp)
+ : options.getDateProvider().now();
+
+ final SentryId traceId = new SentryId();
+ final SpanId spanId = new SpanId();
+ final SpanContext traceContext =
+ new SpanContext(traceId, spanId, "app.start", null, new TracesSamplingDecision(true));
+ traceContext.setStatus(SpanStatus.OK);
+
+ final double startTimestampSecs = dateToSeconds(startDate);
+ final double endTimestampSecs = dateToSeconds(endDate);
+
+ final SentryTransaction transaction =
+ new SentryTransaction(
+ "app.start",
+ startTimestampSecs,
+ endTimestampSecs,
+ new ArrayList<>(),
+ new HashMap<>(),
+ new TransactionInfo(TransactionNameSource.COMPONENT.apiName()));
+
+ transaction.getContexts().setTrace(traceContext);
+
+ for (Map.Entry entry : tags.entrySet()) {
+ transaction.setTag(entry.getKey(), entry.getValue());
+ }
+
+ if (bindApplicationRealtimeMs > 0) {
+ transaction
+ .getSpans()
+ .add(
+ createSpan(
+ traceId,
+ spanId,
+ "bind_application",
+ null,
+ startDate,
+ dateFromUnixTime(unixTimeOffsetMs + bindApplicationRealtimeMs)));
+ }
+
+ if (startInfo.getStartType() == ApplicationStartInfo.START_TYPE_COLD) {
+ attachColdStartInstrumentations(transaction, traceId, spanId);
+ }
+
+ attachActivitySpans(transaction, traceId, spanId);
+
+ if (ttidRealtimeMs > 0) {
+ transaction
+ .getSpans()
+ .add(
+ createSpan(
+ traceId,
+ spanId,
+ "ttid",
+ null,
+ startDate,
+ dateFromUnixTime(unixTimeOffsetMs + ttidRealtimeMs)));
+ }
+ if (ttfdRealtimeMs > 0) {
+ transaction
+ .getSpans()
+ .add(
+ createSpan(
+ traceId,
+ spanId,
+ "ttfd",
+ null,
+ startDate,
+ dateFromUnixTime(unixTimeOffsetMs + ttfdRealtimeMs)));
+ }
+
+ scopes.captureTransaction(transaction, null, null);
+ }
+
+ private @NotNull SentrySpan createSpan(
+ final @NotNull SentryId traceId,
+ final @NotNull SpanId parentSpanId,
+ final @NotNull String operation,
+ final @Nullable String description,
+ final @NotNull SentryDate startDate,
+ final @NotNull SentryDate endDate) {
+
+ final Map spanData = new HashMap<>();
+ spanData.put(SpanDataConvention.THREAD_ID, AndroidThreadChecker.mainThreadSystemId);
+ spanData.put(SpanDataConvention.THREAD_NAME, "main");
+
+ final double startTimestampSecs = dateToSeconds(startDate);
+ final double endTimestampSecs = dateToSeconds(endDate);
+
+ return new SentrySpan(
+ startTimestampSecs,
+ endTimestampSecs,
+ traceId,
+ new SpanId(),
+ parentSpanId,
+ operation,
+ description,
+ SpanStatus.OK,
+ "manual",
+ new ConcurrentHashMap<>(),
+ new ConcurrentHashMap<>(),
+ spanData);
+ }
+
+ private void attachColdStartInstrumentations(
+ final @NotNull SentryTransaction transaction,
+ final @NotNull SentryId traceId,
+ final @NotNull SpanId parentSpanId) {
+
+ final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
+ final @NotNull List contentProviderSpans =
+ appStartMetrics.getContentProviderOnCreateTimeSpans();
+
+ for (final TimeSpan cpSpan : contentProviderSpans) {
+ if (cpSpan.hasStarted() && cpSpan.hasStopped()) {
+ final SentryDate cpStartDate = dateFromUnixTime(cpSpan.getStartTimestampMs());
+ final SentryDate cpEndDate = dateFromUnixTime(cpSpan.getProjectedStopTimestampMs());
+
+ transaction
+ .getSpans()
+ .add(
+ createSpan(
+ traceId,
+ parentSpanId,
+ "contentprovider.on_create",
+ cpSpan.getDescription(),
+ cpStartDate,
+ cpEndDate));
+ }
+ }
+
+ final TimeSpan appOnCreateSpan = appStartMetrics.getApplicationOnCreateTimeSpan();
+ final String appOnCreateDescription =
+ appOnCreateSpan.hasStarted() ? appOnCreateSpan.getDescription() : null;
+
+ if (appOnCreateSpan.hasStarted() && appOnCreateSpan.hasStopped()) {
+ final SentryDate appOnCreateStart = dateFromUnixTime(appOnCreateSpan.getStartTimestampMs());
+ final SentryDate appOnCreateEnd =
+ dateFromUnixTime(appOnCreateSpan.getProjectedStopTimestampMs());
+
+ transaction
+ .getSpans()
+ .add(
+ createSpan(
+ traceId,
+ parentSpanId,
+ "application.on_create",
+ appOnCreateDescription,
+ appOnCreateStart,
+ appOnCreateEnd));
+ }
+ }
+
+ private void attachActivitySpans(
+ final @NotNull SentryTransaction transaction,
+ final @NotNull SentryId traceId,
+ final @NotNull SpanId parentSpanId) {
+
+ final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
+ final @NotNull List activityLifecycleTimeSpans =
+ appStartMetrics.getActivityLifecycleTimeSpans();
+
+ for (final ActivityLifecycleTimeSpan span : activityLifecycleTimeSpans) {
+ final TimeSpan onCreate = span.getOnCreate();
+ final TimeSpan onStart = span.getOnStart();
+
+ if (onCreate.hasStarted() && onCreate.hasStopped()) {
+ final SentryDate start = dateFromUnixTime(onCreate.getStartTimestampMs());
+ final SentryDate end = dateFromUnixTime(onCreate.getProjectedStopTimestampMs());
+
+ transaction
+ .getSpans()
+ .add(
+ createSpan(
+ traceId,
+ parentSpanId,
+ "activity.on_create",
+ onCreate.getDescription(),
+ start,
+ end));
+ }
+
+ if (onStart.hasStarted() && onStart.hasStopped()) {
+ final SentryDate start = dateFromUnixTime(onStart.getStartTimestampMs());
+ final SentryDate end = dateFromUnixTime(onStart.getProjectedStopTimestampMs());
+
+ transaction
+ .getSpans()
+ .add(
+ createSpan(
+ traceId,
+ parentSpanId,
+ "activity.on_start",
+ onStart.getDescription(),
+ start,
+ end));
+ }
+ }
+ }
+
+ private static double dateToSeconds(final @NotNull SentryDate date) {
+ return date.nanoTimestamp() / 1e9;
+ }
+
+ private static @NotNull SentryDate dateFromUnixTime(final long timeMillis) {
+ return new SentryNanotimeDate(new Date(timeMillis), 0);
+ }
+
+ private long getStartTimestampMs(final @NotNull ApplicationStartInfo startInfo) {
+ final Map timestamps = startInfo.getStartupTimestamps();
+ final Long forkTime = timestamps.get(ApplicationStartInfo.START_TIMESTAMP_FORK);
+ return forkTime != null ? TimeUnit.NANOSECONDS.toMillis(forkTime) : 0;
+ }
+
+ private long getBindApplicationTimestampMs(final @NotNull ApplicationStartInfo startInfo) {
+ final Map timestamps = startInfo.getStartupTimestamps();
+ final Long bindTime = timestamps.get(ApplicationStartInfo.START_TIMESTAMP_BIND_APPLICATION);
+ return bindTime != null ? TimeUnit.NANOSECONDS.toMillis(bindTime) : 0;
+ }
+
+ private long getFirstFrameTimestampMs(final @NotNull ApplicationStartInfo startInfo) {
+ final Map timestamps = startInfo.getStartupTimestamps();
+ final Long firstFrameTime = timestamps.get(ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME);
+ return firstFrameTime != null ? TimeUnit.NANOSECONDS.toMillis(firstFrameTime) : 0;
+ }
+
+ private long getFullyDrawnTimestampMs(final @NotNull ApplicationStartInfo startInfo) {
+ final Map timestamps = startInfo.getStartupTimestamps();
+ final Long fullyDrawnTime = timestamps.get(ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN);
+ return fullyDrawnTime != null ? TimeUnit.NANOSECONDS.toMillis(fullyDrawnTime) : 0;
+ }
+}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/IApplicationStartInfoProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/IApplicationStartInfoProcessor.java
new file mode 100644
index 0000000000..3eb868914e
--- /dev/null
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/IApplicationStartInfoProcessor.java
@@ -0,0 +1,32 @@
+package io.sentry.android.core;
+
+import androidx.annotation.RequiresApi;
+import io.sentry.IScopes;
+import java.util.Map;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Interface for processing ApplicationStartInfo data from Android system.
+ *
+ * Processors are registered with {@link ApplicationStartInfoIntegration} and are called when app
+ * start data becomes available, either from historical data or the current app start.
+ *
+ *
Requires API level 35 (Android 15) or higher.
+ */
+@ApiStatus.Internal
+interface IApplicationStartInfoProcessor {
+
+ /**
+ * Process the ApplicationStartInfo data.
+ *
+ * @param startInfo The ApplicationStartInfo from Android system
+ * @param tags Extracted tags (start.reason, start.type, start.launch_mode)
+ * @param scopes The Sentry scopes for capturing events
+ */
+ @RequiresApi(api = 35)
+ void process(
+ @NotNull android.app.ApplicationStartInfo startInfo,
+ @NotNull Map tags,
+ @NotNull IScopes scopes);
+}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java
index d106f63e75..f00e1be8d4 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java
@@ -243,6 +243,37 @@ public interface BeforeCaptureCallback {
private boolean enableTombstone = false;
+ /**
+ * Controls whether to collect and report application startup information from the {@link
+ * android.app.ApplicationStartInfo} system API (Android 15+). When enabled, creates transactions
+ * and metrics for each application start event.
+ *
+ * Requires API level 35 (Android 15) or higher.
+ *
+ *
Default is false (opt-in).
+ */
+ private boolean enableApplicationStartInfo = false;
+
+ /**
+ * Controls whether to generate transactions from ApplicationStartInfo data. Requires {@link
+ * #enableApplicationStartInfo} to be true.
+ *
+ *
Requires API level 35 (Android 15) or higher.
+ *
+ *
Default is false (opt-in).
+ */
+ @ApiStatus.Experimental private boolean enableApplicationStartInfoTracing = false;
+
+ /**
+ * Controls whether to emit metrics from ApplicationStartInfo data. Requires {@link
+ * #enableApplicationStartInfo} to be true.
+ *
+ *
Requires API level 35 (Android 15) or higher.
+ *
+ *
Default is false (opt-in).
+ */
+ @ApiStatus.Experimental private boolean enableApplicationStartInfoMetrics = false;
+
public SentryAndroidOptions() {
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
setSdkVersion(createSdkVersion());
@@ -335,6 +366,69 @@ public boolean isTombstoneEnabled() {
return enableTombstone;
}
+ /**
+ * Sets ApplicationStartInfo collection to enabled or disabled. Requires API level 35 (Android 15)
+ * or higher.
+ *
+ * @param enableApplicationStartInfo true for enabled and false for disabled
+ */
+ @ApiStatus.Experimental
+ public void setEnableApplicationStartInfo(final boolean enableApplicationStartInfo) {
+ this.enableApplicationStartInfo = enableApplicationStartInfo;
+ }
+
+ /**
+ * Checks if ApplicationStartInfo collection is enabled or disabled. Default is disabled.
+ *
+ * @return true if enabled or false otherwise
+ */
+ @ApiStatus.Experimental
+ public boolean isEnableApplicationStartInfo() {
+ return enableApplicationStartInfo;
+ }
+
+ /**
+ * Controls whether to generate transactions from ApplicationStartInfo data. Requires {@link
+ * #enableApplicationStartInfo} to be true.
+ *
+ * @param enable true to enable transaction generation, false to disable
+ */
+ @ApiStatus.Experimental
+ public void setEnableApplicationStartInfoTracing(final boolean enable) {
+ this.enableApplicationStartInfoTracing = enable;
+ }
+
+ /**
+ * Checks if ApplicationStartInfo tracing is enabled or disabled. Default is disabled.
+ *
+ * @return true if enabled or false otherwise
+ */
+ @ApiStatus.Experimental
+ public boolean isEnableApplicationStartInfoTracing() {
+ return enableApplicationStartInfoTracing;
+ }
+
+ /**
+ * Controls whether to emit metrics from ApplicationStartInfo data. Requires {@link
+ * #enableApplicationStartInfo} to be true.
+ *
+ * @param enable true to enable metrics emission, false to disable
+ */
+ @ApiStatus.Experimental
+ public void setEnableApplicationStartInfoMetrics(final boolean enable) {
+ this.enableApplicationStartInfoMetrics = enable;
+ }
+
+ /**
+ * Checks if ApplicationStartInfo metrics are enabled or disabled. Default is disabled.
+ *
+ * @return true if enabled or false otherwise
+ */
+ @ApiStatus.Experimental
+ public boolean isEnableApplicationStartInfoMetrics() {
+ return enableApplicationStartInfoMetrics;
+ }
+
public boolean isEnableActivityLifecycleBreadcrumbs() {
return enableActivityLifecycleBreadcrumbs;
}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java
index 5aad7ef1b2..74bba7c25f 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java
@@ -35,6 +35,7 @@ public final class AndroidEnvelopeCache extends EnvelopeCache {
public static final String LAST_ANR_REPORT = "last_anr_report";
public static final String LAST_TOMBSTONE_REPORT = "last_tombstone_report";
+ public static final String LAST_APP_START_REPORT = "last_app_start_report";
private final @NotNull ICurrentDateProvider currentDateProvider;
@@ -209,6 +210,29 @@ private void writeLastReportedMarker(
return lastReportedMarker(options, LAST_TOMBSTONE_REPORT, LAST_TOMBSTONE_MARKER_LABEL);
}
+ public static @Nullable Long lastReportedAppStart(final @NotNull SentryOptions options) {
+ return lastReportedMarker(options, LAST_APP_START_REPORT, LAST_APP_START_MARKER_LABEL);
+ }
+
+ public static void storeAppStartTimestamp(
+ final @NotNull SentryOptions options, final long timestamp) {
+ final String cacheDirPath = options.getCacheDirPath();
+ if (cacheDirPath == null) {
+ options
+ .getLogger()
+ .log(DEBUG, "Cache dir path is null, the App Start marker will not be written");
+ return;
+ }
+
+ final File marker = new File(cacheDirPath, LAST_APP_START_REPORT);
+ try (final OutputStream outputStream = new FileOutputStream(marker)) {
+ outputStream.write(String.valueOf(timestamp).getBytes(UTF_8));
+ outputStream.flush();
+ } catch (Throwable e) {
+ options.getLogger().log(ERROR, "Error writing the App Start marker to the disk", e);
+ }
+ }
+
private static final class TimestampMarkerHandler {
interface TimestampExtractor {
@NotNull
@@ -254,6 +278,7 @@ void handle(
public static final String LAST_TOMBSTONE_MARKER_LABEL = "Tombstone";
public static final String LAST_ANR_MARKER_LABEL = "ANR";
+ public static final String LAST_APP_START_MARKER_LABEL = "App Start";
private static final List> TIMESTAMP_MARKER_HANDLERS =
Arrays.asList(
new TimestampMarkerHandler<>(
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java
index 1bb95b9061..a9587f57c9 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java
@@ -85,6 +85,11 @@ public enum AppStartType {
private final AtomicInteger activeActivitiesCounter = new AtomicInteger();
private final AtomicBoolean firstDrawDone = new AtomicBoolean(false);
+ // ApplicationStartInfo data (API 35+)
+ private volatile @Nullable ApplicationStartInfo applicationStartInfo = null;
+ private volatile @Nullable Map applicationStartInfoTags = null;
+ private volatile long applicationStartInfoUnixOffsetMs = 0;
+
public static @NotNull AppStartMetrics getInstance() {
if (instance == null) {
try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) {
@@ -235,6 +240,43 @@ long getFirstIdle() {
return firstIdle;
}
+ /**
+ * Store ApplicationStartInfo data for later access.
+ *
+ * @param info The ApplicationStartInfo from Android system
+ * @param tags Extracted tags (start.reason, start.type, start.launch_mode)
+ * @param unixOffsetMs Offset to convert realtime to unix time
+ */
+ public void setApplicationStartInfo(
+ final @Nullable ApplicationStartInfo info,
+ final @Nullable Map tags,
+ final long unixOffsetMs) {
+ this.applicationStartInfo = info;
+ this.applicationStartInfoTags = tags;
+ this.applicationStartInfoUnixOffsetMs = unixOffsetMs;
+ }
+
+ /**
+ * @return The stored ApplicationStartInfo, or null if not available
+ */
+ public @Nullable ApplicationStartInfo getApplicationStartInfo() {
+ return applicationStartInfo;
+ }
+
+ /**
+ * @return The extracted tags from ApplicationStartInfo, or null if not available
+ */
+ public @Nullable Map getApplicationStartInfoTags() {
+ return applicationStartInfoTags;
+ }
+
+ /**
+ * @return The unix time offset for ApplicationStartInfo timestamps
+ */
+ public long getApplicationStartInfoUnixOffsetMs() {
+ return applicationStartInfoUnixOffsetMs;
+ }
+
@TestOnly
public void clear() {
appStartType = AppStartType.UNKNOWN;
@@ -258,6 +300,9 @@ public void clear() {
firstDrawDone.set(false);
activeActivitiesCounter.set(0);
firstIdle = -1;
+ applicationStartInfo = null;
+ applicationStartInfoTags = null;
+ applicationStartInfoUnixOffsetMs = 0;
}
public @Nullable ITransactionProfiler getAppStartProfiler() {
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt
index 348075ff90..9d6a77b9cc 100644
--- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt
@@ -936,4 +936,12 @@ class AndroidOptionsInitializerTest {
fixture.initSut()
assertIs(fixture.sentryOptions.runtimeManager)
}
+
+ @Test
+ fun `ApplicationStartInfoIntegration is added to integration list`() {
+ fixture.initSut()
+ val actual =
+ fixture.sentryOptions.integrations.firstOrNull { it is ApplicationStartInfoIntegration }
+ assertNotNull(actual)
+ }
}
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationStartInfoIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationStartInfoIntegrationTest.kt
new file mode 100644
index 0000000000..262039b56b
--- /dev/null
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationStartInfoIntegrationTest.kt
@@ -0,0 +1,279 @@
+package io.sentry.android.core
+
+import android.app.ActivityManager
+import android.content.Context
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.sentry.IScopes
+import io.sentry.ISentryExecutorService
+import io.sentry.protocol.SentryTransaction
+import java.util.concurrent.Callable
+import java.util.function.Consumer
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+@Config(sdk = [35])
+class ApplicationStartInfoIntegrationTest {
+
+ private lateinit var context: Context
+ private lateinit var options: SentryAndroidOptions
+ private lateinit var scopes: IScopes
+ private lateinit var activityManager: ActivityManager
+ private lateinit var executor: ISentryExecutorService
+ private lateinit var buildInfoProvider: BuildInfoProvider
+
+ @Before
+ fun setup() {
+ context = mock()
+ options = SentryAndroidOptions()
+ scopes = mock()
+ activityManager = mock()
+ executor = mock()
+ buildInfoProvider = mock()
+
+ // Setup default options
+ options.isEnableApplicationStartInfo = true
+ options.executorService = executor
+ options.setLogger(mock())
+
+ val mockDateProvider = mock()
+ val mockDate = mock()
+ whenever(mockDate.nanoTimestamp()).thenReturn(System.currentTimeMillis() * 1_000_000L)
+ whenever(mockDateProvider.now()).thenReturn(mockDate)
+ options.dateProvider = mockDateProvider
+
+ // Mock BuildInfoProvider to return API 35+
+ whenever(buildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+
+ // Execute tasks immediately for testing
+ whenever(executor.submit(any>())).thenAnswer {
+ val callable = it.arguments[0] as Callable<*>
+ callable.call()
+ mock>()
+ }
+ whenever(executor.submit(any())).thenAnswer {
+ val runnable = it.arguments[0] as Runnable
+ runnable.run()
+ mock>()
+ }
+
+ // Mock ActivityManager as system service
+ whenever(context.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn(activityManager)
+ }
+
+ @Test
+ fun `integration does not register when disabled`() {
+ options.isEnableApplicationStartInfo = false
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+
+ integration.register(scopes, options)
+
+ verify(executor, never()).submit(any())
+ }
+
+ @Test
+ fun `integration registers completion listener on API 35+`() {
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+ integration.register(scopes, options)
+
+ verify(activityManager).addApplicationStartInfoCompletionListener(any(), any())
+ }
+
+ @Test
+ fun `transaction includes correct tags from ApplicationStartInfo`() {
+ val listenerCaptor = argumentCaptor>()
+ val transactionCaptor = argumentCaptor()
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+ integration.register(scopes, options)
+
+ verify(activityManager)
+ .addApplicationStartInfoCompletionListener(any(), listenerCaptor.capture())
+
+ val startInfo = createMockApplicationStartInfo()
+ listenerCaptor.firstValue.accept(startInfo)
+
+ verify(scopes).captureTransaction(transactionCaptor.capture(), anyOrNull(), anyOrNull())
+ val transaction = transactionCaptor.firstValue
+ assertNotNull(transaction.tags)
+ assertTrue(transaction.tags!!.containsKey("start.reason"))
+ assertTrue(transaction.tags!!.containsKey("start.type"))
+ assertTrue(transaction.tags!!.containsKey("start.launch_mode"))
+ }
+
+ @Test
+ fun `transaction includes start type from ApplicationStartInfo`() {
+ val listenerCaptor = argumentCaptor>()
+ val transactionCaptor = argumentCaptor()
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+ integration.register(scopes, options)
+
+ verify(activityManager)
+ .addApplicationStartInfoCompletionListener(any(), listenerCaptor.capture())
+
+ val startInfo =
+ createMockApplicationStartInfo(startType = android.app.ApplicationStartInfo.START_TYPE_COLD)
+ listenerCaptor.firstValue.accept(startInfo)
+
+ verify(scopes).captureTransaction(transactionCaptor.capture(), anyOrNull(), anyOrNull())
+ assertEquals("cold", transactionCaptor.firstValue.tags!!["start.type"])
+ }
+
+ @Test
+ fun `transaction includes launch mode from ApplicationStartInfo`() {
+ val listenerCaptor = argumentCaptor>()
+ val transactionCaptor = argumentCaptor()
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+ integration.register(scopes, options)
+
+ verify(activityManager)
+ .addApplicationStartInfoCompletionListener(any(), listenerCaptor.capture())
+
+ val startInfo =
+ createMockApplicationStartInfo(
+ launchMode = android.app.ApplicationStartInfo.LAUNCH_MODE_STANDARD
+ )
+ listenerCaptor.firstValue.accept(startInfo)
+
+ verify(scopes).captureTransaction(transactionCaptor.capture(), anyOrNull(), anyOrNull())
+ assertEquals("standard", transactionCaptor.firstValue.tags!!["start.launch_mode"])
+ }
+
+ @Test
+ fun `creates bind_application span when timestamp available`() {
+ val listenerCaptor = argumentCaptor>()
+ val transactionCaptor = argumentCaptor()
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+ integration.register(scopes, options)
+
+ verify(activityManager)
+ .addApplicationStartInfoCompletionListener(any(), listenerCaptor.capture())
+
+ val startInfo =
+ createMockApplicationStartInfo(forkTime = 1000000000L, bindApplicationTime = 1100000000L)
+ listenerCaptor.firstValue.accept(startInfo)
+
+ verify(scopes).captureTransaction(transactionCaptor.capture(), anyOrNull(), anyOrNull())
+ val spans = transactionCaptor.firstValue.spans
+ assertTrue(spans.any { it.op == "bind_application" })
+ }
+
+ @Test
+ fun `creates ttid span when timestamp available`() {
+ val listenerCaptor = argumentCaptor>()
+ val transactionCaptor = argumentCaptor()
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+ integration.register(scopes, options)
+
+ verify(activityManager)
+ .addApplicationStartInfoCompletionListener(any(), listenerCaptor.capture())
+
+ val startInfo =
+ createMockApplicationStartInfo(forkTime = 1000000000L, firstFrameTime = 1500000000L)
+ listenerCaptor.firstValue.accept(startInfo)
+
+ verify(scopes).captureTransaction(transactionCaptor.capture(), anyOrNull(), anyOrNull())
+ val spans = transactionCaptor.firstValue.spans
+ assertTrue(spans.any { it.op == "ttid" })
+ }
+
+ @Test
+ fun `creates ttfd span when timestamp available`() {
+ val listenerCaptor = argumentCaptor>()
+ val transactionCaptor = argumentCaptor()
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+ integration.register(scopes, options)
+
+ verify(activityManager)
+ .addApplicationStartInfoCompletionListener(any(), listenerCaptor.capture())
+
+ val startInfo =
+ createMockApplicationStartInfo(forkTime = 1000000000L, fullyDrawnTime = 2000000000L)
+ listenerCaptor.firstValue.accept(startInfo)
+
+ verify(scopes).captureTransaction(transactionCaptor.capture(), anyOrNull(), anyOrNull())
+ val spans = transactionCaptor.firstValue.spans
+ assertTrue(spans.any { it.op == "ttfd" })
+ }
+
+ @Test
+ fun `closes integration without errors`() {
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+ integration.register(scopes, options)
+
+ integration.close()
+ // Should not throw exception
+ }
+
+ @Test
+ fun `transaction name is app_start`() {
+ val listenerCaptor = argumentCaptor>()
+ val transactionCaptor = argumentCaptor()
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+ integration.register(scopes, options)
+
+ verify(activityManager)
+ .addApplicationStartInfoCompletionListener(any(), listenerCaptor.capture())
+
+ val startInfo = createMockApplicationStartInfo()
+ listenerCaptor.firstValue.accept(startInfo)
+
+ verify(scopes).captureTransaction(transactionCaptor.capture(), anyOrNull(), anyOrNull())
+ assertEquals("app.start", transactionCaptor.firstValue.transaction)
+ }
+
+ @Test
+ fun `does not register on API lower than 35`() {
+ whenever(buildInfoProvider.sdkInfoVersion).thenReturn(34)
+ val integration = ApplicationStartInfoIntegration(context, buildInfoProvider)
+
+ integration.register(scopes, options)
+
+ verify(activityManager, never()).addApplicationStartInfoCompletionListener(any(), any())
+ }
+
+ // Helper methods
+ private fun createMockApplicationStartInfo(
+ forkTime: Long = 1000000000L, // nanoseconds
+ bindApplicationTime: Long = 0L,
+ firstFrameTime: Long = 0L,
+ fullyDrawnTime: Long = 0L,
+ reason: Int = android.app.ApplicationStartInfo.START_REASON_LAUNCHER,
+ startType: Int = android.app.ApplicationStartInfo.START_TYPE_COLD,
+ launchMode: Int = android.app.ApplicationStartInfo.LAUNCH_MODE_STANDARD,
+ ): android.app.ApplicationStartInfo {
+ val startInfo = mock()
+
+ val timestamps = mutableMapOf()
+ timestamps[android.app.ApplicationStartInfo.START_TIMESTAMP_FORK] = forkTime
+ if (bindApplicationTime > 0) {
+ timestamps[android.app.ApplicationStartInfo.START_TIMESTAMP_BIND_APPLICATION] =
+ bindApplicationTime
+ }
+ if (firstFrameTime > 0) {
+ timestamps[android.app.ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME] = firstFrameTime
+ }
+ if (fullyDrawnTime > 0) {
+ timestamps[android.app.ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN] = fullyDrawnTime
+ }
+
+ whenever(startInfo.reason).thenReturn(reason)
+ whenever(startInfo.startType).thenReturn(startType)
+ whenever(startInfo.launchMode).thenReturn(launchMode)
+ whenever(startInfo.startupTimestamps).thenReturn(timestamps)
+
+ return startInfo
+ }
+}