Skip to content

Commit c21a34f

Browse files
authored
Merge ed06f9f into b03eb7f
2 parents b03eb7f + ed06f9f commit c21a34f

File tree

10 files changed

+575
-424
lines changed

10 files changed

+575
-424
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixes
66

77
- Fix testTag not working for Jetpack Compose user interaction tracking ([#3878](https://github.com/getsentry/sentry-java/pull/3878))
8+
- Fix warm start detection ([#3937](https://github.com/getsentry/sentry-java/pull/3937))
89

910
## 7.18.0
1011

sentry-android-core/api/sentry-android-core.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
2727
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
2828
public fun onActivityDestroyed (Landroid/app/Activity;)V
2929
public fun onActivityPaused (Landroid/app/Activity;)V
30+
public fun onActivityPostCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
3031
public fun onActivityPostResumed (Landroid/app/Activity;)V
32+
public fun onActivityPostStarted (Landroid/app/Activity;)V
33+
public fun onActivityPreCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
3134
public fun onActivityPrePaused (Landroid/app/Activity;)V
35+
public fun onActivityPreStarted (Landroid/app/Activity;)V
3236
public fun onActivityResumed (Landroid/app/Activity;)V
3337
public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
3438
public fun onActivityStarted (Landroid/app/Activity;)V
@@ -444,17 +448,21 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
444448
public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics;
445449
public fun getSdkInitTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
446450
public fun isAppLaunchedInForeground ()Z
451+
public fun isColdStartValid ()Z
447452
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
453+
public fun onAppStartSpansSent ()V
448454
public static fun onApplicationCreate (Landroid/app/Application;)V
449455
public static fun onApplicationPostCreate (Landroid/app/Application;)V
450456
public static fun onContentProviderCreate (Landroid/content/ContentProvider;)V
451457
public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V
452458
public fun registerApplicationForegroundCheck (Landroid/app/Application;)V
459+
public fun restartAppStart (J)V
453460
public fun setAppLaunchedInForeground (Z)V
454461
public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V
455462
public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V
456463
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V
457464
public fun setClassLoadedUptimeMs (J)V
465+
public fun shouldSendStartMeasurements ()Z
458466
}
459467

460468
public final class io/sentry/android/core/performance/AppStartMetrics$AppStartType : java/lang/Enum {

sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java

Lines changed: 126 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
import android.os.Bundle;
1010
import android.os.Handler;
1111
import android.os.Looper;
12-
import android.view.View;
13-
import androidx.annotation.NonNull;
12+
import android.os.SystemClock;
1413
import io.sentry.FullyDisplayedReporter;
1514
import io.sentry.IHub;
1615
import io.sentry.IScope;
@@ -29,6 +28,7 @@
2928
import io.sentry.TransactionOptions;
3029
import io.sentry.android.core.internal.util.ClassUtil;
3130
import io.sentry.android.core.internal.util.FirstDrawDoneListener;
31+
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
3232
import io.sentry.android.core.performance.AppStartMetrics;
3333
import io.sentry.android.core.performance.TimeSpan;
3434
import io.sentry.protocol.MeasurementValue;
@@ -77,8 +77,10 @@ public final class ActivityLifecycleIntegration
7777
private @Nullable ISpan appStartSpan;
7878
private final @NotNull WeakHashMap<Activity, ISpan> ttidSpanMap = new WeakHashMap<>();
7979
private final @NotNull WeakHashMap<Activity, ISpan> ttfdSpanMap = new WeakHashMap<>();
80+
private final @NotNull WeakHashMap<Activity, ActivityLifecycleTimeSpan> activityLifecycleMap =
81+
new WeakHashMap<>();
8082
private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
81-
private final @NotNull Handler mainHandler = new Handler(Looper.getMainLooper());
83+
private long lastPausedUptimeMillis = 0;
8284
private @Nullable Future<?> ttfdAutoCloseFuture = null;
8385

8486
// WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the
@@ -369,9 +371,32 @@ private void finishTransaction(
369371
}
370372
}
371373

374+
@Override
375+
public void onActivityPreCreated(
376+
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
377+
// The very first activity start timestamp cannot be set to the class instantiation time, as it
378+
// may happen before an activity is started (service, broadcast receiver, etc). So we set it
379+
// here.
380+
if (firstActivityCreated) {
381+
return;
382+
}
383+
lastPausedTime =
384+
hub != null
385+
? hub.getOptions().getDateProvider().now()
386+
: AndroidDateUtils.getCurrentSentryDateTime();
387+
lastPausedUptimeMillis = SystemClock.uptimeMillis();
388+
389+
final @NotNull ActivityLifecycleTimeSpan timeSpan = new ActivityLifecycleTimeSpan();
390+
timeSpan.getOnCreate().setStartedAt(lastPausedUptimeMillis);
391+
activityLifecycleMap.put(activity, timeSpan);
392+
}
393+
372394
@Override
373395
public synchronized void onActivityCreated(
374396
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
397+
if (!isAllActivityCallbacksAvailable) {
398+
onActivityPreCreated(activity, savedInstanceState);
399+
}
375400
setColdStart(savedInstanceState);
376401
if (hub != null && options != null && options.isEnableScreenTracking()) {
377402
final @Nullable String activityClassName = ClassUtil.getClassName(activity);
@@ -387,8 +412,39 @@ public synchronized void onActivityCreated(
387412
}
388413
}
389414

415+
@Override
416+
public void onActivityPostCreated(
417+
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
418+
if (appStartSpan == null) {
419+
activityLifecycleMap.remove(activity);
420+
return;
421+
}
422+
423+
final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity);
424+
if (timeSpan != null) {
425+
timeSpan.getOnCreate().stop();
426+
timeSpan.getOnCreate().setDescription(activity.getClass().getName() + ".onCreate");
427+
}
428+
}
429+
430+
@Override
431+
public void onActivityPreStarted(final @NotNull Activity activity) {
432+
final long now = SystemClock.uptimeMillis();
433+
if (appStartSpan == null) {
434+
return;
435+
}
436+
final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity);
437+
if (timeSpan != null) {
438+
timeSpan.getOnStart().setStartedAt(now);
439+
}
440+
}
441+
390442
@Override
391443
public synchronized void onActivityStarted(final @NotNull Activity activity) {
444+
if (!isAllActivityCallbacksAvailable) {
445+
onActivityPostCreated(activity, null);
446+
onActivityPreStarted(activity);
447+
}
392448
if (performanceEnabled) {
393449
// The docs on the screen rendering performance tracing
394450
// (https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#definition),
@@ -400,74 +456,75 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) {
400456
}
401457
}
402458

459+
@Override
460+
public void onActivityPostStarted(final @NotNull Activity activity) {
461+
final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.remove(activity);
462+
if (appStartSpan == null) {
463+
return;
464+
}
465+
if (timeSpan != null) {
466+
timeSpan.getOnStart().stop();
467+
timeSpan.getOnStart().setDescription(activity.getClass().getName() + ".onStart");
468+
AppStartMetrics.getInstance().addActivityLifecycleTimeSpans(timeSpan);
469+
}
470+
}
471+
403472
@Override
404473
public synchronized void onActivityResumed(final @NotNull Activity activity) {
474+
if (!isAllActivityCallbacksAvailable) {
475+
onActivityPostStarted(activity);
476+
}
405477
if (performanceEnabled) {
406-
407478
final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity);
408479
final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity);
409-
final View rootView = activity.findViewById(android.R.id.content);
410-
if (rootView != null) {
480+
if (activity.getWindow() != null) {
411481
FirstDrawDoneListener.registerForNextDraw(
412-
rootView, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider);
482+
activity, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider);
413483
} else {
414484
// Posting a task to the main thread's handler will make it executed after it finished
415485
// its current job. That is, right after the activity draws the layout.
416-
mainHandler.post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan));
486+
new Handler(Looper.getMainLooper()).post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan));
417487
}
418488
}
419489
}
420490

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

426496
@Override
427-
public void onActivityPrePaused(@NonNull Activity activity) {
497+
public void onActivityPrePaused(@NotNull Activity activity) {
428498
// only executed if API >= 29 otherwise it happens on onActivityPaused
429-
if (isAllActivityCallbacksAvailable) {
430-
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
431-
// well
432-
// this ensures any newly launched activity will not use the app start timestamp as txn start
433-
firstActivityCreated = true;
434-
if (hub == null) {
435-
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
436-
} else {
437-
lastPausedTime = hub.getOptions().getDateProvider().now();
438-
}
439-
}
499+
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
500+
// well
501+
// this ensures any newly launched activity will not use the app start timestamp as txn start
502+
firstActivityCreated = true;
503+
lastPausedTime =
504+
hub != null
505+
? hub.getOptions().getDateProvider().now()
506+
: AndroidDateUtils.getCurrentSentryDateTime();
507+
lastPausedUptimeMillis = SystemClock.uptimeMillis();
440508
}
441509

442510
@Override
443511
public synchronized void onActivityPaused(final @NotNull Activity activity) {
444512
// only executed if API < 29 otherwise it happens on onActivityPrePaused
445513
if (!isAllActivityCallbacksAvailable) {
446-
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
447-
// well
448-
// this ensures any newly launched activity will not use the app start timestamp as txn start
449-
firstActivityCreated = true;
450-
if (hub == null) {
451-
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
452-
} else {
453-
lastPausedTime = hub.getOptions().getDateProvider().now();
454-
}
514+
onActivityPrePaused(activity);
455515
}
456516
}
457517

458518
@Override
459-
public synchronized void onActivityStopped(final @NotNull Activity activity) {
460-
// no-op
461-
}
519+
public void onActivityStopped(final @NotNull Activity activity) {}
462520

463521
@Override
464-
public synchronized void onActivitySaveInstanceState(
465-
final @NotNull Activity activity, final @NotNull Bundle outState) {
466-
// no-op
467-
}
522+
public void onActivitySaveInstanceState(
523+
final @NotNull Activity activity, final @NotNull Bundle outState) {}
468524

469525
@Override
470526
public synchronized void onActivityDestroyed(final @NotNull Activity activity) {
527+
activityLifecycleMap.remove(activity);
471528
if (performanceEnabled) {
472529

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

496553
// clear it up, so we don't start again for the same activity if the activity is in the
497-
// activity
498-
// stack still.
554+
// activity stack still.
499555
// if the activity is opened again and not in memory, transactions will be created normally.
500556
activitiesWithOngoingTransactions.remove(activity);
557+
558+
if (activitiesWithOngoingTransactions.isEmpty()) {
559+
clear();
560+
}
561+
}
562+
563+
private void clear() {
564+
firstActivityCreated = false;
565+
lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
566+
lastPausedUptimeMillis = 0;
567+
activityLifecycleMap.clear();
501568
}
502569

503570
private void finishSpan(final @Nullable ISpan span) {
@@ -604,6 +671,16 @@ WeakHashMap<Activity, ITransaction> getActivitiesWithOngoingTransactions() {
604671
return activitiesWithOngoingTransactions;
605672
}
606673

674+
@TestOnly
675+
@NotNull WeakHashMap<Activity, ActivityLifecycleTimeSpan> getActivityLifecycleMap() {
676+
return activityLifecycleMap;
677+
}
678+
679+
@TestOnly
680+
void setFirstActivityCreated(boolean firstActivityCreated) {
681+
this.firstActivityCreated = firstActivityCreated;
682+
}
683+
607684
@TestOnly
608685
@NotNull
609686
ActivityFramesTracker getActivityFramesTracker() {
@@ -629,20 +706,17 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() {
629706
}
630707

631708
private void setColdStart(final @Nullable Bundle savedInstanceState) {
632-
// The very first activity start timestamp cannot be set to the class instantiation time, as it
633-
// may happen before an activity is started (service, broadcast receiver, etc). So we set it
634-
// here.
635-
if (hub != null && lastPausedTime.nanoTimestamp() == 0) {
636-
lastPausedTime = hub.getOptions().getDateProvider().now();
637-
} else if (lastPausedTime.nanoTimestamp() == 0) {
638-
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
639-
}
640709
if (!firstActivityCreated) {
641-
// if Activity has savedInstanceState then its a warm start
642-
// https://developer.android.com/topic/performance/vitals/launch-time#warm
643-
// SentryPerformanceProvider sets this already
644-
// pre-performance-v2: back-fill with best guess
645-
if (options != null && !options.isEnablePerformanceV2()) {
710+
final @NotNull TimeSpan appStartSpan = AppStartMetrics.getInstance().getAppStartTimeSpan();
711+
// If the app start span already started and stopped, it means the app restarted without
712+
// killing the process, so we are in a warm start
713+
// If the app has an invalid cold start, it means it was started in the background, like
714+
// via BroadcastReceiver, so we consider it a warm start
715+
if ((appStartSpan.hasStarted() && appStartSpan.hasStopped())
716+
|| (!AppStartMetrics.getInstance().isColdStartValid())) {
717+
AppStartMetrics.getInstance().restartAppStart(lastPausedUptimeMillis);
718+
AppStartMetrics.getInstance().setAppStartType(AppStartMetrics.AppStartType.WARM);
719+
} else {
646720
AppStartMetrics.getInstance()
647721
.setAppStartType(
648722
savedInstanceState == null

0 commit comments

Comments
 (0)