Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Fixes

- Fix warm app start type detection for edge cases ([#4999](https://github.com/getsentry/sentry-java/pull/4999))

### Features

- Added `io.sentry.ndk.sdk-name` Android manifest option to configure the native SDK's name ([#5027](https://github.com/getsentry/sentry-java/pull/5027))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import android.app.Activity;
import android.app.Application;
import android.content.ContentProvider;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.MessageQueue;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -21,6 +23,7 @@
import io.sentry.android.core.SentryAndroidOptions;
import io.sentry.android.core.internal.util.FirstDrawDoneListener;
import io.sentry.util.AutoClosableReentrantLock;
import io.sentry.util.LazyEvaluator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -56,7 +59,15 @@ public enum AppStartType {
new AutoClosableReentrantLock();

private @NotNull AppStartType appStartType = AppStartType.UNKNOWN;
private boolean appLaunchedInForeground;
private final LazyEvaluator<Boolean> appLaunchedInForeground =
new LazyEvaluator<>(
new LazyEvaluator.Evaluator<Boolean>() {
@Override
public @NotNull Boolean evaluate() {
return ContextUtils.isForegroundImportance();
}
});
private volatile long firstIdle = -1;

private final @NotNull TimeSpan appStartSpan;
private final @NotNull TimeSpan sdkInitTimeSpan;
Expand Down Expand Up @@ -89,7 +100,6 @@ public AppStartMetrics() {
applicationOnCreate = new TimeSpan();
contentProviderOnCreates = new HashMap<>();
activityLifecycles = new ArrayList<>();
appLaunchedInForeground = ContextUtils.isForegroundImportance();
}

/**
Expand Down Expand Up @@ -140,12 +150,12 @@ public void setAppStartType(final @NotNull AppStartType appStartType) {
}

public boolean isAppLaunchedInForeground() {
return appLaunchedInForeground;
return appLaunchedInForeground.getValue();
}

@VisibleForTesting
public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) {
this.appLaunchedInForeground = appLaunchedInForeground;
this.appLaunchedInForeground.setValue(appLaunchedInForeground);
}

/**
Expand Down Expand Up @@ -176,7 +186,7 @@ public void onAppStartSpansSent() {
}

public boolean shouldSendStartMeasurements() {
return shouldSendStartMeasurements && appLaunchedInForeground;
return shouldSendStartMeasurements && appLaunchedInForeground.getValue();
}

public long getClassLoadedUptimeMs() {
Expand All @@ -191,7 +201,7 @@ public long getClassLoadedUptimeMs() {
final @NotNull SentryAndroidOptions options) {
// If the app start type was never determined or app wasn't launched in foreground,
// the app start is considered invalid
if (appStartType != AppStartType.UNKNOWN && appLaunchedInForeground) {
if (appStartType != AppStartType.UNKNOWN && appLaunchedInForeground.getValue()) {
if (options.isEnablePerformanceV2()) {
// Only started when sdk version is >= N
final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan();
Expand All @@ -212,6 +222,16 @@ public long getClassLoadedUptimeMs() {
return new TimeSpan();
}

@TestOnly
void setFirstIdle(final long firstIdle) {
this.firstIdle = firstIdle;
}

@TestOnly
long getFirstIdle() {
return firstIdle;
}

@TestOnly
public void clear() {
appStartType = AppStartType.UNKNOWN;
Expand All @@ -229,11 +249,12 @@ public void clear() {
}
appStartContinuousProfiler = null;
appStartSamplingDecision = null;
appLaunchedInForeground = false;
appLaunchedInForeground.resetValue();
isCallbackRegistered = false;
shouldSendStartMeasurements = true;
firstDrawDone.set(false);
activeActivitiesCounter.set(0);
firstIdle = -1;
}

public @Nullable ITransactionProfiler getAppStartProfiler() {
Expand Down Expand Up @@ -301,7 +322,8 @@ public static void onApplicationPostCreate(final @NotNull Application applicatio
}

/**
* Register a callback to check if an activity was started after the application was created
* Register a callback to check if an activity was started after the application was created. Must
* be called from the main thread.
*
* @param application The application object to register the callback to
*/
Expand All @@ -310,61 +332,85 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
return;
}
isCallbackRegistered = true;
appLaunchedInForeground = appLaunchedInForeground || ContextUtils.isForegroundImportance();
appLaunchedInForeground.resetValue();
application.registerActivityLifecycleCallbacks(instance);
// We post on the main thread a task to post a check on the main thread. On Pixel devices
// (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());

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Looper.getMainLooper()
.getQueue()
.addIdleHandler(
new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
firstIdle = SystemClock.uptimeMillis();
checkCreateTimeOnMain();
return false;
}
});
} else {
// We post on the main thread a task to post a check on the main thread. On Pixel devices
// (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.
final Handler handler = new Handler(Looper.getMainLooper());
handler.post(
new Runnable() {
@Override
public void run() {
// not technically correct, but close enough for pre-M
firstIdle = SystemClock.uptimeMillis();
handler.post(() -> checkCreateTimeOnMain());
}
});
}
}

private void checkCreateTimeOnMain() {
new Handler(Looper.getMainLooper())
.post(
() -> {
// if no activity has ever been created, app was launched in background
if (activeActivitiesCounter.get() == 0) {
appLaunchedInForeground = false;

// we stop the app start profilers, as they are useless and likely to timeout
if (appStartProfiler != null && appStartProfiler.isRunning()) {
appStartProfiler.close();
appStartProfiler = null;
}
if (appStartContinuousProfiler != null && appStartContinuousProfiler.isRunning()) {
appStartContinuousProfiler.close(true);
appStartContinuousProfiler = null;
}
}
});
// if no activity has ever been created, app was launched in background
if (activeActivitiesCounter.get() == 0) {
appLaunchedInForeground.setValue(false);

// we stop the app start profilers, as they are useless and likely to timeout
if (appStartProfiler != null && appStartProfiler.isRunning()) {
appStartProfiler.close();
appStartProfiler = null;
}
if (appStartContinuousProfiler != null && appStartContinuousProfiler.isRunning()) {
appStartContinuousProfiler.close(true);
appStartContinuousProfiler = null;
}
}
}

@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
final long activityCreatedUptimeMillis = SystemClock.uptimeMillis();
CurrentActivityHolder.getInstance().setActivity(activity);

// the first activity determines the app start type
if (activeActivitiesCounter.incrementAndGet() == 1 && !firstDrawDone.get()) {
final long nowUptimeMs = SystemClock.uptimeMillis();

// 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
final long durationSinceAppStartMillis = nowUptimeMs - appStartSpan.getStartUptimeMs();
if (!appLaunchedInForeground || durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) {
if (!appLaunchedInForeground.getValue()
|| durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) {
appStartType = AppStartType.WARM;

shouldSendStartMeasurements = true;
appStartSpan.reset();
appStartSpan.start();
appStartSpan.setStartedAt(nowUptimeMs);
CLASS_LOADED_UPTIME_MS = nowUptimeMs;
appStartSpan.setStartedAt(activityCreatedUptimeMillis);
CLASS_LOADED_UPTIME_MS = activityCreatedUptimeMillis;
contentProviderOnCreates.clear();
applicationOnCreate.reset();
} else if (savedInstanceState != null) {
appStartType = AppStartType.WARM;
} else if (firstIdle != -1 && activityCreatedUptimeMillis > firstIdle) {
appStartType = AppStartType.WARM;
} else {
appStartType = savedInstanceState == null ? AppStartType.COLD : AppStartType.WARM;
appStartType = AppStartType.COLD;
}
}
appLaunchedInForeground = true;
appLaunchedInForeground.setValue(true);
}

@Override
Expand Down Expand Up @@ -403,9 +449,9 @@ public void onActivityDestroyed(@NonNull Activity activity) {

final int remainingActivities = activeActivitiesCounter.decrementAndGet();
// if the app is moving into background
// as the next Activity is considered like a new app start
// as the next onActivityCreated will treat it as a new warm app start
if (remainingActivities == 0 && !activity.isChangingConfigurations()) {
appLaunchedInForeground = false;
appLaunchedInForeground.setValue(true);
shouldSendStartMeasurements = true;
firstDrawDone.set(false);
}
Expand Down
Loading
Loading