Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e7047cb
fix(android): Improve app start type detection with main thread timing
markushi Dec 29, 2025
4164619
Update CHANGELOG.md
markushi Dec 29, 2025
70879c1
Reduce number of foreground checks and add maestro tests
markushi Jan 15, 2026
1547b1f
Merge branch 'main' into fix/app-start-warm-detection
markushi Jan 15, 2026
acf7e58
Merge branch 'main' into fix/app-start-warm-detection
markushi Jan 15, 2026
b0722d3
Merge branch 'main' into fix/app-start-warm-detection
markushi Jan 16, 2026
7034d95
Update Changelog
markushi Jan 16, 2026
ef2b3bf
Switch to MessageQueue.IdleHandler
markushi Jan 16, 2026
15eeb49
Merge branch 'main' into fix/app-start-warm-detection
markushi Feb 3, 2026
ac3defe
fix(android): Improve warm start detection with API 35+ support
markushi Feb 3, 2026
46daaf3
Trying to fix notification drawer open
markushi Feb 3, 2026
2e01fc9
include maestro debug logs
markushi Feb 3, 2026
e4c4fbd
Enable adb screenrecord
markushi Feb 3, 2026
81e7e51
capture maestro errors
markushi Feb 3, 2026
0ac6ce5
single-line maestro as test runner executes scripts as individual lines
markushi Feb 3, 2026
71e4a2c
put maestro run into single line
markushi Feb 3, 2026
7cc431f
fix empty screenrecordings
markushi Feb 3, 2026
d8d7ba7
send sigint to screenrecord to gracefully finish recording
markushi Feb 3, 2026
87e6e30
fix typo
markushi Feb 3, 2026
cf8083e
use google_apis in order to have a launcher for notification access
markushi Feb 4, 2026
f1c7451
enable more debugging
markushi Feb 4, 2026
adad43c
Use adb emu screenrecord instead of adb shell screenrecord
markushi Feb 4, 2026
d2678dd
switch to mac-os
markushi Feb 4, 2026
c948a88
Ensure paths exist
markushi Feb 5, 2026
6f21404
bump maestro version
markushi Feb 5, 2026
1688287
Improve swipe
markushi Feb 5, 2026
ea305a2
Use ubuntu everywhere, disable maestro debug logs
markushi Feb 5, 2026
4d1e949
slow swipe
markushi Feb 5, 2026
b4641eb
Merge branch 'main' into fix/app-start-warm-detection
markushi Feb 5, 2026
d7d4236
retry swiping
markushi Feb 5, 2026
75ca4b9
Merge branch 'fix/app-start-warm-detection' of github.com:getsentry/s…
markushi Feb 5, 2026
e181878
try different swipe params
markushi Feb 5, 2026
744f5b0
Increase emulator memory
markushi Feb 6, 2026
fec2342
Update Changelog
markushi Feb 6, 2026
f6787f3
Merge branch 'main' into fix/app-start-warm-detection
markushi Feb 6, 2026
7af3e59
Fix Changelog
markushi Feb 6, 2026
51be24b
test(android): Add API 35 tests for app start detection
markushi Feb 6, 2026
8e4ecca
Format code
getsentry-bot Feb 6, 2026
0e0c8b9
Merge branch 'main' into fix/app-start-warm-detection
markushi Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- **IMPORTANT:** This disables collecting external storage size (total/free) by default, to enable it back
use `options.isCollectExternalStorageContext = true` or `<meta-data android:name="io.sentry.external-storage-context" android:value="true" />`
- Fix `NullPointerException` when reading ANR marker ([#4979](https://github.com/getsentry/sentry-java/pull/4979))
- Improve app start type detection with main thread timing ([#4999](https://github.com/getsentry/sentry-java/pull/4999))

### Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public enum AppStartType {

private @NotNull AppStartType appStartType = AppStartType.UNKNOWN;
private boolean appLaunchedInForeground;
private volatile long firstPostUptimeMillis = -1;

private final @NotNull TimeSpan appStartSpan;
private final @NotNull TimeSpan sdkInitTimeSpan;
Expand Down Expand Up @@ -234,6 +235,7 @@ public void clear() {
shouldSendStartMeasurements = true;
firstDrawDone.set(false);
activeActivitiesCounter.set(0);
firstPostUptimeMillis = -1;
}

public @Nullable ITransactionProfiler getAppStartProfiler() {
Expand Down Expand Up @@ -316,7 +318,15 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
// (possibly others) the first task posted on the main thread is called before the
// Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate
// callback is called before the application one.
new Handler(Looper.getMainLooper()).post(() -> checkCreateTimeOnMain());
new Handler(Looper.getMainLooper())
.post(
new Runnable() {
@Override
public void run() {
firstPostUptimeMillis = SystemClock.uptimeMillis();
checkCreateTimeOnMain();
}
});
}

private void checkCreateTimeOnMain() {
Expand Down Expand Up @@ -348,7 +358,7 @@ public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle saved
if (activeActivitiesCounter.incrementAndGet() == 1 && !firstDrawDone.get()) {
final long nowUptimeMs = SystemClock.uptimeMillis();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we still need this variable given it's just few microseconds apart from activityCreatedUptimeMillis?


// If the app (process) was launched more than 1 minute ago, it's likely wrong
// If the app (process) was launched more than 1 minute ago, consider it a warm start
Comment thread
cursor[bot] marked this conversation as resolved.
final long durationSinceAppStartMillis = nowUptimeMs - appStartSpan.getStartUptimeMs();
if (!appLaunchedInForeground || durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) {
appStartType = AppStartType.WARM;
Comment thread
sentry[bot] marked this conversation as resolved.
Expand All @@ -360,8 +370,12 @@ public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle saved
CLASS_LOADED_UPTIME_MS = nowUptimeMs;
contentProviderOnCreates.clear();
applicationOnCreate.reset();
} else if (savedInstanceState != null) {
appStartType = AppStartType.WARM;
} else if (firstPostUptimeMillis > 0 && nowUptimeMs > firstPostUptimeMillis) {
appStartType = AppStartType.WARM;
} else {
appStartType = savedInstanceState == null ? AppStartType.COLD : AppStartType.WARM;
appStartType = AppStartType.COLD;
}
}
appLaunchedInForeground = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -537,4 +537,127 @@ class AppStartMetricsTest {

assertEquals(secondActivity, CurrentActivityHolder.getInstance().activity)
}

@Test
fun `firstPostUptimeMillis is properly cleared`() {
val metrics = AppStartMetrics.getInstance()
metrics.registerLifecycleCallbacks(mock<Application>())
Shadows.shadowOf(Looper.getMainLooper()).idle()

val reflectionField = AppStartMetrics::class.java.getDeclaredField("firstPostUptimeMillis")
reflectionField.isAccessible = true
val firstPostValue = reflectionField.getLong(metrics)
assertTrue(firstPostValue > 0)

metrics.clear()

val clearedValue = reflectionField.getLong(metrics)
assertEquals(-1, clearedValue)
}

@Test
fun `firstPostUptimeMillis is set when registerLifecycleCallbacks is called`() {
val metrics = AppStartMetrics.getInstance()
val beforeRegister = SystemClock.uptimeMillis()

metrics.registerLifecycleCallbacks(mock<Application>())
Shadows.shadowOf(Looper.getMainLooper()).idle()

val afterIdle = SystemClock.uptimeMillis()

val reflectionField = AppStartMetrics::class.java.getDeclaredField("firstPostUptimeMillis")
reflectionField.isAccessible = true
val firstPostValue = reflectionField.getLong(metrics)

assertTrue(firstPostValue >= beforeRegister)
assertTrue(firstPostValue <= afterIdle)
}

@Test
fun `Sets app launch type to WARM when activity created after firstPost`() {
val metrics = AppStartMetrics.getInstance()
assertEquals(AppStartMetrics.AppStartType.UNKNOWN, metrics.appStartType)

metrics.registerLifecycleCallbacks(mock<Application>())
Shadows.shadowOf(Looper.getMainLooper()).idle()

SystemClock.setCurrentTimeMillis(SystemClock.uptimeMillis() + 100)
metrics.onActivityCreated(mock<Activity>(), null)

assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)
}

@Test
fun `Sets app launch type to COLD when activity created before firstPost executes`() {
val metrics = AppStartMetrics.getInstance()
assertEquals(AppStartMetrics.AppStartType.UNKNOWN, metrics.appStartType)

metrics.registerLifecycleCallbacks(mock<Application>())
metrics.onActivityCreated(mock<Activity>(), null)

assertEquals(AppStartMetrics.AppStartType.COLD, metrics.appStartType)

Shadows.shadowOf(Looper.getMainLooper()).idle()

assertEquals(AppStartMetrics.AppStartType.COLD, metrics.appStartType)
}

@Test
fun `Sets app launch type to COLD when activity created at same time as firstPost`() {
val metrics = AppStartMetrics.getInstance()

val now = SystemClock.uptimeMillis()
val reflectionField = AppStartMetrics::class.java.getDeclaredField("firstPostUptimeMillis")
reflectionField.isAccessible = true
reflectionField.setLong(metrics, now)

SystemClock.setCurrentTimeMillis(now)
metrics.onActivityCreated(mock<Activity>(), null)

assertEquals(AppStartMetrics.AppStartType.COLD, metrics.appStartType)
}

@Test
fun `savedInstanceState check takes precedence over firstPost timing`() {
val metrics = AppStartMetrics.getInstance()

metrics.registerLifecycleCallbacks(mock<Application>())
Shadows.shadowOf(Looper.getMainLooper()).idle()

SystemClock.setCurrentTimeMillis(SystemClock.uptimeMillis() + 100)
metrics.onActivityCreated(mock<Activity>(), mock<Bundle>())

assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)
}

@Test
fun `timeout check takes precedence over firstPost timing`() {
val metrics = AppStartMetrics.getInstance()

metrics.registerLifecycleCallbacks(mock<Application>())
Shadows.shadowOf(Looper.getMainLooper()).idle()

val futureTime = SystemClock.uptimeMillis() + TimeUnit.MINUTES.toMillis(2)
SystemClock.setCurrentTimeMillis(futureTime)
metrics.onActivityCreated(mock<Activity>(), null)

assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)
assertTrue(metrics.appStartTimeSpan.hasStarted())
assertEquals(futureTime, metrics.appStartTimeSpan.startUptimeMs)
}

@Test
fun `firstPost timing does not affect subsequent activity creations`() {
val metrics = AppStartMetrics.getInstance()

metrics.registerLifecycleCallbacks(mock<Application>())
Shadows.shadowOf(Looper.getMainLooper()).idle()

SystemClock.setCurrentTimeMillis(SystemClock.uptimeMillis() + 100)
metrics.onActivityCreated(mock<Activity>(), null)
assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)

metrics.onActivityCreated(mock<Activity>(), mock<Bundle>())
assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)
}
}
Loading