Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixes

- Fix warm start detection ([#3937](https://github.com/getsentry/sentry-java/pull/3937))

## 7.18.1

### Fixes
Expand Down
24 changes: 24 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityDestroyed (Landroid/app/Activity;)V
public fun onActivityPaused (Landroid/app/Activity;)V
public fun onActivityPostCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityPostResumed (Landroid/app/Activity;)V
public fun onActivityPostStarted (Landroid/app/Activity;)V
public fun onActivityPreCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityPrePaused (Landroid/app/Activity;)V
public fun onActivityPreStarted (Landroid/app/Activity;)V
public fun onActivityResumed (Landroid/app/Activity;)V
public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityStarted (Landroid/app/Activity;)V
Expand Down Expand Up @@ -420,6 +424,20 @@ public class io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapte
public fun onActivityStopped (Landroid/app/Activity;)V
}

public class io/sentry/android/core/performance/ActivityLifecycleSpanHelper {
public fun <init> (Ljava/lang/String;)V
public fun clear ()V
public fun createAndStopOnCreateSpan (Lio/sentry/ISpan;)V
public fun createAndStopOnStartSpan (Lio/sentry/ISpan;)V
public fun getOnCreateSpan ()Lio/sentry/ISpan;
public fun getOnCreateStartTimestamp ()Lio/sentry/SentryDate;
public fun getOnStartSpan ()Lio/sentry/ISpan;
public fun getOnStartStartTimestamp ()Lio/sentry/SentryDate;
public fun saveSpanToAppStartMetrics ()V
public fun setOnCreateStartTimestamp (Lio/sentry/SentryDate;)V
public fun setOnStartStartTimestamp (Lio/sentry/SentryDate;)V
}

public class io/sentry/android/core/performance/ActivityLifecycleTimeSpan : java/lang/Comparable {
public fun <init> ()V
public fun compareTo (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)I
Expand All @@ -432,6 +450,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public fun <init> ()V
public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V
public fun clear ()V
public fun createProcessInitSpan ()Lio/sentry/android/core/performance/TimeSpan;
public fun getActivityLifecycleTimeSpans ()Ljava/util/List;
public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler;
public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision;
Expand All @@ -444,17 +463,21 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics;
public fun getSdkInitTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
public fun isAppLaunchedInForeground ()Z
public fun isColdStartValid ()Z
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onAppStartSpansSent ()V
public static fun onApplicationCreate (Landroid/app/Application;)V
public static fun onApplicationPostCreate (Landroid/app/Application;)V
public static fun onContentProviderCreate (Landroid/content/ContentProvider;)V
public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V
public fun registerApplicationForegroundCheck (Landroid/app/Application;)V
public fun restartAppStart (J)V
public fun setAppLaunchedInForeground (Z)V
public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V
public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V
public fun setClassLoadedUptimeMs (J)V
public fun shouldSendStartMeasurements ()Z
}

public final class io/sentry/android/core/performance/AppStartMetrics$AppStartType : java/lang/Enum {
Expand Down Expand Up @@ -487,6 +510,7 @@ public class io/sentry/android/core/performance/TimeSpan : java/lang/Comparable
public fun setStartUnixTimeMs (J)V
public fun setStartedAt (J)V
public fun setStoppedAt (J)V
public fun setup (Ljava/lang/String;JJJ)V
public fun start ()V
public fun stop ()V
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import androidx.annotation.NonNull;
import android.os.SystemClock;
import io.sentry.FullyDisplayedReporter;
import io.sentry.IHub;
import io.sentry.IScope;
Expand All @@ -29,6 +28,7 @@
import io.sentry.TransactionOptions;
import io.sentry.android.core.internal.util.ClassUtil;
import io.sentry.android.core.internal.util.FirstDrawDoneListener;
import io.sentry.android.core.performance.ActivityLifecycleSpanHelper;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.protocol.MeasurementValue;
Expand Down Expand Up @@ -77,8 +77,10 @@ public final class ActivityLifecycleIntegration
private @Nullable ISpan appStartSpan;
private final @NotNull WeakHashMap<Activity, ISpan> ttidSpanMap = new WeakHashMap<>();
private final @NotNull WeakHashMap<Activity, ISpan> ttfdSpanMap = new WeakHashMap<>();
private final @NotNull WeakHashMap<Activity, ActivityLifecycleSpanHelper> activitySpanHelpers =
new WeakHashMap<>();
private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
private final @NotNull Handler mainHandler = new Handler(Looper.getMainLooper());
private long lastPausedUptimeMillis = 0;
private @Nullable Future<?> ttfdAutoCloseFuture = null;

// WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the
Expand Down Expand Up @@ -369,9 +371,32 @@ private void finishTransaction(
}
}

@Override
public void onActivityPreCreated(
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
final ActivityLifecycleSpanHelper helper =
new ActivityLifecycleSpanHelper(activity.getClass().getName());
activitySpanHelpers.put(activity, helper);
// The very first activity start timestamp cannot be set to the class instantiation time, as it
// may happen before an activity is started (service, broadcast receiver, etc). So we set it
// here.
if (firstActivityCreated) {
return;
}
lastPausedTime =
hub != null
? hub.getOptions().getDateProvider().now()
: AndroidDateUtils.getCurrentSentryDateTime();
lastPausedUptimeMillis = SystemClock.uptimeMillis();
helper.setOnCreateStartTimestamp(lastPausedTime);
}

@Override
public synchronized void onActivityCreated(
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
if (!isAllActivityCallbacksAvailable) {
onActivityPreCreated(activity, savedInstanceState);
}
setColdStart(savedInstanceState);
if (hub != null && options != null && options.isEnableScreenTracking()) {
final @Nullable String activityClassName = ClassUtil.getClassName(activity);
Expand All @@ -387,8 +412,32 @@ public synchronized void onActivityCreated(
}
}

@Override
public void onActivityPostCreated(
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity);
if (helper != null) {
helper.createAndStopOnCreateSpan(appStartSpan);
}
}

@Override
public void onActivityPreStarted(final @NotNull Activity activity) {
final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity);
if (helper != null) {
helper.setOnStartStartTimestamp(
options != null
? options.getDateProvider().now()
: AndroidDateUtils.getCurrentSentryDateTime());
}
}

@Override
public synchronized void onActivityStarted(final @NotNull Activity activity) {
if (!isAllActivityCallbacksAvailable) {
onActivityPostCreated(activity, null);
onActivityPreStarted(activity);
}
if (performanceEnabled) {
// The docs on the screen rendering performance tracing
// (https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#definition),
Expand All @@ -400,74 +449,75 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) {
}
}

@Override
public void onActivityPostStarted(final @NotNull Activity activity) {
final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity);
if (helper != null) {
helper.createAndStopOnStartSpan(appStartSpan);
// Needed to handle hybrid SDKs
helper.saveSpanToAppStartMetrics();
}
}

@Override
public synchronized void onActivityResumed(final @NotNull Activity activity) {
if (!isAllActivityCallbacksAvailable) {
onActivityPostStarted(activity);
}
if (performanceEnabled) {

final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity);
final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity);
final View rootView = activity.findViewById(android.R.id.content);
if (rootView != null) {
if (activity.getWindow() != null) {
FirstDrawDoneListener.registerForNextDraw(
rootView, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider);
activity, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider);
} else {
// Posting a task to the main thread's handler will make it executed after it finished
// its current job. That is, right after the activity draws the layout.
mainHandler.post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan));
new Handler(Looper.getMainLooper()).post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan));
}
}
}

@Override
public void onActivityPostResumed(@NonNull Activity activity) {
public void onActivityPostResumed(@NotNull Activity activity) {
// empty override, required to avoid a api-level breaking super.onActivityPostResumed() calls
}

@Override
public void onActivityPrePaused(@NonNull Activity activity) {
public void onActivityPrePaused(@NotNull Activity activity) {
// only executed if API >= 29 otherwise it happens on onActivityPaused
if (isAllActivityCallbacksAvailable) {
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
// well
// this ensures any newly launched activity will not use the app start timestamp as txn start
firstActivityCreated = true;
if (hub == null) {
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
} else {
lastPausedTime = hub.getOptions().getDateProvider().now();
}
}
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
// well
// this ensures any newly launched activity will not use the app start timestamp as txn start
firstActivityCreated = true;
lastPausedTime =
hub != null
? hub.getOptions().getDateProvider().now()
: AndroidDateUtils.getCurrentSentryDateTime();
lastPausedUptimeMillis = SystemClock.uptimeMillis();
}

@Override
public synchronized void onActivityPaused(final @NotNull Activity activity) {
// only executed if API < 29 otherwise it happens on onActivityPrePaused
if (!isAllActivityCallbacksAvailable) {
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
// well
// this ensures any newly launched activity will not use the app start timestamp as txn start
firstActivityCreated = true;
if (hub == null) {
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
} else {
lastPausedTime = hub.getOptions().getDateProvider().now();
}
onActivityPrePaused(activity);
}
}

@Override
public synchronized void onActivityStopped(final @NotNull Activity activity) {
// no-op
}
public void onActivityStopped(final @NotNull Activity activity) {}

@Override
public synchronized void onActivitySaveInstanceState(
final @NotNull Activity activity, final @NotNull Bundle outState) {
// no-op
}
public void onActivitySaveInstanceState(
final @NotNull Activity activity, final @NotNull Bundle outState) {}

@Override
public synchronized void onActivityDestroyed(final @NotNull Activity activity) {
final ActivityLifecycleSpanHelper helper = activitySpanHelpers.remove(activity);
if (helper != null) {
helper.clear();
}
if (performanceEnabled) {

// in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid
Expand All @@ -494,10 +544,20 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) {
}

// clear it up, so we don't start again for the same activity if the activity is in the
// activity
// stack still.
// activity stack still.
// if the activity is opened again and not in memory, transactions will be created normally.
activitiesWithOngoingTransactions.remove(activity);

if (activitiesWithOngoingTransactions.isEmpty()) {
clear();
}
}

private void clear() {
firstActivityCreated = false;
lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
lastPausedUptimeMillis = 0;
activitySpanHelpers.clear();
}

private void finishSpan(final @Nullable ISpan span) {
Expand Down Expand Up @@ -542,8 +602,7 @@ private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable I
final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan();
final @NotNull TimeSpan sdkInitTimeSpan = appStartMetrics.getSdkInitTimeSpan();

// in case the SentryPerformanceProvider is disabled it does not set the app start end times,
// and we need to set the end time manually here
// and we need to set the end time of the app start here, after the first frame is drawn.
if (appStartTimeSpan.hasStarted() && appStartTimeSpan.hasNotStopped()) {
appStartTimeSpan.stop();
}
Expand Down Expand Up @@ -604,6 +663,17 @@ WeakHashMap<Activity, ITransaction> getActivitiesWithOngoingTransactions() {
return activitiesWithOngoingTransactions;
}

@TestOnly
@NotNull
WeakHashMap<Activity, ActivityLifecycleSpanHelper> getActivitySpanHelpers() {
return activitySpanHelpers;
}

@TestOnly
void setFirstActivityCreated(boolean firstActivityCreated) {
this.firstActivityCreated = firstActivityCreated;
}

@TestOnly
@NotNull
ActivityFramesTracker getActivityFramesTracker() {
Expand All @@ -629,20 +699,17 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() {
}

private void setColdStart(final @Nullable Bundle savedInstanceState) {
// The very first activity start timestamp cannot be set to the class instantiation time, as it
// may happen before an activity is started (service, broadcast receiver, etc). So we set it
// here.
if (hub != null && lastPausedTime.nanoTimestamp() == 0) {
lastPausedTime = hub.getOptions().getDateProvider().now();
} else if (lastPausedTime.nanoTimestamp() == 0) {
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
}
if (!firstActivityCreated) {
// if Activity has savedInstanceState then its a warm start
// https://developer.android.com/topic/performance/vitals/launch-time#warm
// SentryPerformanceProvider sets this already
// pre-performance-v2: back-fill with best guess
if (options != null && !options.isEnablePerformanceV2()) {
final @NotNull TimeSpan appStartSpan = AppStartMetrics.getInstance().getAppStartTimeSpan();
// If the app start span already started and stopped, it means the app restarted without
// killing the process, so we are in a warm start
// If the app has an invalid cold start, it means it was started in the background, like
// via BroadcastReceiver, so we consider it a warm start
if ((appStartSpan.hasStarted() && appStartSpan.hasStopped())
|| (!AppStartMetrics.getInstance().isColdStartValid())) {
AppStartMetrics.getInstance().restartAppStart(lastPausedUptimeMillis);
AppStartMetrics.getInstance().setAppStartType(AppStartMetrics.AppStartType.WARM);
} else {
AppStartMetrics.getInstance()
.setAppStartType(
savedInstanceState == null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,7 @@ public static Map<String, Object> getAppStartMeasurement() {
final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
final @NotNull List<Map<String, Object>> spans = new ArrayList<>();

final @NotNull TimeSpan processInitNativeSpan = new TimeSpan();
processInitNativeSpan.setStartedAt(metrics.getAppStartTimeSpan().getStartUptimeMs());
processInitNativeSpan.setStartUnixTimeMs(
metrics.getAppStartTimeSpan().getStartTimestampMs()); // This has to go after setStartedAt
processInitNativeSpan.setStoppedAt(metrics.getClassLoadedUptimeMs());
processInitNativeSpan.setDescription("Process Initialization");

addTimeSpanToSerializedSpans(processInitNativeSpan, spans);
addTimeSpanToSerializedSpans(metrics.createProcessInitSpan(), spans);
addTimeSpanToSerializedSpans(metrics.getApplicationOnCreateTimeSpan(), spans);

for (final TimeSpan span : metrics.getContentProviderOnCreateTimeSpans()) {
Expand Down
Loading
Loading