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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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))
- Report discarded log in batch processor as `log_byte` ([#4971](https://github.com/getsentry/sentry-java/pull/4971))
- Fix warm app start type detection for edge cases ([#4999](https://github.com/getsentry/sentry-java/pull/4999))
Copy link
Contributor

Choose a reason for hiding this comment

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

  • 🚫 The changelog entry seems to be part of an already released section ## 8.30.0.
    Consider moving the entry to the ## Unreleased section, please.


### Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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 +57,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 firstPostUptimeMillis = -1;

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

/**
Expand Down Expand Up @@ -140,12 +148,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 +184,7 @@ public void onAppStartSpansSent() {
}

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

public long getClassLoadedUptimeMs() {
Expand All @@ -191,7 +199,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 +220,16 @@ public long getClassLoadedUptimeMs() {
return new TimeSpan();
}

@TestOnly
void setFirstPostUptimeMillis(final long firstPostUptimeMillis) {
this.firstPostUptimeMillis = firstPostUptimeMillis;
}

@TestOnly
long getFirstPostUptimeMillis() {
return firstPostUptimeMillis;
}

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

public @Nullable ITransactionProfiler getAppStartProfiler() {
Expand Down Expand Up @@ -310,13 +329,21 @@ 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());
new Handler(Looper.getMainLooper())
.post(
new Runnable() {
@Override
public void run() {
firstPostUptimeMillis = SystemClock.uptimeMillis();
checkCreateTimeOnMain();
}
});
}

private void checkCreateTimeOnMain() {
Expand All @@ -325,7 +352,7 @@ private void checkCreateTimeOnMain() {
() -> {
// if no activity has ever been created, app was launched in background
if (activeActivitiesCounter.get() == 0) {
appLaunchedInForeground = false;
appLaunchedInForeground.setValue(false);

// we stop the app start profilers, as they are useless and likely to timeout
if (appStartProfiler != null && appStartProfiler.isRunning()) {
Expand All @@ -342,29 +369,36 @@ private void checkCreateTimeOnMain() {

@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 (firstPostUptimeMillis != -1
&& activityCreatedUptimeMillis > firstPostUptimeMillis) {
// Application creation always queues Activity creation
// So if Activity is created after our first measured post, it's a warm start
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 +437,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