From d59ca3f6ef5551fbcf80946c8a52be86f64c8b8d Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 21 Nov 2023 10:03:49 +0100 Subject: [PATCH 01/15] Attach app-start spans --- .../api/sentry-android-core.api | 111 +++++++-- .../core/ActivityLifecycleIntegration.java | 56 +++-- .../io/sentry/android/core/AppStartState.java | 129 ---------- .../io/sentry/android/core/ContextUtils.java | 2 +- .../core/DefaultAndroidEventProcessor.java | 10 +- .../android/core/InternalSentrySdk.java | 11 +- .../android/core/ManifestMetadataReader.java | 5 + .../PerformanceAndroidEventProcessor.java | 165 ++++++++++++- .../io/sentry/android/core/SentryAndroid.java | 28 ++- .../android/core/SentryAndroidOptions.java | 12 + .../core/SentryPerformanceProvider.java | 225 ++++++++++++------ .../core/cache/AndroidEnvelopeCache.java | 14 +- .../gestures/WindowCallbackAdapter.java | 4 +- .../ActivityLifecycleCallbacksAdapter.java | 31 +++ .../ActivityLifecycleTimeSpan.java | 15 ++ .../core/performance/AppStartMetrics.java | 183 ++++++++++++++ .../core/performance/NextDrawListener.java | 138 +++++++++++ .../android/core/performance/TimeSpan.java | 164 +++++++++++++ .../WindowContentChangedCallback.java | 22 ++ .../core/ActivityLifecycleIntegrationTest.kt | 87 ++++--- .../sentry/android/core/AppStartStateTest.kt | 94 -------- .../core/ManifestMetadataReaderTest.kt | 25 ++ .../PerformanceAndroidEventProcessorTest.kt | 155 ++++++++++-- .../android/core/SentryAndroidOptionsTest.kt | 13 + .../sentry/android/core/SentryAndroidTest.kt | 16 +- .../android/core/SentryLogcatAdapterTest.kt | 3 +- .../core/SentryPerformanceProviderTest.kt | 119 +++++---- .../core/cache/AndroidEnvelopeCacheTest.kt | 9 +- .../ActivityLifecycleTimeSpanTest.kt | 37 +++ .../android/core/performance/TimeSpanTest.kt | 154 ++++++++++++ .../src/main/AndroidManifest.xml | 2 + 31 files changed, 1560 insertions(+), 479 deletions(-) delete mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapter.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpan.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/performance/NextDrawListener.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/performance/WindowContentChangedCallback.java delete mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpanTest.kt create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index d6253b3f02..f5511b996c 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -115,17 +115,6 @@ public final class io/sentry/android/core/AppLifecycleIntegration : io/sentry/In public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } -public final class io/sentry/android/core/AppStartState { - public fun getAppStartEndTime ()Lio/sentry/SentryDate; - public fun getAppStartInterval ()Ljava/lang/Long; - public fun getAppStartMillis ()Ljava/lang/Long; - public fun getAppStartTime ()Lio/sentry/SentryDate; - public static fun getInstance ()Lio/sentry/android/core/AppStartState; - public fun isColdStart ()Ljava/lang/Boolean; - public fun reset ()V - public fun setAppStartMillis (J)V -} - public final class io/sentry/android/core/AppState { public static fun getInstance ()Lio/sentry/android/core/AppState; public fun isInBackground ()Ljava/lang/Boolean; @@ -151,6 +140,7 @@ public final class io/sentry/android/core/BuildInfoProvider { } public final class io/sentry/android/core/ContextUtils { + public static fun isForegroundImportance (Landroid/content/Context;)Z } public class io/sentry/android/core/CurrentActivityHolder { @@ -269,6 +259,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableFramesTracking ()Z public fun isEnableNetworkEventBreadcrumbs ()Z public fun isEnableRootCheck ()Z + public fun isEnableStarfish ()Z public fun isEnableSystemEventBreadcrumbs ()Z public fun isReportHistoricalAnrs ()Z public fun setAnrEnabled (Z)V @@ -289,6 +280,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableFramesTracking (Z)V public fun setEnableNetworkEventBreadcrumbs (Z)V public fun setEnableRootCheck (Z)V + public fun setEnableStarfish (Z)V public fun setEnableSystemEventBreadcrumbs (Z)V public fun setNativeSdkName (Ljava/lang/String;)V public fun setProfilingTracesHz (I)V @@ -326,17 +318,12 @@ public final class io/sentry/android/core/SentryLogcatAdapter { public static fun wtf (Ljava/lang/String;Ljava/lang/Throwable;)I } -public final class io/sentry/android/core/SentryPerformanceProvider : android/app/Application$ActivityLifecycleCallbacks { +public final class io/sentry/android/core/SentryPerformanceProvider { public fun ()V public fun attachInfo (Landroid/content/Context;Landroid/content/pm/ProviderInfo;)V + public fun getActivityCallback ()Landroid/app/Application$ActivityLifecycleCallbacks; public fun getType (Landroid/net/Uri;)Ljava/lang/String; - 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 onActivityResumed (Landroid/app/Activity;)V - public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V - public fun onActivityStarted (Landroid/app/Activity;)V - public fun onActivityStopped (Landroid/app/Activity;)V + public fun onAppLaunched ()V public fun onCreate ()Z } @@ -387,3 +374,89 @@ public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } +public class io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapter : android/app/Application$ActivityLifecycleCallbacks { + public fun ()V + 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 onActivityResumed (Landroid/app/Activity;)V + public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V + public fun onActivityStarted (Landroid/app/Activity;)V + public fun onActivityStopped (Landroid/app/Activity;)V +} + +public class io/sentry/android/core/performance/ActivityLifecycleTimeSpan : java/lang/Comparable { + public final field onCreate Lio/sentry/android/core/performance/TimeSpan; + public final field onStart Lio/sentry/android/core/performance/TimeSpan; + public fun ()V + public fun compareTo (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)I + public synthetic fun compareTo (Ljava/lang/Object;)I +} + +public class io/sentry/android/core/performance/AppStartMetrics { + public fun ()V + public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V + public fun clear ()V + public fun getActivityLifecycleTimeSpans ()Ljava/util/List; + public fun getAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; + public fun getAppStartType ()Lio/sentry/android/core/performance/AppStartMetrics$AppStartType; + public fun getApplicationOnCreateTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; + public fun getContentProviderOnCreateTimeSpans ()Ljava/util/List; + public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics; + public fun getLegacyAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; + public fun isAppLaunchedInForeground ()Z + 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 setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V +} + +public final class io/sentry/android/core/performance/AppStartMetrics$AppStartType : java/lang/Enum { + public static final field COLD Lio/sentry/android/core/performance/AppStartMetrics$AppStartType; + public static final field UNKNOWN Lio/sentry/android/core/performance/AppStartMetrics$AppStartType; + public static final field WARM Lio/sentry/android/core/performance/AppStartMetrics$AppStartType; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/android/core/performance/AppStartMetrics$AppStartType; + public static fun values ()[Lio/sentry/android/core/performance/AppStartMetrics$AppStartType; +} + +public class io/sentry/android/core/performance/NextDrawListener : android/view/View$OnAttachStateChangeListener, android/view/ViewTreeObserver$OnDrawListener { + protected fun (Landroid/os/Handler;Ljava/lang/Runnable;)V + public static fun forActivity (Landroid/app/Activity;Ljava/lang/Runnable;)Lio/sentry/android/core/performance/NextDrawListener; + public fun onDraw ()V + public fun onViewAttachedToWindow (Landroid/view/View;)V + public fun onViewDetachedFromWindow (Landroid/view/View;)V + public fun unregister ()V +} + +public class io/sentry/android/core/performance/TimeSpan : java/lang/Comparable { + public fun ()V + public fun compareTo (Lio/sentry/android/core/performance/TimeSpan;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public fun getDescription ()Ljava/lang/String; + public fun getDurationMs ()J + public fun getProjectedStopTimestamp ()Lio/sentry/SentryDate; + public fun getProjectedStopTimestampMs ()J + public fun getProjectedStopTimestampS ()D + public fun getStartTimestamp ()Lio/sentry/SentryDate; + public fun getStartTimestampMs ()J + public fun getStartTimestampS ()D + public fun getStartUptimeMs ()J + public fun hasNotStarted ()Z + public fun hasNotStopped ()Z + public fun hasStarted ()Z + public fun hasStopped ()Z + public fun reset ()V + public fun setDescription (Ljava/lang/String;)V + public fun setStartUnixTimeMs (J)V + public fun setStartedAt (J)V + public fun setStoppedAt (J)V + public fun start ()V + public fun stop ()V +} + +public class io/sentry/android/core/performance/WindowContentChangedCallback : io/sentry/android/core/internal/gestures/WindowCallbackAdapter { + public fun (Landroid/view/Window$Callback;Ljava/lang/Runnable;)V + public fun onContentChanged ()V +} + diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 12eea7922a..c7ed4ee9b7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -30,6 +30,8 @@ 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.AppStartMetrics; +import io.sentry.android.core.performance.TimeSpan; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.TransactionNameSource; import io.sentry.util.Objects; @@ -70,7 +72,6 @@ public final class ActivityLifecycleIntegration private boolean isAllActivityCallbacksAvailable; private boolean firstActivityCreated = false; - private final boolean foregroundImportance; private @Nullable FullyDisplayedReporter fullyDisplayedReporter = null; private @Nullable ISpan appStartSpan; @@ -100,10 +101,6 @@ public ActivityLifecycleIntegration( if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.Q) { isAllActivityCallbacksAvailable = true; } - - // we only track app start for processes that will show an Activity (full launch). - // Here we check the process importance which will tell us that. - foregroundImportance = ContextUtils.isForegroundImportance(this.application); } @Override @@ -182,15 +179,27 @@ private void startTracing(final @NotNull Activity activity) { if (!performanceEnabled) { activitiesWithOngoingTransactions.put(activity, NoOpTransaction.getInstance()); TracingUtils.startNewTrace(hub); - } else if (performanceEnabled) { + } else { // as we allow a single transaction running on the bound Scope, we finish the previous ones stopPreviousTransactions(); final String activityName = getActivityName(activity); - final SentryDate appStartTime = - foregroundImportance ? AppStartState.getInstance().getAppStartTime() : null; - final Boolean coldStart = AppStartState.getInstance().isColdStart(); + final @Nullable SentryDate appStartTime; + final @Nullable Boolean coldStart; + final TimeSpan appStartTimeSpan = getAppStartTimeSpan(); + + // we only track app start for processes that will show an Activity (full launch). + // Here we check the process importance which will tell us that. + final boolean foregroundImportance = ContextUtils.isForegroundImportance(this.application); + if (foregroundImportance && appStartTimeSpan.hasStarted()) { + appStartTime = appStartTimeSpan.getStartTimestamp(); + coldStart = + AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD; + } else { + appStartTime = null; + coldStart = null; + } final TransactionOptions transactionOptions = new TransactionOptions(); if (options.isEnableActivityLifecycleTracingAutoFinish()) { @@ -406,13 +415,13 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) { public synchronized void onActivityResumed(final @NotNull Activity activity) { if (performanceEnabled) { // app start span - @Nullable final SentryDate appStartStartTime = AppStartState.getInstance().getAppStartTime(); - @Nullable final SentryDate appStartEndTime = AppStartState.getInstance().getAppStartEndTime(); + final @NotNull TimeSpan appStartTimeSpan = getAppStartTimeSpan(); + // in case the SentryPerformanceProvider is disabled it does not set the app start times, // and we need to set the end time manually here, // the start time gets set manually in SentryAndroid.init() - if (appStartStartTime != null && appStartEndTime == null) { - AppStartState.getInstance().setAppStartEnd(); + if (appStartTimeSpan.hasStarted() && appStartTimeSpan.hasNotStopped()) { + appStartTimeSpan.stop(); } finishAppStartSpan(); @@ -625,7 +634,15 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) { if (!firstActivityCreated) { // if Activity has savedInstanceState then its a warm start // https://developer.android.com/topic/performance/vitals/launch-time#warm - AppStartState.getInstance().setColdStart(savedInstanceState == null); + // SentryPerformanceProvider sets this already + // pre-starfish: back-fill with best guess + if (options != null && !options.isEnableStarfish()) { + AppStartMetrics.getInstance() + .setAppStartType( + savedInstanceState == null + ? AppStartMetrics.AppStartType.COLD + : AppStartMetrics.AppStartType.WARM); + } } } @@ -661,9 +678,18 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) { } private void finishAppStartSpan() { - final @Nullable SentryDate appStartEndTime = AppStartState.getInstance().getAppStartEndTime(); + final @NotNull TimeSpan appStartTimeSpan = getAppStartTimeSpan(); + final @Nullable SentryDate appStartEndTime = appStartTimeSpan.getProjectedStopTimestamp(); if (performanceEnabled && appStartEndTime != null) { finishSpan(appStartSpan, appStartEndTime); } } + + private @NotNull TimeSpan getAppStartTimeSpan() { + final @NotNull TimeSpan appStartTimeSpan = + options.isEnableStarfish() + ? AppStartMetrics.getInstance().getAppStartTimeSpan() + : AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + return appStartTimeSpan; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java deleted file mode 100644 index de690aa668..0000000000 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java +++ /dev/null @@ -1,129 +0,0 @@ -package io.sentry.android.core; - -import android.os.SystemClock; -import io.sentry.DateUtils; -import io.sentry.SentryDate; -import io.sentry.SentryLongDate; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; - -/** AppStartState holds the state of the App Start metric and appStartTime */ -@ApiStatus.Internal -public final class AppStartState { - - private static @NotNull AppStartState instance = new AppStartState(); - - /** We filter out App starts more than 60s */ - private static final int MAX_APP_START_MILLIS = 60000; - - private @Nullable Long appStartMillis; - - private @Nullable Long appStartEndMillis; - - /** The type of App start coldStart=true -> Cold start, coldStart=false -> Warm start */ - private @Nullable Boolean coldStart = null; - - /** appStart as a Date used in the App's Context */ - private @Nullable SentryDate appStartTime; - - private AppStartState() {} - - public static @NotNull AppStartState getInstance() { - return instance; - } - - @TestOnly - void resetInstance() { - instance = new AppStartState(); - } - - synchronized void setAppStartEnd() { - setAppStartEnd(SystemClock.uptimeMillis()); - } - - @TestOnly - void setAppStartEnd(final long appStartEndMillis) { - this.appStartEndMillis = appStartEndMillis; - } - - @Nullable - public synchronized Long getAppStartInterval() { - if (appStartMillis == null || appStartEndMillis == null || coldStart == null) { - return null; - } - final long appStart = appStartEndMillis - appStartMillis; - - // We filter out app start more than 60s. - // This could be due to many different reasons. - // If you do the manual init and init the SDK too late and it does not compute the app start end - // in the very first Activity. - // If the process starts but the App isn't in the foreground. - // If the system forked the zygote earlier to accelerate the app start. - // And some unknown reasons that could not be reproduced. - // We've seen app starts with hours, days and even months. - if (appStart >= MAX_APP_START_MILLIS) { - return null; - } - - return appStart; - } - - public @Nullable Boolean isColdStart() { - return coldStart; - } - - synchronized void setColdStart(final boolean coldStart) { - if (this.coldStart != null) { - return; - } - this.coldStart = coldStart; - } - - @Nullable - public SentryDate getAppStartTime() { - return appStartTime; - } - - @Nullable - public SentryDate getAppStartEndTime() { - @Nullable final SentryDate start = getAppStartTime(); - if (start != null) { - @Nullable final Long durationMillis = getAppStartInterval(); - if (durationMillis != null) { - final long startNanos = start.nanoTimestamp(); - final long endNanos = startNanos + DateUtils.millisToNanos(durationMillis); - return new SentryLongDate(endNanos); - } - } - return null; - } - - @Nullable - public Long getAppStartMillis() { - return appStartMillis; - } - - synchronized void setAppStartTime( - final long appStartMillis, final @NotNull SentryDate appStartTime) { - // method is synchronized because the SDK may by init. on a background thread. - if (this.appStartTime != null && this.appStartMillis != null) { - return; - } - this.appStartTime = appStartTime; - this.appStartMillis = appStartMillis; - } - - @TestOnly - public synchronized void setAppStartMillis(final long appStartMillis) { - this.appStartMillis = appStartMillis; - } - - @TestOnly - public synchronized void reset() { - appStartTime = null; - appStartMillis = null; - appStartEndMillis = null; - } -} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java index 25d4cb0536..f4d8359465 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java @@ -169,7 +169,7 @@ static String getVersionName(final @NotNull PackageInfo packageInfo) { * * @return true if IMPORTANCE_FOREGROUND and false otherwise */ - static boolean isForegroundImportance(final @NotNull Context context) { + public static boolean isForegroundImportance(final @NotNull Context context) { try { final Object service = context.getSystemService(Context.ACTIVITY_SERVICE); if (service instanceof ActivityManager) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 75cc482136..834ac25090 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -10,6 +10,8 @@ import io.sentry.SentryEvent; import io.sentry.SentryLevel; import io.sentry.android.core.internal.util.AndroidMainThreadChecker; +import io.sentry.android.core.performance.AppStartMetrics; +import io.sentry.android.core.performance.TimeSpan; import io.sentry.protocol.App; import io.sentry.protocol.OperatingSystem; import io.sentry.protocol.SentryThread; @@ -192,7 +194,13 @@ private void setDist(final @NotNull SentryBaseEvent event, final @NotNull String private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); - app.setAppStartTime(DateUtils.toUtilDate(AppStartState.getInstance().getAppStartTime())); + final @NotNull TimeSpan appStartTimeSpan = + options.isEnableStarfish() + ? AppStartMetrics.getInstance().getAppStartTimeSpan() + : AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + if (appStartTimeSpan.hasStarted()) { + app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp())); + } // This should not be set by Hybrid SDKs since they have their own app's lifecycle if (!HintUtils.isFromHybridSdk(hint) && app.getInForeground() == null) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index aa25a4e745..79e93a0837 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -16,6 +16,8 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.Session; +import io.sentry.android.core.performance.AppStartMetrics; +import io.sentry.android.core.performance.TimeSpan; import io.sentry.protocol.App; import io.sentry.protocol.Device; import io.sentry.protocol.SentryId; @@ -99,7 +101,14 @@ public static Map serializeScope( app = new App(); } app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); - app.setAppStartTime(DateUtils.toUtilDate(AppStartState.getInstance().getAppStartTime())); + + final @NotNull TimeSpan appStartTimeSpan = + options.isEnableStarfish() + ? AppStartMetrics.getInstance().getAppStartTimeSpan() + : AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + if (appStartTimeSpan.hasStarted()) { + app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp())); + } final @NotNull BuildInfoProvider buildInfoProvider = new BuildInfoProvider(options.getLogger()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 014268cff7..533757a29c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -96,6 +96,8 @@ final class ManifestMetadataReader { static final String SEND_MODULES = "io.sentry.send-modules"; + static final String ENABLE_STARFISH = "io.sentry.starfish.enable"; + /** ManifestMetadataReader ctor */ private ManifestMetadataReader() {} @@ -360,6 +362,9 @@ static void applyMetadata( readBool(metadata, logger, ENABLE_ROOT_CHECK, options.isEnableRootCheck())); options.setSendModules(readBool(metadata, logger, SEND_MODULES, options.isSendModules())); + + options.setEnableStarfish( + readBool(metadata, logger, ENABLE_STARFISH, options.isEnableStarfish())); } options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index eae0297614..34e4ae5ea0 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -9,11 +9,17 @@ import io.sentry.MeasurementUnit; import io.sentry.SentryEvent; import io.sentry.SpanContext; +import io.sentry.SpanId; +import io.sentry.SpanStatus; +import io.sentry.android.core.performance.ActivityLifecycleTimeSpan; +import io.sentry.android.core.performance.AppStartMetrics; +import io.sentry.android.core.performance.TimeSpan; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentrySpan; import io.sentry.protocol.SentryTransaction; import io.sentry.util.Objects; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.jetbrains.annotations.NotNull; @@ -22,6 +28,7 @@ /** Event Processor responsible for adding Android metrics to transactions */ final class PerformanceAndroidEventProcessor implements EventProcessor { + private static final String APP_METRICS_ORIGN = "auto.ui"; private boolean sentStartMeasurement = false; private final @NotNull ActivityFramesTracker activityFramesTracker; @@ -62,20 +69,27 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { // the app start measurement is only sent once and only if the transaction has // the app.start span, which is automatically created by the SDK. - if (!sentStartMeasurement && hasAppStartSpan(transaction.getSpans())) { - final Long appStartUpInterval = AppStartState.getInstance().getAppStartInterval(); - // if appStartUpInterval is null, metrics are not ready to be sent - if (appStartUpInterval != null) { + if (!sentStartMeasurement && hasAppStartSpan(transaction)) { + final @NotNull TimeSpan appStartTimeSpan = + options.isEnableStarfish() + ? AppStartMetrics.getInstance().getAppStartTimeSpan() + : AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + final long appStartUpInterval = appStartTimeSpan.getDurationMs(); + + // if appStartUpInterval is 0, metrics are not ready to be sent + if (appStartUpInterval != 0) { final MeasurementValue value = new MeasurementValue( (float) appStartUpInterval, MeasurementUnit.Duration.MILLISECOND.apiName()); final String appStartKey = - AppStartState.getInstance().isColdStart() + AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD ? MeasurementValue.KEY_APP_START_COLD : MeasurementValue.KEY_APP_START_WARM; transaction.getMeasurements().put(appStartKey, value); + + attachColdAppStartSpans(AppStartMetrics.getInstance(), transaction); sentStartMeasurement = true; } } @@ -99,13 +113,148 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { return transaction; } - private boolean hasAppStartSpan(final @NotNull List spans) { - for (final SentrySpan span : spans) { + private boolean hasAppStartSpan(final @NotNull SentryTransaction txn) { + final @NotNull List spans = txn.getSpans(); + for (final @NotNull SentrySpan span : spans) { if (span.getOp().contentEquals(APP_START_COLD) || span.getOp().contentEquals(APP_START_WARM)) { return true; } } - return false; + + final @Nullable SpanContext context = txn.getContexts().getTrace(); + return context != null + && (context.getOperation().equals(APP_START_COLD) + || context.getOperation().equals(APP_START_WARM)); + } + + private void attachColdAppStartSpans( + final @NotNull AppStartMetrics appStartMetrics, final @NotNull SentryTransaction txn) { + + // data will be filled anyway only for cold app starts + if (appStartMetrics.getAppStartType() != AppStartMetrics.AppStartType.COLD) { + return; + } + + final @Nullable SpanContext traceContext = txn.getContexts().getTrace(); + if (traceContext == null) { + return; + } + final @NotNull SentryId traceId = traceContext.getTraceId(); + + // Application.onCreate + final @NotNull TimeSpan appOnCreate = appStartMetrics.getApplicationOnCreateTimeSpan(); + if (appOnCreate.hasStopped()) { + final SentrySpan span = + new SentrySpan( + appOnCreate.getStartTimestampS(), + appOnCreate.getProjectedStopTimestampS(), + traceId, + new SpanId(), + null, + UI_LOAD_OP, + appOnCreate.getDescription(), + SpanStatus.OK, + APP_METRICS_ORIGN, + new HashMap<>(), + null); + txn.getSpans().add(span); + } + + // Content Provider + final @NotNull List contentProviderOnCreates = + appStartMetrics.getContentProviderOnCreateTimeSpans(); + if (!contentProviderOnCreates.isEmpty()) { + final @NotNull SentrySpan contentProviderRootSpan = + new SentrySpan( + contentProviderOnCreates.get(0).getStartTimestampS(), + contentProviderOnCreates + .get(contentProviderOnCreates.size() - 1) + .getProjectedStopTimestampS(), + traceId, + new SpanId(), + null, + UI_LOAD_OP, + "ContentProvider", + SpanStatus.OK, + APP_METRICS_ORIGN, + new HashMap<>(), + null); + txn.getSpans().add(contentProviderRootSpan); + for (final @NotNull TimeSpan contentProvider : contentProviderOnCreates) { + final SentrySpan contentProviderSpan = + new SentrySpan( + contentProvider.getStartTimestampS(), + contentProvider.getProjectedStopTimestampS(), + traceId, + new SpanId(), + contentProviderRootSpan.getSpanId(), + UI_LOAD_OP, + contentProvider.getDescription(), + SpanStatus.OK, + APP_METRICS_ORIGN, + new HashMap<>(), + null); + txn.getSpans().add(contentProviderSpan); + } + } + + // Activities + final @NotNull List activityLifecycleTimeSpans = + appStartMetrics.getActivityLifecycleTimeSpans(); + if (!activityLifecycleTimeSpans.isEmpty()) { + final SentrySpan activityRootSpan = + new SentrySpan( + activityLifecycleTimeSpans.get(0).onCreate.getStartTimestampS(), + activityLifecycleTimeSpans + .get(activityLifecycleTimeSpans.size() - 1) + .onStart + .getProjectedStopTimestampS(), + traceId, + new SpanId(), + null, + UI_LOAD_OP, + "Activity", + SpanStatus.OK, + APP_METRICS_ORIGN, + new HashMap<>(), + null); + txn.getSpans().add(activityRootSpan); + + for (ActivityLifecycleTimeSpan activityTimeSpan : activityLifecycleTimeSpans) { + if (activityTimeSpan.onCreate.hasStarted() && activityTimeSpan.onCreate.hasStopped()) { + final SentrySpan onCreateSpan = + new SentrySpan( + activityTimeSpan.onCreate.getStartTimestampS(), + activityTimeSpan.onCreate.getProjectedStopTimestampS(), + traceId, + new SpanId(), + activityRootSpan.getSpanId(), + UI_LOAD_OP, + activityTimeSpan.onCreate.getDescription(), + SpanStatus.OK, + APP_METRICS_ORIGN, + new HashMap<>(), + null); + txn.getSpans().add(onCreateSpan); + } + if (activityTimeSpan.onStart.hasStarted() && activityTimeSpan.onStart.hasStopped()) { + final SentrySpan onStartSpan = + new SentrySpan( + activityTimeSpan.onStart.getStartTimestampS(), + activityTimeSpan.onStart.getProjectedStopTimestampS(), + traceId, + new SpanId(), + activityRootSpan.getSpanId(), + UI_LOAD_OP, + activityTimeSpan.onStart.getDescription(), + SpanStatus.OK, + APP_METRICS_ORIGN, + new HashMap<>(), + null); + txn.getSpans().add(onStartSpan); + } + } + } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 044e727a5c..0642516f65 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -1,16 +1,18 @@ package io.sentry.android.core; import android.content.Context; +import android.os.Process; import android.os.SystemClock; import io.sentry.IHub; import io.sentry.ILogger; import io.sentry.Integration; import io.sentry.OptionsContainer; import io.sentry.Sentry; -import io.sentry.SentryDate; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.BreadcrumbFactory; +import io.sentry.android.core.performance.AppStartMetrics; +import io.sentry.android.core.performance.TimeSpan; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; import java.lang.reflect.InvocationTargetException; @@ -21,9 +23,6 @@ /** Sentry initialization class */ public final class SentryAndroid { - // static to rely on Class load init. - private static final @NotNull SentryDate appStartTime = - AndroidDateUtils.getCurrentSentryDateTime(); // SystemClock.uptimeMillis() isn't affected by phone provider or clock changes. private static final long appStart = SystemClock.uptimeMillis(); @@ -81,9 +80,6 @@ public static synchronized void init( @NotNull final Context context, @NotNull ILogger logger, @NotNull Sentry.OptionsConfiguration configuration) { - // if SentryPerformanceProvider was disabled or removed, we set the App Start when - // the SDK is called. - AppStartState.getInstance().setAppStartTime(appStart, appStartTime); try { Sentry.init( @@ -124,6 +120,24 @@ public static synchronized void init( configuration.configure(options); + // if SentryPerformanceProvider was disabled or removed, we set the App Start when + // the SDK is called. + // pre-starfish: fill-back the app start time to the SDK init time + if (options.isEnableStarfish()) { + final @NotNull TimeSpan appStartTimeSpan = + AppStartMetrics.getInstance().getAppStartTimeSpan(); + if (appStartTimeSpan.hasNotStarted() + && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + appStartTimeSpan.setStartedAt(Process.getStartUptimeMillis()); + } + } else { + final @NotNull TimeSpan appStartTime = + AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + if (appStartTime.hasNotStarted()) { + appStartTime.setStartedAt(appStart); + } + } + AndroidOptionsInitializer.initializeIntegrationsAndProcessors( options, context, buildInfoProvider, loadClass, activityFramesTracker); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 62437a0425..a5e55c654e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -203,6 +203,8 @@ public interface BeforeCaptureCallback { */ private boolean attachAnrThreadDump = false; + private boolean enableStarfish; + public SentryAndroidOptions() { setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(createSdkVersion()); @@ -547,4 +549,14 @@ public boolean isAttachAnrThreadDump() { public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) { this.attachAnrThreadDump = attachAnrThreadDump; } + + @ApiStatus.Internal + public boolean isEnableStarfish() { + return enableStarfish; + } + + @ApiStatus.Internal + public void setEnableStarfish(final boolean enableStarfish) { + this.enableStarfish = enableStarfish; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 7ba270351b..77859067c6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -6,55 +6,36 @@ import android.content.pm.ProviderInfo; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; import android.os.SystemClock; -import io.sentry.SentryDate; +import androidx.annotation.NonNull; +import io.sentry.android.core.performance.ActivityLifecycleCallbacksAdapter; +import io.sentry.android.core.performance.ActivityLifecycleTimeSpan; +import io.sentry.android.core.performance.AppStartMetrics; +import io.sentry.android.core.performance.NextDrawListener; +import io.sentry.android.core.performance.TimeSpan; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; -/** - * SentryPerformanceProvider is responsible for collecting data (eg appStart) as early as possible - * as ContentProvider is the only reliable hook for libraries that works across all the supported - * SDK versions. When minSDK is >= 24, we could use Process.getStartUptimeMillis() We could also use - * AppComponentFactory but it depends on androidx.core.app.AppComponentFactory - */ @ApiStatus.Internal -public final class SentryPerformanceProvider extends EmptySecureContentProvider - implements Application.ActivityLifecycleCallbacks { +public final class SentryPerformanceProvider extends EmptySecureContentProvider { // static to rely on Class load - private static @NotNull SentryDate appStartTime = AndroidDateUtils.getCurrentSentryDateTime(); // SystemClock.uptimeMillis() isn't affected by phone provider or clock changes. - private static long appStartMillis = SystemClock.uptimeMillis(); + private static final long legacyAppStartMillis = SystemClock.uptimeMillis(); - private boolean firstActivityCreated = false; - private boolean firstActivityResumed = false; - - private @Nullable Application application; - - public SentryPerformanceProvider() { - AppStartState.getInstance().setAppStartTime(appStartMillis, appStartTime); - } + private @Nullable Application app; + private @Nullable Application.ActivityLifecycleCallbacks activityCallback; @Override public boolean onCreate() { - Context context = getContext(); - - if (context == null) { - return false; - } - - // it returns null if ContextImpl, so let's check for nullability - if (context.getApplicationContext() != null) { - context = context.getApplicationContext(); - } - - if (context instanceof Application) { - application = ((Application) context); - application.registerActivityLifecycleCallbacks(this); - } - + onAppLaunched(); return true; } @@ -74,53 +55,145 @@ public String getType(@NotNull Uri uri) { return null; } - @TestOnly - static void setAppStartTime( - final long appStartMillisLong, final @NotNull SentryDate appStartTimeDate) { - appStartMillis = appStartMillisLong; - appStartTime = appStartTimeDate; - } - - @Override - public void onActivityCreated(@NotNull Activity activity, @Nullable Bundle savedInstanceState) { - // Hybrid Apps like RN or Flutter init the Android SDK after the MainActivity of the App - // has been created, and some frameworks overwrites the behaviour of activity lifecycle - // or it's already too late to get the callback for the very first Activity, hence we - // register the ActivityLifecycleCallbacks here, since this Provider is always run first. - if (!firstActivityCreated) { - // if Activity has savedInstanceState then its a warm start - // https://developer.android.com/topic/performance/vitals/launch-time#warm - final boolean coldStart = savedInstanceState == null; - AppStartState.getInstance().setColdStart(coldStart); - - firstActivityCreated = true; + @ApiStatus.Internal + public void onAppLaunched() { + // pre-starfish: use static field init as app start time + final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); + final @NotNull TimeSpan legacyAppStartSpan = appStartMetrics.getLegacyAppStartTimeSpan(); + legacyAppStartSpan.setStartedAt(legacyAppStartMillis); + + // starfish: Use Process.getStartUptimeMillis() + // Process.getStartUptimeMillis() requires API level 24+ + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N) { + return; } - } - @Override - public void onActivityStarted(@NotNull Activity activity) {} - - @Override - public void onActivityResumed(@NotNull Activity activity) { - if (!firstActivityResumed) { - // sets App start as finished when the very first activity calls onResume - firstActivityResumed = true; - AppStartState.getInstance().setAppStartEnd(); + @Nullable Context context = getContext(); + if (context != null) { + context = context.getApplicationContext(); } - if (application != null) { - application.unregisterActivityLifecycleCallbacks(this); + if (context instanceof Application) { + app = (Application) context; + } + if (app == null) { + return; } - } - @Override - public void onActivityPaused(@NotNull Activity activity) {} + final @NotNull TimeSpan appStartTimespan = appStartMetrics.getAppStartTimeSpan(); + appStartTimespan.setStartedAt(Process.getStartUptimeMillis()); + + final AtomicBoolean firstDrawDone = new AtomicBoolean(false); + final Handler handler = new Handler(Looper.getMainLooper()); + + activityCallback = + new ActivityLifecycleCallbacksAdapter() { + final WeakHashMap activityLifecycleMap = + new WeakHashMap<>(); + + @Override + public void onActivityPreCreated( + @NonNull Activity activity, @Nullable Bundle savedInstanceState) { + final long now = SystemClock.uptimeMillis(); + if (appStartMetrics.getAppStartTimeSpan().hasStopped()) { + return; + } + + final ActivityLifecycleTimeSpan timeSpan = new ActivityLifecycleTimeSpan(); + timeSpan.onCreate.setStartedAt(now); + activityLifecycleMap.put(activity, timeSpan); + } + + @Override + public void onActivityCreated( + @NonNull Activity activity, @Nullable Bundle savedInstanceState) { + if (appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.UNKNOWN) { + appStartMetrics.setAppStartType( + savedInstanceState == null + ? AppStartMetrics.AppStartType.COLD + : AppStartMetrics.AppStartType.WARM); + } + } + + @Override + public void onActivityPostCreated( + @NonNull Activity activity, @Nullable Bundle savedInstanceState) { + if (appStartMetrics.getAppStartTimeSpan().hasStopped()) { + return; + } + + final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity); + if (timeSpan != null) { + timeSpan.onCreate.stop(); + timeSpan.onCreate.setDescription(activity.getClass().getName() + ".onCreate"); + } + } + + @Override + public void onActivityPreStarted(@NonNull Activity activity) { + final long now = SystemClock.uptimeMillis(); + if (appStartMetrics.getAppStartTimeSpan().hasStopped()) { + return; + } + final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity); + if (timeSpan != null) { + timeSpan.onStart.setStartedAt(now); + } + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + if (firstDrawDone.get()) { + return; + } + NextDrawListener.forActivity( + activity, + () -> { + handler.postAtFrontOfQueue( + () -> { + if (firstDrawDone.compareAndSet(false, true)) { + onAppStartDone(); + } + }); + }); + } + + @Override + public void onActivityPostStarted(@NonNull Activity activity) { + final @Nullable ActivityLifecycleTimeSpan timeSpan = + activityLifecycleMap.remove(activity); + if (appStartMetrics.getAppStartTimeSpan().hasStopped()) { + return; + } + if (timeSpan != null) { + timeSpan.onStart.stop(); + timeSpan.onStart.setDescription(activity.getClass().getName() + ".onStart"); + + appStartMetrics.addActivityLifecycleTimeSpans(timeSpan); + } + } + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + // safety net for activities which were created but never stopped + activityLifecycleMap.remove(activity); + } + }; + + app.registerActivityLifecycleCallbacks(activityCallback); + } - @Override - public void onActivityStopped(@NotNull Activity activity) {} + private synchronized void onAppStartDone() { + AppStartMetrics.getInstance().getAppStartTimeSpan().stop(); - @Override - public void onActivitySaveInstanceState(@NotNull Activity activity, @NotNull Bundle outState) {} + if (app != null) { + if (activityCallback != null) { + app.unregisterActivityLifecycleCallbacks(activityCallback); + } + } + } - @Override - public void onActivityDestroyed(@NotNull Activity activity) {} + @TestOnly + public @Nullable Application.ActivityLifecycleCallbacks getActivityCallback() { + return activityCallback; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java index bd3e0809b5..bf83bbb116 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java @@ -9,9 +9,10 @@ import io.sentry.SentryOptions; import io.sentry.UncaughtExceptionHandlerIntegration; import io.sentry.android.core.AnrV2Integration; -import io.sentry.android.core.AppStartState; import io.sentry.android.core.SentryAndroidOptions; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; +import io.sentry.android.core.performance.AppStartMetrics; +import io.sentry.android.core.performance.TimeSpan; import io.sentry.cache.EnvelopeCache; import io.sentry.transport.ICurrentDateProvider; import io.sentry.util.FileUtils; @@ -52,10 +53,15 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) { final SentryAndroidOptions options = (SentryAndroidOptions) this.options; - final Long appStartTime = AppStartState.getInstance().getAppStartMillis(); + final TimeSpan appStartTimeSpan = + options.isEnableStarfish() + ? AppStartMetrics.getInstance().getAppStartTimeSpan() + : AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + if (HintUtils.hasType(hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class) - && appStartTime != null) { - long timeSinceSdkInit = currentDateProvider.getCurrentTimeMillis() - appStartTime; + && appStartTimeSpan.hasStarted()) { + long timeSinceSdkInit = + currentDateProvider.getCurrentTimeMillis() - appStartTimeSpan.getStartTimestampMs(); if (timeSinceSdkInit <= options.getStartupCrashDurationThresholdMillis()) { options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/WindowCallbackAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/WindowCallbackAdapter.java index a6568ad057..c6a3e62711 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/WindowCallbackAdapter.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/WindowCallbackAdapter.java @@ -16,11 +16,11 @@ import org.jetbrains.annotations.Nullable; @Open -class WindowCallbackAdapter implements Window.Callback { +public class WindowCallbackAdapter implements Window.Callback { private final @NotNull Window.Callback delegate; - WindowCallbackAdapter(final Window.@NotNull Callback delegate) { + public WindowCallbackAdapter(final Window.@NotNull Callback delegate) { this.delegate = delegate; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapter.java new file mode 100644 index 0000000000..c7a449d1ef --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapter.java @@ -0,0 +1,31 @@ +package io.sentry.android.core.performance; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ActivityLifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks { + + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {} + + @Override + public void onActivityStarted(@NonNull Activity activity) {} + + @Override + public void onActivityResumed(@NonNull Activity activity) {} + + @Override + public void onActivityPaused(@NonNull Activity activity) {} + + @Override + public void onActivityStopped(@NonNull Activity activity) {} + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} + + @Override + public void onActivityDestroyed(@NonNull Activity activity) {} +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpan.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpan.java new file mode 100644 index 0000000000..1d7f4eb2aa --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpan.java @@ -0,0 +1,15 @@ +package io.sentry.android.core.performance; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public class ActivityLifecycleTimeSpan implements Comparable { + public final @NotNull TimeSpan onCreate = new TimeSpan(); + public final @NotNull TimeSpan onStart = new TimeSpan(); + + @Override + public int compareTo(ActivityLifecycleTimeSpan o) { + return Long.compare(onCreate.getStartUptimeMs(), o.onCreate.getStartUptimeMs()); + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java new file mode 100644 index 0000000000..71219aa2bc --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -0,0 +1,183 @@ +package io.sentry.android.core.performance; + +import android.app.Application; +import android.content.ContentProvider; +import android.os.SystemClock; +import androidx.annotation.Nullable; +import io.sentry.android.core.ContextUtils; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +/** + * An in-memory representation for app-metrics during app start. As the SDK can't be initialized + * that early, we can't use transactions or spans directly. Thus simple TimeSpans are used and later + * transformed into SDK specific txn/span data structures. + */ +public class AppStartMetrics { + + public enum AppStartType { + UNKNOWN, + COLD, + WARM + } + + private static volatile @Nullable AppStartMetrics instance; + + private @NotNull AppStartType appStartType = AppStartType.UNKNOWN; + private boolean appLaunchedInForeground = false; + + private final @NotNull TimeSpan appStartSpan; + private final @NotNull TimeSpan legacyAppStartSpan; + private final @NotNull TimeSpan applicationOnCreate; + private final @NotNull Map contentProviderOnCreates; + private final @NotNull List activityLifecycles; + + public static @NotNull AppStartMetrics getInstance() { + + if (instance == null) { + synchronized (AppStartMetrics.class) { + if (instance == null) { + instance = new AppStartMetrics(); + } + } + } + //noinspection DataFlowIssue + return instance; + } + + public AppStartMetrics() { + appStartSpan = new TimeSpan(); + legacyAppStartSpan = new TimeSpan(); + applicationOnCreate = new TimeSpan(); + contentProviderOnCreates = new HashMap<>(); + activityLifecycles = new ArrayList<>(); + } + + /** + * @return the app start span Uses Process.getStartUptimeMillis() as start timestamp, which + * requires API level 24+ + */ + public @NotNull TimeSpan getAppStartTimeSpan() { + return appStartSpan; + } + + /** + * @return the app start time span, as measured pre-starfish Uses ContentProvider/Sdk init time as + * start timestamp + */ + public @NotNull TimeSpan getLegacyAppStartTimeSpan() { + return legacyAppStartSpan; + } + + public @NotNull TimeSpan getApplicationOnCreateTimeSpan() { + return applicationOnCreate; + } + + public void setAppStartType(final @NotNull AppStartType appStartType) { + this.appStartType = appStartType; + } + + public @NotNull AppStartType getAppStartType() { + return appStartType; + } + + public boolean isAppLaunchedInForeground() { + return appLaunchedInForeground; + } + + /** + * Provides all collected content provider onCreate time spans + * + * @return A sorted list of all onCreate calls + */ + public @NotNull List getContentProviderOnCreateTimeSpans() { + final List measurements = new ArrayList<>(contentProviderOnCreates.values()); + Collections.sort(measurements); + return measurements; + } + + public @NotNull List getActivityLifecycleTimeSpans() { + final List measurements = new ArrayList<>(activityLifecycles); + Collections.sort(measurements); + return measurements; + } + + public void addActivityLifecycleTimeSpans(final @NotNull ActivityLifecycleTimeSpan timeSpan) { + activityLifecycles.add(timeSpan); + } + + public void clear() { + appStartType = AppStartType.UNKNOWN; + appStartSpan.reset(); + legacyAppStartSpan.reset(); + applicationOnCreate.reset(); + contentProviderOnCreates.clear(); + } + + /** + * Called by instrumentation + * + * @param application The application object where onCreate was called on + * @noinspection unused + */ + public static void onApplicationCreate(final @NotNull Application application) { + final long now = SystemClock.uptimeMillis(); + + final @NotNull AppStartMetrics instance = getInstance(); + if (instance.applicationOnCreate.hasNotStarted()) { + instance.applicationOnCreate.setStartedAt(now); + instance.appLaunchedInForeground = ContextUtils.isForegroundImportance(application); + } + } + + /** + * Called by instrumentation + * + * @param application The application object where onCreate was called on + * @noinspection unused + */ + public static void onApplicationPostCreate(final @NotNull Application application) { + final long now = SystemClock.uptimeMillis(); + + final @NotNull AppStartMetrics instance = getInstance(); + if (instance.applicationOnCreate.hasNotStopped()) { + instance.applicationOnCreate.setDescription(application.getClass().getName() + ".onCreate"); + instance.applicationOnCreate.setStoppedAt(now); + } + } + + /** + * Called by instrumentation + * + * @param contentProvider The content provider where onCreate was called on + * @noinspection unused + */ + public static void onContentProviderCreate(final @NotNull ContentProvider contentProvider) { + final long now = SystemClock.uptimeMillis(); + + final TimeSpan measurement = new TimeSpan(); + measurement.setStartedAt(now); + getInstance().contentProviderOnCreates.put(contentProvider, measurement); + } + + /** + * Called by instrumentation + * + * @param contentProvider The content provider where onCreate was called on + * @noinspection unused + */ + public static void onContentProviderPostCreate(final @NotNull ContentProvider contentProvider) { + final long now = SystemClock.uptimeMillis(); + + final @Nullable TimeSpan measurement = + getInstance().contentProviderOnCreates.get(contentProvider); + if (measurement != null && measurement.hasNotStopped()) { + measurement.setDescription(contentProvider.getClass().getName() + ".onCreate"); + measurement.setStoppedAt(now); + } + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/NextDrawListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/NextDrawListener.java new file mode 100644 index 0000000000..48b669f1c7 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/NextDrawListener.java @@ -0,0 +1,138 @@ +package io.sentry.android.core.performance; + +import android.app.Activity; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.Window; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import io.sentry.android.core.internal.gestures.NoOpWindowCallback; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Inspired by https://blog.p-y.wtf/tracking-android-app-launch-in-production Adapted from: + * https://github.com/square/papa/blob/31eebb3d70908bcb1209d82f066ec4d4377183ee/papa/src/main/java/papa/internal/ViewTreeObservers.kt + * + *

Copyright 2021 Square Inc. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +@ApiStatus.Internal +public class NextDrawListener + implements ViewTreeObserver.OnDrawListener, View.OnAttachStateChangeListener { + + private @NotNull final Runnable onDrawCallback; + private @NotNull final Handler mainHandler; + private boolean invoked; + + private @Nullable View view; + + protected NextDrawListener( + final @NotNull Handler handler, final @NotNull Runnable onDrawCallback) { + this.mainHandler = handler; + this.onDrawCallback = onDrawCallback; + } + + public static NextDrawListener forActivity( + final @NotNull Activity activity, final @NotNull Runnable onDrawCallback) { + final NextDrawListener listener = + new NextDrawListener(new Handler(Looper.getMainLooper()), onDrawCallback); + + @Nullable Window window = activity.getWindow(); + if (window != null) { + @Nullable View decorView = window.peekDecorView(); + if (decorView != null) { + listener.safelyRegisterForNextDraw(decorView); + } else { + @Nullable Window.Callback oldCallback = window.getCallback(); + if (oldCallback == null) { + oldCallback = new NoOpWindowCallback(); + } + window.setCallback( + new WindowContentChangedCallback( + oldCallback, + () -> { + @Nullable View newDecorView = window.peekDecorView(); + if (newDecorView != null) { + listener.safelyRegisterForNextDraw(newDecorView); + } + })); + } + } + return listener; + } + + @Override + public void onDraw() { + if (invoked) { + return; + } + invoked = true; + // ViewTreeObserver.removeOnDrawListener() throws if called from the onDraw() callback + mainHandler.post( + () -> { + final ViewTreeObserver observer = view.getViewTreeObserver(); + if (observer != null && observer.isAlive()) { + observer.removeOnDrawListener(NextDrawListener.this); + } + }); + onDrawCallback.run(); + } + + private void safelyRegisterForNextDraw(final @NotNull View view) { + this.view = view; + // Prior to API 26, OnDrawListener wasn't merged back from the floating ViewTreeObserver into + // the real ViewTreeObserver. + // https://android.googlesource.com/platform/frameworks/base/+/9f8ec54244a5e0343b9748db3329733f259604f3 + final @Nullable ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); + if (Build.VERSION.SDK_INT >= 26 + && viewTreeObserver != null + && (viewTreeObserver.isAlive() && ViewCompat.isAttachedToWindow(view))) { + viewTreeObserver.addOnDrawListener(this); + } else { + view.addOnAttachStateChangeListener(this); + } + } + + @Override + public void onViewAttachedToWindow(@NonNull View v) { + if (view != null) { + // Backed by CopyOnWriteArrayList, ok to self remove from onViewDetachedFromWindow() + view.removeOnAttachStateChangeListener(this); + + final @Nullable ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); + if (viewTreeObserver != null && viewTreeObserver.isAlive()) { + viewTreeObserver.addOnDrawListener(this); + } + } + } + + @Override + public void onViewDetachedFromWindow(@NonNull View v) { + unregister(); + } + + public void unregister() { + if (view != null) { + view.removeOnAttachStateChangeListener(this); + + final @Nullable ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); + if (viewTreeObserver != null && viewTreeObserver.isAlive()) { + viewTreeObserver.removeOnDrawListener(this); + } + } + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java new file mode 100644 index 0000000000..946cdc1179 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java @@ -0,0 +1,164 @@ +package io.sentry.android.core.performance; + +import android.os.SystemClock; +import io.sentry.DateUtils; +import io.sentry.SentryDate; +import io.sentry.SentryLongDate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +/** + * A measurement for time critical components on a macro (ms) level. Based on {@link + * SystemClock#uptimeMillis()} to ensure linear time progression (as opposed to a syncable clock). + * To provide real world unix time information, the start uptime time is stored alongside the unix + * time. The stop unix time is artificial, it gets projected based on the start time + duration of + * the time span. + */ +public class TimeSpan implements Comparable { + + private @Nullable String description; + + private long startUnixTimeMs; + private long startUptimeMs; + private long stopUptimeMs; + + /** Start the time span */ + public void start() { + startUptimeMs = SystemClock.uptimeMillis(); + startUnixTimeMs = System.currentTimeMillis(); + } + + /** + * @param uptimeMs the uptime in ms, provided by {@link SystemClock#uptimeMillis()} + */ + public void setStartedAt(final long uptimeMs) { + // TODO maybe sanity check? + this.startUptimeMs = uptimeMs; + + final long shiftMs = SystemClock.uptimeMillis() - startUptimeMs; + startUnixTimeMs = System.currentTimeMillis() - shiftMs; + } + + /** Stops the time span */ + public void stop() { + stopUptimeMs = SystemClock.uptimeMillis(); + } + + /** + * @param uptimeMs the uptime in ms, provided by {@link SystemClock#uptimeMillis()} + */ + public void setStoppedAt(final long uptimeMs) { + // TODO maybe sanity check? + stopUptimeMs = uptimeMs; + } + + public boolean hasStarted() { + return startUptimeMs != 0; + } + + public boolean hasNotStarted() { + return startUptimeMs == 0; + } + + public boolean hasStopped() { + return stopUptimeMs != 0; + } + + public boolean hasNotStopped() { + return stopUptimeMs == 0; + } + + /** + * @return the start timestamp of this measurement, as uptime, in ms + */ + public long getStartUptimeMs() { + return startUptimeMs; + } + + /** + * @return the start timestamp of this measurement, unix time, in ms + */ + public long getStartTimestampMs() { + return startUnixTimeMs; + } + + /** + * @return the start timestamp of this measurement, unix time + */ + public @Nullable SentryDate getStartTimestamp() { + if (hasStarted()) { + return new SentryLongDate(DateUtils.millisToNanos(getStartTimestampMs())); + } + return null; + } + + /** + * @return the start timestamp of this measurement, unix time, in ms + */ + public double getStartTimestampS() { + return (double) startUnixTimeMs / 1000.0d; + } + + /** + * @return the projected stop timestamp of this measurement, based on the start timestamp and the + * duration. If the time span was not started 0 is returned, if the time span was not stopped + * the start timestamp is returned. + */ + public long getProjectedStopTimestampMs() { + if (hasStarted()) { + return startUnixTimeMs + getDurationMs(); + } + return 0; + } + + public double getProjectedStopTimestampS() { + return (double) getProjectedStopTimestampMs() / 1000.0d; + } + + /** + * @return the start timestamp of this measurement, unix time + */ + public @Nullable SentryDate getProjectedStopTimestamp() { + if (hasStopped()) { + return new SentryLongDate(DateUtils.millisToNanos(getProjectedStopTimestampMs())); + } + return null; + } + + /** + * @return the duration of this measurement, in ms, or 0 if no end time is set + */ + public long getDurationMs() { + if (hasStopped()) { + return stopUptimeMs - startUptimeMs; + } else { + return 0; + } + } + + @TestOnly + public void setStartUnixTimeMs(long startUnixTimeMs) { + this.startUnixTimeMs = startUnixTimeMs; + } + + public @Nullable String getDescription() { + return description; + } + + public void setDescription(@Nullable final String description) { + this.description = description; + } + + public void reset() { + description = null; + startUptimeMs = 0; + stopUptimeMs = 0; + startUnixTimeMs = 0; + } + + @Override + public int compareTo(@NotNull final TimeSpan o) { + return Long.compare(startUnixTimeMs, o.startUnixTimeMs); + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/WindowContentChangedCallback.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/WindowContentChangedCallback.java new file mode 100644 index 0000000000..79180a7bd5 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/WindowContentChangedCallback.java @@ -0,0 +1,22 @@ +package io.sentry.android.core.performance; + +import android.view.Window; +import io.sentry.android.core.internal.gestures.WindowCallbackAdapter; +import org.jetbrains.annotations.NotNull; + +public class WindowContentChangedCallback extends WindowCallbackAdapter { + + private final @NotNull Runnable callback; + + public WindowContentChangedCallback( + final @NotNull Window.Callback delegate, final @NotNull Runnable callback) { + super(delegate); + this.callback = callback; + } + + @Override + public void onContentChanged() { + super.onContentChanged(); + callback.run(); + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 26bd42e5ab..69113a85b8 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -30,6 +30,8 @@ import io.sentry.TraceContext import io.sentry.TransactionContext import io.sentry.TransactionFinishedCallback import io.sentry.TransactionOptions +import io.sentry.android.core.performance.AppStartMetrics +import io.sentry.android.core.performance.AppStartMetrics.AppStartType import io.sentry.protocol.MeasurementValue import io.sentry.protocol.TransactionNameSource import io.sentry.test.getProperty @@ -129,7 +131,7 @@ class ActivityLifecycleIntegrationTest { @BeforeTest fun `reset instance`() { - AppStartState.getInstance().resetInstance() + AppStartMetrics.getInstance().clear() } @AfterTest @@ -602,7 +604,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityDestroyed(activity) val span = fixture.transaction.children.first() - assertEquals(span.status, SpanStatus.CANCELLED) + assertEquals(SpanStatus.CANCELLED, span.status) assertTrue(span.isFinished) } @@ -787,7 +789,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, null) - assertTrue(AppStartState.getInstance().isColdStart!!) + assertEquals(AppStartType.COLD, AppStartMetrics.getInstance().appStartType) } @Test @@ -800,7 +802,7 @@ class ActivityLifecycleIntegrationTest { val bundle = Bundle() sut.onActivityCreated(activity, bundle) - assertFalse(AppStartState.getInstance().isColdStart!!) + assertEquals(AppStartType.WARM, AppStartMetrics.getInstance().appStartType) } @Test @@ -814,7 +816,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, bundle) sut.onActivityCreated(activity, null) - assertFalse(AppStartState.getInstance().isColdStart!!) + assertEquals(AppStartType.WARM, AppStartMetrics.getInstance().appStartType) } @Test @@ -823,14 +825,19 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) - val date = SentryNanotimeDate(Date(0), 0) + val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) // call only once - verify(fixture.hub).startTransaction(any(), check { assertEquals(date, it.startTimestamp) }) + verify(fixture.hub).startTransaction( + any(), + check { + assertEquals(date.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) + } + ) } @Test @@ -840,9 +847,9 @@ class ActivityLifecycleIntegrationTest { sut.register(fixture.hub, fixture.options) // usually set by SentryPerformanceProvider - val date = SentryNanotimeDate(Date(0), 0) + val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) - AppStartState.getInstance().setAppStartEnd(1) + AppStartMetrics.getInstance().legacyAppStartTimeSpan.setStoppedAt(2) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -858,12 +865,13 @@ class ActivityLifecycleIntegrationTest { sut.register(fixture.hub, fixture.options) // usually set by SentryPerformanceProvider - val startDate = SentryNanotimeDate(Date(0), 0) + val startDate = SentryNanotimeDate(Date(1), 0) setAppStartTime(startDate) - AppStartState.getInstance().setColdStart(false) - AppStartState.getInstance().setAppStartEnd(1) + val appStartMetrics = AppStartMetrics.getInstance() + appStartMetrics.appStartType = AppStartType.WARM + appStartMetrics.legacyAppStartTimeSpan.setStoppedAt(2) - val endDate = AppStartState.getInstance().appStartEndTime!! + val endDate = appStartMetrics.legacyAppStartTimeSpan.projectedStopTimestamp val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -871,7 +879,7 @@ class ActivityLifecycleIntegrationTest { val appStartSpanCount = fixture.transaction.children.count { it.spanContext.operation.startsWith("app.start.warm") && it.startDate.nanoTimestamp() == startDate.nanoTimestamp() && - it.finishDate!!.nanoTimestamp() == endDate.nanoTimestamp() + it.finishDate!!.nanoTimestamp() == endDate!!.nanoTimestamp() } assertEquals(1, appStartSpanCount) } @@ -884,20 +892,20 @@ class ActivityLifecycleIntegrationTest { // usually done by SentryPerformanceProvider, if disabled it's done by // SentryAndroid.init - val startDate = SentryNanotimeDate(Date(0), 0) + val startDate = SentryNanotimeDate(Date(1), 0) setAppStartTime(startDate) - AppStartState.getInstance().setColdStart(false) + AppStartMetrics.getInstance().appStartType = AppStartType.WARM // when activity is created val activity = mock() sut.onActivityCreated(activity, fixture.bundle) // then app-start end time should still be null - assertNull(AppStartState.getInstance().appStartEndTime) + assertTrue(AppStartMetrics.getInstance().legacyAppStartTimeSpan.hasNotStopped()) // when activity is resumed sut.onActivityResumed(activity) // end-time should be set - assertNotNull(AppStartState.getInstance().appStartEndTime) + assertTrue(AppStartMetrics.getInstance().legacyAppStartTimeSpan.hasStopped()) } @Test @@ -907,10 +915,10 @@ class ActivityLifecycleIntegrationTest { sut.register(fixture.hub, fixture.options) // usually done by SentryPerformanceProvider - val startDate = SentryNanotimeDate(Date(0), 0) + val startDate = SentryNanotimeDate(Date(1), 0) setAppStartTime(startDate) - AppStartState.getInstance().setColdStart(false) - AppStartState.getInstance().setAppStartEnd(1234) + AppStartMetrics.getInstance().appStartType = AppStartType.WARM + AppStartMetrics.getInstance().legacyAppStartTimeSpan.setStoppedAt(1234) // when activity is created and resumed val activity = mock() @@ -920,7 +928,7 @@ class ActivityLifecycleIntegrationTest { // then the end time should not be overwritten assertEquals( DateUtils.millisToNanos(1234), - AppStartState.getInstance().appStartEndTime!!.nanoTimestamp() + AppStartMetrics.getInstance().legacyAppStartTimeSpan.projectedStopTimestamp!!.nanoTimestamp() ) } @@ -931,9 +939,9 @@ class ActivityLifecycleIntegrationTest { sut.register(fixture.hub, fixture.options) // usually done by SentryPerformanceProvider - val startDate = SentryNanotimeDate(Date(0), 0) + val startDate = SentryNanotimeDate(Date(1), 0) setAppStartTime(startDate) - AppStartState.getInstance().setColdStart(false) + AppStartMetrics.getInstance().appStartType = AppStartType.WARM // when activity is created, started and resumed multiple times val activity = mock() @@ -941,7 +949,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityStarted(activity) sut.onActivityResumed(activity) - val firstAppStartEndTime = AppStartState.getInstance().appStartEndTime + val firstAppStartEndTime = AppStartMetrics.getInstance().legacyAppStartTimeSpan.projectedStopTimestamp Thread.sleep(1) sut.onActivityPaused(activity) @@ -952,7 +960,7 @@ class ActivityLifecycleIntegrationTest { // then the end time should not be overwritten assertEquals( firstAppStartEndTime!!.nanoTimestamp(), - AppStartState.getInstance().appStartEndTime!!.nanoTimestamp() + AppStartMetrics.getInstance().legacyAppStartTimeSpan.projectedStopTimestamp!!.nanoTimestamp() ) } @@ -962,7 +970,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) - val date = SentryNanotimeDate(Date(0), 0) + val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) val activity = mock() @@ -970,7 +978,7 @@ class ActivityLifecycleIntegrationTest { val span = fixture.transaction.children.first() assertEquals(span.operation, "app.start.warm") - assertSame(span.startDate, date) + assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) } @Test @@ -979,7 +987,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) - val date = SentryNanotimeDate(Date(0), 0) + val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) val activity = mock() @@ -987,7 +995,7 @@ class ActivityLifecycleIntegrationTest { val span = fixture.transaction.children.first() assertEquals(span.operation, "app.start.cold") - assertSame(span.startDate, date) + assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) } @Test @@ -996,7 +1004,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) - val date = SentryNanotimeDate(Date(0), 0) + val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) val activity = mock() @@ -1004,7 +1012,7 @@ class ActivityLifecycleIntegrationTest { val span = fixture.transaction.children.first() assertEquals(span.description, "Warm Start") - assertSame(span.startDate, date) + assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) } @Test @@ -1013,7 +1021,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) - val date = SentryNanotimeDate(Date(0), 0) + val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) val activity = mock() @@ -1021,7 +1029,7 @@ class ActivityLifecycleIntegrationTest { val span = fixture.transaction.children.first() assertEquals(span.description, "Cold Start") - assertSame(span.startDate, date) + assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) } @Test @@ -1030,7 +1038,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) - val date = SentryNanotimeDate(Date(0), 0) + val date = SentryNanotimeDate(Date(1), 0) setAppStartTime() val activity = mock() @@ -1488,8 +1496,13 @@ class ActivityLifecycleIntegrationTest { shadowOf(Looper.getMainLooper()).idle() } - private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(0), 0)) { + private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(1), 0)) { // set by SentryPerformanceProvider so forcing it here - AppStartState.getInstance().setAppStartTime(0, date) + val appStartTimeSpan = AppStartMetrics.getInstance().legacyAppStartTimeSpan + val millis = DateUtils.nanosToMillis(date.nanoTimestamp().toDouble()).toLong() + + appStartTimeSpan.setStartedAt(millis) + appStartTimeSpan.setStartUnixTimeMs(millis) + appStartTimeSpan.setStoppedAt(0) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt deleted file mode 100644 index 421274b42a..0000000000 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt +++ /dev/null @@ -1,94 +0,0 @@ -package io.sentry.android.core - -import io.sentry.SentryInstantDate -import io.sentry.SentryNanotimeDate -import java.util.Date -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull -import kotlin.test.assertSame -import kotlin.test.assertTrue - -class AppStartStateTest { - - @BeforeTest - fun `reset instance`() { - AppStartState.getInstance().resetInstance() - } - - @Test - fun `appStartInterval returns null if end time is not set`() { - val sut = AppStartState.getInstance() - - sut.setAppStartTime(0, SentryNanotimeDate(Date(0), 0)) - sut.setColdStart(true) - - assertNull(sut.appStartInterval) - } - - @Test - fun `appStartInterval returns null if start time is not set`() { - val sut = AppStartState.getInstance() - - sut.setAppStartEnd() - sut.setColdStart(true) - - assertNull(sut.appStartInterval) - } - - @Test - fun `appStartInterval returns null if coldStart is not set`() { - val sut = AppStartState.getInstance() - - sut.setAppStartTime(0, SentryNanotimeDate(Date(0), 0)) - sut.setAppStartEnd() - - assertNull(sut.appStartInterval) - } - - @Test - fun `do not overwrite app start values if already set`() { - val sut = AppStartState.getInstance() - - val date = SentryNanotimeDate() - sut.setAppStartTime(0, date) - sut.setAppStartTime(1, SentryInstantDate()) - - assertSame(date, sut.appStartTime) - } - - @Test - fun `do not overwrite cold start value if already set`() { - val sut = AppStartState.getInstance() - - sut.setColdStart(true) - sut.setColdStart(false) - - assertTrue(sut.isColdStart!!) - } - - @Test - fun `getAppStartInterval returns right calculation`() { - val sut = AppStartState.getInstance() - - val date = SentryNanotimeDate() - sut.setAppStartTime(100, date) - sut.setAppStartEnd(500) - sut.setColdStart(true) - - assertEquals(400, sut.appStartInterval) - } - - @Test - fun `getAppStartInterval returns null if more than 60s`() { - val sut = AppStartState.getInstance() - - val date = SentryNanotimeDate() - sut.setAppStartTime(100, date) - sut.setAppStartEnd(60100) - sut.setColdStart(true) - - assertNull(sut.appStartInterval) - } -} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 62d829403b..fe9ecabdb6 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -1318,4 +1318,29 @@ class ManifestMetadataReaderTest { // Assert assertTrue(fixture.options.isSendModules) } + + @Test + fun `applyMetadata reads starfish flag to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.ENABLE_STARFISH to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertTrue(fixture.options.isEnableStarfish) + } + + @Test + fun `applyMetadata reads starfish flag to options and keeps default if not found`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertFalse(fixture.options.isEnableStarfish) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 5935748045..0c76fa59f9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -3,12 +3,19 @@ package io.sentry.android.core import io.sentry.Hint import io.sentry.IHub import io.sentry.MeasurementUnit -import io.sentry.SentryNanotimeDate import io.sentry.SentryTracer +import io.sentry.SpanContext +import io.sentry.SpanId +import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_COLD import io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP +import io.sentry.android.core.performance.ActivityLifecycleTimeSpan +import io.sentry.android.core.performance.AppStartMetrics +import io.sentry.android.core.performance.AppStartMetrics.AppStartType import io.sentry.protocol.MeasurementValue +import io.sentry.protocol.SentrySpan import io.sentry.protocol.SentryTransaction import org.mockito.kotlin.any import org.mockito.kotlin.mock @@ -28,8 +35,12 @@ class PerformanceAndroidEventProcessorTest { lateinit var tracer: SentryTracer val activityFramesTracker = mock() - fun getSut(tracesSampleRate: Double? = 1.0): PerformanceAndroidEventProcessor { + fun getSut( + tracesSampleRate: Double? = 1.0, + enableStarfish: Boolean = false + ): PerformanceAndroidEventProcessor { options.tracesSampleRate = tracesSampleRate + options.isEnableStarfish = enableStarfish whenever(hub.options).thenReturn(options) tracer = SentryTracer(context, hub) return PerformanceAndroidEventProcessor(options, activityFramesTracker) @@ -40,15 +51,28 @@ class PerformanceAndroidEventProcessorTest { @BeforeTest fun `reset instance`() { - AppStartState.getInstance().resetInstance() + AppStartMetrics.getInstance().clear() } @Test fun `add cold start measurement`() { val sut = fixture.getSut() - var tr = getTransaction() - setAppStart() + var tr = getTransaction(AppStartType.COLD) + setAppStart(fixture.options) + + tr = sut.process(tr, Hint()) + + assertTrue(tr.measurements.containsKey(MeasurementValue.KEY_APP_START_COLD)) + } + + @Test + fun `add cold start measurement for starfish`() { + val sut = fixture.getSut() + fixture.options.isEnableStarfish = true + + var tr = getTransaction(AppStartType.COLD) + setAppStart(fixture.options) tr = sut.process(tr, Hint()) @@ -59,8 +83,8 @@ class PerformanceAndroidEventProcessorTest { fun `add warm start measurement`() { val sut = fixture.getSut() - var tr = getTransaction("app.start.warm") - setAppStart(false) + var tr = getTransaction(AppStartType.WARM) + setAppStart(fixture.options, false) tr = sut.process(tr, Hint()) @@ -71,8 +95,8 @@ class PerformanceAndroidEventProcessorTest { fun `set app cold start unit measurement`() { val sut = fixture.getSut() - var tr = getTransaction() - setAppStart() + var tr = getTransaction(AppStartType.COLD) + setAppStart(fixture.options) tr = sut.process(tr, Hint()) @@ -84,12 +108,12 @@ class PerformanceAndroidEventProcessorTest { fun `do not add app start metric twice`() { val sut = fixture.getSut() - var tr1 = getTransaction() - setAppStart(false) + var tr1 = getTransaction(AppStartType.COLD) + setAppStart(fixture.options, false) tr1 = sut.process(tr1, Hint()) - var tr2 = getTransaction() + var tr2 = getTransaction(AppStartType.UNKNOWN) tr2 = sut.process(tr2, Hint()) assertTrue(tr1.measurements.containsKey(MeasurementValue.KEY_APP_START_WARM)) @@ -100,7 +124,7 @@ class PerformanceAndroidEventProcessorTest { fun `do not add app start metric if its not ready`() { val sut = fixture.getSut() - var tr = getTransaction() + var tr = getTransaction(AppStartType.UNKNOWN) tr = sut.process(tr, Hint()) @@ -111,7 +135,7 @@ class PerformanceAndroidEventProcessorTest { fun `do not add app start metric if performance is disabled`() { val sut = fixture.getSut(tracesSampleRate = null) - var tr = getTransaction() + var tr = getTransaction(AppStartType.COLD) tr = sut.process(tr, Hint()) @@ -122,7 +146,7 @@ class PerformanceAndroidEventProcessorTest { fun `do not add app start metric if no app_start span`() { val sut = fixture.getSut(tracesSampleRate = null) - var tr = getTransaction("task") + var tr = getTransaction(AppStartType.UNKNOWN) tr = sut.process(tr, Hint()) @@ -132,7 +156,7 @@ class PerformanceAndroidEventProcessorTest { @Test fun `do not add slow and frozen frames if not auto transaction`() { val sut = fixture.getSut() - var tr = getTransaction("task") + var tr = getTransaction(AppStartType.UNKNOWN) tr = sut.process(tr, Hint()) @@ -142,7 +166,7 @@ class PerformanceAndroidEventProcessorTest { @Test fun `do not add slow and frozen frames if tracing is disabled`() { val sut = fixture.getSut(null) - var tr = getTransaction("task") + var tr = getTransaction(AppStartType.UNKNOWN) tr = sut.process(tr, Hint()) @@ -156,7 +180,12 @@ class PerformanceAndroidEventProcessorTest { val tracer = SentryTracer(context, fixture.hub) var tr = SentryTransaction(tracer) - val metrics = mapOf(MeasurementValue.KEY_FRAMES_TOTAL to MeasurementValue(1f, MeasurementUnit.Duration.MILLISECOND.apiName())) + val metrics = mapOf( + MeasurementValue.KEY_FRAMES_TOTAL to MeasurementValue( + 1f, + MeasurementUnit.Duration.MILLISECOND.apiName() + ) + ) whenever(fixture.activityFramesTracker.takeMetrics(any())).thenReturn(metrics) tr = sut.process(tr, Hint()) @@ -164,14 +193,90 @@ class PerformanceAndroidEventProcessorTest { assertTrue(tr.measurements.containsKey(MeasurementValue.KEY_FRAMES_TOTAL)) } - private fun setAppStart(coldStart: Boolean = true) { - AppStartState.getInstance().setColdStart(coldStart) - AppStartState.getInstance().setAppStartTime(0, SentryNanotimeDate()) - AppStartState.getInstance().setAppStartEnd() + @Test + fun `adds app start metrics to app start txn`() { + // given some app start metrics + val appStartMetrics = AppStartMetrics.getInstance() + appStartMetrics.appStartType = AppStartType.COLD + appStartMetrics.appStartTimeSpan.setStartedAt(123) + appStartMetrics.appStartTimeSpan.setStoppedAt(456) + + val activityTimeSpan = ActivityLifecycleTimeSpan() + activityTimeSpan.onCreate.description = "MainActivity.onCreate" + activityTimeSpan.onStart.description = "MainActivity.onStart" + + activityTimeSpan.onCreate.setStartedAt(200) + activityTimeSpan.onStart.setStartedAt(220) + activityTimeSpan.onStart.setStoppedAt(240) + activityTimeSpan.onCreate.setStoppedAt(260) + appStartMetrics.addActivityLifecycleTimeSpans(activityTimeSpan) + + // when an activity transaction is created + val sut = fixture.getSut(enableStarfish = true) + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.hub) + var tr = SentryTransaction(tracer) + + // and it contains an app start signal + tr.spans.add( + SentrySpan( + 0.0, + 1.0, + tr.contexts.trace!!.traceId, + SpanId(), + null, + APP_START_COLD, + "App Start", + SpanStatus.OK, + null, + emptyMap(), + null + ) + ) + + // then the app start metrics should be attached + tr = sut.process(tr, Hint()) + + assertTrue( + tr.spans.any { + UI_LOAD_OP == it.op && "Activity" == it.description + } + ) + assertTrue( + tr.spans.any { + UI_LOAD_OP == it.op && "MainActivity.onCreate" == it.description + } + ) + assertTrue( + tr.spans.any { + UI_LOAD_OP == it.op && "MainActivity.onStart" == it.description + } + ) + } + + private fun setAppStart(options: SentryAndroidOptions, coldStart: Boolean = true) { + AppStartMetrics.getInstance().apply { + appStartType = when (coldStart) { + true -> AppStartType.COLD + false -> AppStartType.WARM + } + val timeSpan = + if (options.isEnableStarfish) appStartTimeSpan else legacyAppStartTimeSpan + timeSpan.apply { + setStartedAt(1) + setStoppedAt(2) + } + } } - private fun getTransaction(op: String = "app.start.cold"): SentryTransaction { - fixture.tracer.startChild(op) - return SentryTransaction(fixture.tracer) + private fun getTransaction(type: AppStartType): SentryTransaction { + val op = when (type) { + AppStartType.COLD -> "app.start.cold" + AppStartType.WARM -> "app.start.warm" + AppStartType.UNKNOWN -> "ui.load" + } + val txn = SentryTransaction(fixture.tracer) + txn.contexts.trace = SpanContext(op, TracesSamplingDecision(false)) + return txn } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt index 07e16af252..540e667e5d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt @@ -133,6 +133,19 @@ class SentryAndroidOptionsTest { assertNull(sentryOptions.nativeSdkName) } + @Test + fun `starfish is disabled by default`() { + val sentryOptions = SentryAndroidOptions() + assertFalse(sentryOptions.isEnableStarfish) + } + + @Test + fun `starfish can be enabled`() { + val sentryOptions = SentryAndroidOptions() + sentryOptions.isEnableStarfish = true + assertTrue(sentryOptions.isEnableStarfish) + } + private class CustomDebugImagesLoader : IDebugImagesLoader { override fun loadDebugImages(): List? = null override fun clearDebugImages() {} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index b441c7789a..2a9e20be29 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -5,6 +5,7 @@ import android.app.Application import android.app.ApplicationExitInfo import android.content.Context import android.os.Bundle +import android.os.SystemClock import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb @@ -21,6 +22,7 @@ import io.sentry.Session import io.sentry.ShutdownHookIntegration import io.sentry.UncaughtExceptionHandlerIntegration import io.sentry.android.core.cache.AndroidEnvelopeCache +import io.sentry.android.core.performance.AppStartMetrics import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.timber.SentryTimberIntegration import io.sentry.cache.IEnvelopeCache @@ -60,6 +62,7 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -150,7 +153,7 @@ class SentryAndroidTest { @BeforeTest fun `set up`() { Sentry.close() - AppStartState.getInstance().resetInstance() + AppStartMetrics.getInstance().clear() context = ApplicationProvider.getApplicationContext() val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? fixture.shadowActivityManager = Shadow.extract(activityManager) @@ -202,10 +205,15 @@ class SentryAndroidTest { fixture.initSut(autoInit = true) // done by ActivityLifecycleIntegration so forcing it here - AppStartState.getInstance().setAppStartEnd() - AppStartState.getInstance().setColdStart(true) + AppStartMetrics.getInstance().apply { + appStartType = AppStartMetrics.AppStartType.COLD + appStartTimeSpan.apply { + setStartedAt(1) + setStoppedAt(1 + SystemClock.uptimeMillis()) + } + } - assertNotNull(AppStartState.getInstance().appStartInterval) + assertNotEquals(0, AppStartMetrics.getInstance().appStartTimeSpan.durationMs) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index 89bc9d6037..2543285e23 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -6,6 +6,7 @@ import io.sentry.Breadcrumb import io.sentry.Sentry import io.sentry.SentryLevel import io.sentry.SentryOptions +import io.sentry.android.core.performance.AppStartMetrics import org.junit.runner.RunWith import java.lang.RuntimeException import kotlin.test.BeforeTest @@ -40,7 +41,7 @@ class SentryLogcatAdapterTest { @BeforeTest fun `set up`() { Sentry.close() - AppStartState.getInstance().resetInstance() + AppStartMetrics.getInstance().clear() breadcrumbs.clear() fixture.initSut { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index cf3ca7c2ea..256b15957f 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -1,100 +1,115 @@ package io.sentry.android.core -import android.app.Application import android.content.pm.ProviderInfo +import android.os.Build import android.os.Bundle import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.SentryNanotimeDate +import io.sentry.android.core.performance.AppStartMetrics +import io.sentry.android.core.performance.AppStartMetrics.AppStartType import org.junit.runner.RunWith -import org.mockito.kotlin.any import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import java.util.Date +import org.robolectric.annotation.Config +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull import kotlin.test.assertTrue +@Implements(android.os.Process::class) +class SentryShadowProcess { + + companion object { + + private var startupTimeMillis: Long = 0 + + fun setStartUptimeMillis(value: Long) { + startupTimeMillis = value + } + + @Suppress("unused") + @Implementation + @JvmStatic + fun getStartUptimeMillis(): Long { + return startupTimeMillis + } + } +} + @RunWith(AndroidJUnit4::class) +@Config( + sdk = [Build.VERSION_CODES.N], + shadows = [SentryShadowProcess::class] +) class SentryPerformanceProviderTest { @BeforeTest fun `set up`() { - AppStartState.getInstance().resetInstance() + AppStartMetrics.getInstance().clear() + SentryShadowProcess.setStartUptimeMillis(1234) } @Test - fun `provider sets app start`() { - val providerInfo = ProviderInfo() - - val mockContext = ContextUtilsTest.createMockContext() - providerInfo.authority = AUTHORITY - - val providerAppStartMillis = 10L - val providerAppStartTime = SentryNanotimeDate(Date(0), 0) - SentryPerformanceProvider.setAppStartTime(providerAppStartMillis, providerAppStartTime) + fun `provider starts appStartTimeSpan`() { + assertTrue(AppStartMetrics.getInstance().appStartTimeSpan.hasNotStarted()) + setupProvider() + assertTrue(AppStartMetrics.getInstance().appStartTimeSpan.hasStarted()) + } - val provider = SentryPerformanceProvider() - provider.attachInfo(mockContext, providerInfo) + @Test + fun `provider sets cold start based on first activity`() { + val provider = setupProvider() - // done by ActivityLifecycleIntegration so forcing it here - val lifecycleAppEndMillis = 20L - AppStartState.getInstance().setAppStartEnd(lifecycleAppEndMillis) - AppStartState.getInstance().setColdStart(true) + // up until this point app start is not known + assertEquals(AppStartType.UNKNOWN, AppStartMetrics.getInstance().appStartType) - assertEquals(10L, AppStartState.getInstance().appStartInterval) + // when there's no saved state + provider.activityCallback!!.onActivityCreated(mock(), null) + // then app start should be cold + assertEquals(AppStartType.COLD, AppStartMetrics.getInstance().appStartType) } @Test - fun `provider sets first activity as cold start`() { - val providerInfo = ProviderInfo() + fun `provider sets warm start based on first activity`() { + val provider = setupProvider() - val mockContext = ContextUtilsTest.createMockContext() - providerInfo.authority = AUTHORITY + // up until this point app start is not known + assertEquals(AppStartType.UNKNOWN, AppStartMetrics.getInstance().appStartType) - val provider = SentryPerformanceProvider() - provider.attachInfo(mockContext, providerInfo) - - provider.onActivityCreated(mock(), null) + // when there's a saved state + provider.activityCallback!!.onActivityCreated(mock(), Bundle()) - assertTrue(AppStartState.getInstance().isColdStart!!) + // then app start should be warm + assertEquals(AppStartType.WARM, AppStartMetrics.getInstance().appStartType) } @Test - fun `provider sets first activity as warm start`() { - val providerInfo = ProviderInfo() + fun `provider sets keeps startup state even if multiple activities are launched`() { + val provider = setupProvider() - val mockContext = ContextUtilsTest.createMockContext() - providerInfo.authority = AUTHORITY + // when there's a saved state + provider.activityCallback!!.onActivityCreated(mock(), Bundle()) - val provider = SentryPerformanceProvider() - provider.attachInfo(mockContext, providerInfo) + // then app start should be warm + assertEquals(AppStartType.WARM, AppStartMetrics.getInstance().appStartType) - provider.onActivityCreated(mock(), Bundle()) + // when another activity is launched cold + provider.activityCallback!!.onActivityCreated(mock(), null) - assertFalse(AppStartState.getInstance().isColdStart!!) + // then app start should remain warm + assertEquals(AppStartType.WARM, AppStartMetrics.getInstance().appStartType) } - @Test - fun `provider sets app start end on first activity resume, and unregisters afterwards`() { + private fun setupProvider(): SentryPerformanceProvider { val providerInfo = ProviderInfo() val mockContext = ContextUtilsTest.createMockContext(true) providerInfo.authority = AUTHORITY + // calls onCreate val provider = SentryPerformanceProvider() provider.attachInfo(mockContext, providerInfo) - - provider.onActivityCreated(mock(), Bundle()) - provider.onActivityResumed(mock()) - - assertNotNull(AppStartState.getInstance().appStartInterval) - assertNotNull(AppStartState.getInstance().appStartEndTime) - - verify((mockContext.applicationContext as Application)) - .unregisterActivityLifecycleCallbacks(any()) + return provider } companion object { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index 095da5f32a..2133ad9729 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -4,8 +4,8 @@ import io.sentry.NoOpLogger import io.sentry.SentryEnvelope import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint import io.sentry.android.core.AnrV2Integration.AnrV2Hint -import io.sentry.android.core.AppStartState import io.sentry.android.core.SentryAndroidOptions +import io.sentry.android.core.performance.AppStartMetrics import io.sentry.cache.EnvelopeCache import io.sentry.transport.ICurrentDateProvider import io.sentry.util.HintUtils @@ -48,7 +48,10 @@ class AndroidEnvelopeCacheTest { lastReportedAnrFile = File(options.cacheDirPath!!, AndroidEnvelopeCache.LAST_ANR_REPORT) if (appStartMillis != null) { - AppStartState.getInstance().setAppStartMillis(appStartMillis) + AppStartMetrics.getInstance().apply { + appStartTimeSpan.setStartedAt(appStartMillis) + appStartTimeSpan.setStartUnixTimeMs(appStartMillis) + } } if (currentTimeMillis != null) { whenever(dateProvider.currentTimeMillis).thenReturn(currentTimeMillis) @@ -62,7 +65,7 @@ class AndroidEnvelopeCacheTest { @BeforeTest fun `set up`() { - AppStartState.getInstance().reset() + AppStartMetrics.getInstance().clear() } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpanTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpanTest.kt new file mode 100644 index 0000000000..beeed5e9f1 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpanTest.kt @@ -0,0 +1,37 @@ +package io.sentry.android.core.performance + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ActivityLifecycleTimeSpanTest { + @Test + fun `init does not auto-start the spans`() { + val span = ActivityLifecycleTimeSpan() + + assertTrue(span.onCreate.hasNotStarted()) + assertTrue(span.onStart.hasNotStarted()) + } + + @Test + fun `spans are compareable`() { + // given some spans + val spanA = ActivityLifecycleTimeSpan() + spanA.onCreate.setStartedAt(1) + + val spanB = ActivityLifecycleTimeSpan() + spanB.onCreate.setStartedAt(2) + + val spanC = ActivityLifecycleTimeSpan() + spanC.onCreate.setStartedAt(3) + + // when put into an list out of order + // then sorted + val spans = listOf(spanB, spanC, spanA).sorted() + + // puts them back in order + assertEquals(spanA, spans[0]) + assertEquals(spanB, spans[1]) + assertEquals(spanC, spans[2]) + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt new file mode 100644 index 0000000000..adf4b1bd17 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt @@ -0,0 +1,154 @@ +package io.sentry.android.core.performance + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.DateUtils +import org.junit.runner.RunWith +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class TimeSpanTest { + @Test + fun `init default state`() { + val span = TimeSpan() + + assertTrue(span.hasNotStarted()) + assertTrue(span.hasNotStopped()) + + assertFalse(span.hasStarted()) + assertFalse(span.hasStopped()) + } + + @Test + fun `spans are compareable`() { + // given some spans + val spanA = TimeSpan() + spanA.setStartedAt(1) + + val spanB = TimeSpan() + spanA.setStartedAt(2) + + assertEquals(1, spanA.compareTo(spanB)) + } + + @Test + fun `spans reset`() { + val span = TimeSpan().apply { + setStartedAt(1) + setStoppedAt(2) + } + span.reset() + + assertTrue(span.hasNotStarted()) + assertTrue(span.hasNotStopped()) + assertNull(span.description) + } + + @Test + fun `spans description`() { + val span = TimeSpan().apply { + description = "Hello World" + } + assertEquals("Hello World", span.description) + } + + @Test + fun `span duration`() { + val span = TimeSpan().apply { + setStartedAt(1) + setStoppedAt(10) + } + assertEquals(9, span.durationMs) + } + + @Test + fun `span has no duration if not started`() { + assertEquals(0, TimeSpan().durationMs) + } + + @Test + fun `span has no duration if not stopped`() { + val span = TimeSpan().apply { + setStartedAt(1) + } + assertEquals(0, span.durationMs) + } + + @Test + fun `span unix timestamp is correctly set`() { + val span = TimeSpan() + + span.setStartedAt(100) + span.setStoppedAt(200) + + assertEquals(100, span.projectedStopTimestampMs - span.startTimestampMs) + assertEquals(100, span.durationMs) + } + + @Test + fun `span stop time is 0 if not started`() { + val span = TimeSpan() + assertEquals(0, span.projectedStopTimestampMs) + assertEquals(0.0, span.projectedStopTimestampS) + } + + @Test + fun `span start and stop time is translated correctly into seconds`() { + val span = TimeSpan() + span.setStartedAt(1234) + span.setStoppedAt(1234) + + assertEquals(span.startTimestampMs / 1000.0, span.startTimestampS, 0.001) + assertEquals(span.projectedStopTimestampMs / 1000.0, span.projectedStopTimestampS, 0.001) + } + + @Test + fun `span start and stop time is translated correctly into SentryDate`() { + val span = TimeSpan() + assertNull(span.startTimestamp) + + span.setStartedAt(1234) + span.setStoppedAt(1234) + assertNotNull(span.startTimestamp) + + assertEquals( + span.startTimestampMs.toDouble(), + DateUtils.nanosToMillis(span.startTimestamp!!.nanoTimestamp().toDouble()), + 0.001 + ) + } + + @Test + fun `span start starts the timespan`() { + val span = TimeSpan() + span.start() + + assertTrue(span.hasStarted()) + assertFalse(span.hasNotStarted()) + } + + @Test + fun `span stop stops the timespan`() { + val span = TimeSpan() + span.start() + + assertFalse(span.hasStopped()) + + span.stop() + + assertTrue(span.hasStopped()) + assertFalse(span.hasNotStopped()) + } + + @Test + fun `span start uptime getter`() { + val span = TimeSpan() + span.setStartedAt(1234) + + assertEquals(1234, span.startUptimeMs) + } +} diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 8c6c9662a4..e98065be6d 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -151,5 +151,7 @@ + + From 69aca749948e6f93f5cacad04914d262b785cb18 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 24 Nov 2023 07:59:51 +0100 Subject: [PATCH 02/15] Update Changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f8f969fb..a6250561b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- (Internal, Experimental) Attach spans for Application, ContentProvider, and Activities to app-start ([#3057](https://github.com/getsentry/sentry-java/pull/3057)) + ## 6.34.0 ### Features From e856b81aa5c8de6834aac8f6b48fb07b7ab3e6aa Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 24 Nov 2023 08:08:50 +0100 Subject: [PATCH 03/15] Fix tests --- .../android/core/cache/AndroidEnvelopeCacheTest.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index 2133ad9729..7448c215c7 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -49,8 +49,13 @@ class AndroidEnvelopeCacheTest { if (appStartMillis != null) { AppStartMetrics.getInstance().apply { - appStartTimeSpan.setStartedAt(appStartMillis) - appStartTimeSpan.setStartUnixTimeMs(appStartMillis) + if (options.isEnableStarfish) { + appStartTimeSpan.setStartedAt(appStartMillis) + appStartTimeSpan.setStartUnixTimeMs(appStartMillis) + } else { + legacyAppStartTimeSpan.setStartedAt(appStartMillis) + legacyAppStartTimeSpan.setStartUnixTimeMs(appStartMillis) + } } } if (currentTimeMillis != null) { From aea2a3693ba0e3219c5df10937485ab8c3adf419 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 28 Nov 2023 14:38:14 +0100 Subject: [PATCH 04/15] Implement PR feedback --- .../PerformanceAndroidEventProcessor.java | 93 +++++++------------ 1 file changed, 35 insertions(+), 58 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 34e4ae5ea0..3cf159de9d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -131,7 +131,7 @@ private boolean hasAppStartSpan(final @NotNull SentryTransaction txn) { private void attachColdAppStartSpans( final @NotNull AppStartMetrics appStartMetrics, final @NotNull SentryTransaction txn) { - // data will be filled anyway only for cold app starts + // data will be filled only for cold app starts if (appStartMetrics.getAppStartType() != AppStartMetrics.AppStartType.COLD) { return; } @@ -145,19 +145,7 @@ private void attachColdAppStartSpans( // Application.onCreate final @NotNull TimeSpan appOnCreate = appStartMetrics.getApplicationOnCreateTimeSpan(); if (appOnCreate.hasStopped()) { - final SentrySpan span = - new SentrySpan( - appOnCreate.getStartTimestampS(), - appOnCreate.getProjectedStopTimestampS(), - traceId, - new SpanId(), - null, - UI_LOAD_OP, - appOnCreate.getDescription(), - SpanStatus.OK, - APP_METRICS_ORIGN, - new HashMap<>(), - null); + final SentrySpan span = timeSpanToSentrySpan(appOnCreate, null, traceId); txn.getSpans().add(span); } @@ -175,27 +163,17 @@ private void attachColdAppStartSpans( new SpanId(), null, UI_LOAD_OP, - "ContentProvider", + "Content Providers", SpanStatus.OK, APP_METRICS_ORIGN, new HashMap<>(), null); txn.getSpans().add(contentProviderRootSpan); for (final @NotNull TimeSpan contentProvider : contentProviderOnCreates) { - final SentrySpan contentProviderSpan = - new SentrySpan( - contentProvider.getStartTimestampS(), - contentProvider.getProjectedStopTimestampS(), - traceId, - new SpanId(), - contentProviderRootSpan.getSpanId(), - UI_LOAD_OP, - contentProvider.getDescription(), - SpanStatus.OK, - APP_METRICS_ORIGN, - new HashMap<>(), - null); - txn.getSpans().add(contentProviderSpan); + txn.getSpans() + .add( + timeSpanToSentrySpan( + contentProvider, contentProviderRootSpan.getSpanId(), traceId)); } } @@ -214,7 +192,7 @@ private void attachColdAppStartSpans( new SpanId(), null, UI_LOAD_OP, - "Activity", + "Activities", SpanStatus.OK, APP_METRICS_ORIGN, new HashMap<>(), @@ -223,38 +201,37 @@ private void attachColdAppStartSpans( for (ActivityLifecycleTimeSpan activityTimeSpan : activityLifecycleTimeSpans) { if (activityTimeSpan.onCreate.hasStarted() && activityTimeSpan.onCreate.hasStopped()) { - final SentrySpan onCreateSpan = - new SentrySpan( - activityTimeSpan.onCreate.getStartTimestampS(), - activityTimeSpan.onCreate.getProjectedStopTimestampS(), - traceId, - new SpanId(), - activityRootSpan.getSpanId(), - UI_LOAD_OP, - activityTimeSpan.onCreate.getDescription(), - SpanStatus.OK, - APP_METRICS_ORIGN, - new HashMap<>(), - null); - txn.getSpans().add(onCreateSpan); + txn.getSpans() + .add( + timeSpanToSentrySpan( + activityTimeSpan.onCreate, activityRootSpan.getSpanId(), traceId)); } if (activityTimeSpan.onStart.hasStarted() && activityTimeSpan.onStart.hasStopped()) { - final SentrySpan onStartSpan = - new SentrySpan( - activityTimeSpan.onStart.getStartTimestampS(), - activityTimeSpan.onStart.getProjectedStopTimestampS(), - traceId, - new SpanId(), - activityRootSpan.getSpanId(), - UI_LOAD_OP, - activityTimeSpan.onStart.getDescription(), - SpanStatus.OK, - APP_METRICS_ORIGN, - new HashMap<>(), - null); - txn.getSpans().add(onStartSpan); + txn.getSpans() + .add( + timeSpanToSentrySpan( + activityTimeSpan.onStart, activityRootSpan.getSpanId(), traceId)); } } } } + + @NotNull + private static SentrySpan timeSpanToSentrySpan( + final @NotNull TimeSpan span, + final @Nullable SpanId parentSpanId, + final @NotNull SentryId traceId) { + return new SentrySpan( + span.getStartTimestampS(), + span.getProjectedStopTimestampS(), + traceId, + new SpanId(), + parentSpanId, + ActivityLifecycleIntegration.UI_LOAD_OP, + span.getDescription(), + SpanStatus.OK, + APP_METRICS_ORIGN, + new HashMap<>(), + null); + } } From 80ec3f25524cb15725cea9fd32ea801a3ff13f5a Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 30 Nov 2023 16:18:50 +0100 Subject: [PATCH 05/15] Fix merge --- .../api/sentry-android-core.api | 11 ----------- .../core/ActivityLifecycleIntegration.java | 18 ++++-------------- .../core/SentryPerformanceProviderTest.kt | 1 + 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index f6b815fde5..15b26057ce 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -119,17 +119,6 @@ public final class io/sentry/android/core/AppLifecycleIntegration : io/sentry/In public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } -public final class io/sentry/android/core/AppStartState { - public fun getAppStartEndTime ()Lio/sentry/SentryDate; - public fun getAppStartInterval ()Ljava/lang/Long; - public fun getAppStartMillis ()Ljava/lang/Long; - public fun getAppStartTime ()Lio/sentry/SentryDate; - public static fun getInstance ()Lio/sentry/android/core/AppStartState; - public fun isColdStart ()Ljava/lang/Boolean; - public fun reset ()V - public fun setAppStartMillis (J)V -} - public final class io/sentry/android/core/AppState { public static fun getInstance ()Lio/sentry/android/core/AppState; public fun isInBackground ()Ljava/lang/Boolean; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index fd0801ff93..474bf34d1f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -416,16 +416,6 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) { @Override public synchronized void onActivityResumed(final @NotNull Activity activity) { if (performanceEnabled) { - // app start span - @Nullable final SentryDate appStartStartTime = AppStartState.getInstance().getAppStartTime(); - @Nullable final SentryDate appStartEndTime = AppStartState.getInstance().getAppStartEndTime(); - // in case the SentryPerformanceProvider is disabled it does not set the app start times, - // and we need to set the end time manually here, - // the start time gets set manually in SentryAndroid.init() - if (appStartStartTime != null && appStartEndTime == null) { - AppStartState.getInstance().setAppStartEnd(); - } - finishAppStartSpan(); final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity); final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity); @@ -556,13 +546,13 @@ private void cancelTtfdAutoClose() { private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable ISpan ttidSpan) { // app start span - @Nullable final SentryDate appStartStartTime = AppStartState.getInstance().getAppStartTime(); - @Nullable final SentryDate appStartEndTime = AppStartState.getInstance().getAppStartEndTime(); + final @NotNull TimeSpan appStartTimeSpan = getAppStartTimeSpan(); + // in case the SentryPerformanceProvider is disabled it does not set the app start times, // and we need to set the end time manually here, // the start time gets set manually in SentryAndroid.init() - if (appStartStartTime != null && appStartEndTime == null) { - AppStartState.getInstance().setAppStartEnd(); + if (appStartTimeSpan.hasStarted() && appStartTimeSpan.hasNotStopped()) { + appStartTimeSpan.stop(); } finishAppStartSpan(); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index b99847207b..81195370a8 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -12,6 +12,7 @@ import io.sentry.android.core.performance.AppStartMetrics import io.sentry.android.core.performance.AppStartMetrics.AppStartType import org.junit.runner.RunWith import org.mockito.kotlin.mock +import org.robolectric.Shadows import org.robolectric.annotation.Config import org.robolectric.annotation.Implementation import org.robolectric.annotation.Implements From 1a0fcee3ce9ee0074189404ccb0f41016cfdcafd Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 30 Nov 2023 21:59:23 +0100 Subject: [PATCH 06/15] Address PR feedback, improve tests --- .../api/sentry-android-core.api | 19 +-- .../core/ActivityLifecycleIntegration.java | 2 +- .../core/DefaultAndroidEventProcessor.java | 2 +- .../android/core/InternalSentrySdk.java | 2 +- .../PerformanceAndroidEventProcessor.java | 26 ++-- .../io/sentry/android/core/SentryAndroid.java | 15 +- .../android/core/SentryAndroidOptions.java | 4 +- .../core/SentryPerformanceProvider.java | 40 ++--- .../core/cache/AndroidEnvelopeCache.java | 2 +- .../internal/util/FirstDrawDoneListener.java | 32 ++++ .../ActivityLifecycleTimeSpan.java | 20 ++- .../core/performance/AppStartMetrics.java | 13 +- .../core/performance/NextDrawListener.java | 138 ------------------ .../android/core/performance/TimeSpan.java | 15 +- .../core/ActivityLifecycleIntegrationTest.kt | 20 +-- .../PerformanceAndroidEventProcessorTest.kt | 2 +- .../sentry/android/core/SentryAndroidTest.kt | 18 +++ .../core/SentryPerformanceProviderTest.kt | 22 --- .../android/core/SentryShadowProcess.kt | 24 +++ .../core/cache/AndroidEnvelopeCacheTest.kt | 4 +- .../util/FirstDrawDoneListenerTest.kt | 50 +++++++ .../ActivityLifecycleTimeSpanTest.kt | 17 +++ .../core/performance/AppStartMetricsTest.kt | 57 ++++++++ .../android/core/performance/TimeSpanTest.kt | 6 +- 24 files changed, 304 insertions(+), 246 deletions(-) delete mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/performance/NextDrawListener.java create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/SentryShadowProcess.kt create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 15b26057ce..bfa3a0bbe4 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -396,11 +396,11 @@ public class io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapte } public class io/sentry/android/core/performance/ActivityLifecycleTimeSpan : java/lang/Comparable { - public final field onCreate Lio/sentry/android/core/performance/TimeSpan; - public final field onStart Lio/sentry/android/core/performance/TimeSpan; public fun ()V public fun compareTo (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)I public synthetic fun compareTo (Ljava/lang/Object;)I + public final fun getOnCreate ()Lio/sentry/android/core/performance/TimeSpan; + public final fun getOnStart ()Lio/sentry/android/core/performance/TimeSpan; } public class io/sentry/android/core/performance/AppStartMetrics { @@ -413,7 +413,7 @@ public class io/sentry/android/core/performance/AppStartMetrics { public fun getApplicationOnCreateTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; public fun getContentProviderOnCreateTimeSpans ()Ljava/util/List; public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics; - public fun getLegacyAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; + public fun getSdkAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; public fun isAppLaunchedInForeground ()Z public static fun onApplicationCreate (Landroid/app/Application;)V public static fun onApplicationPostCreate (Landroid/app/Application;)V @@ -430,15 +430,6 @@ public final class io/sentry/android/core/performance/AppStartMetrics$AppStartTy public static fun values ()[Lio/sentry/android/core/performance/AppStartMetrics$AppStartType; } -public class io/sentry/android/core/performance/NextDrawListener : android/view/View$OnAttachStateChangeListener, android/view/ViewTreeObserver$OnDrawListener { - protected fun (Landroid/os/Handler;Ljava/lang/Runnable;)V - public static fun forActivity (Landroid/app/Activity;Ljava/lang/Runnable;)Lio/sentry/android/core/performance/NextDrawListener; - public fun onDraw ()V - public fun onViewAttachedToWindow (Landroid/view/View;)V - public fun onViewDetachedFromWindow (Landroid/view/View;)V - public fun unregister ()V -} - public class io/sentry/android/core/performance/TimeSpan : java/lang/Comparable { public fun ()V public fun compareTo (Lio/sentry/android/core/performance/TimeSpan;)I @@ -447,10 +438,10 @@ public class io/sentry/android/core/performance/TimeSpan : java/lang/Comparable public fun getDurationMs ()J public fun getProjectedStopTimestamp ()Lio/sentry/SentryDate; public fun getProjectedStopTimestampMs ()J - public fun getProjectedStopTimestampS ()D + public fun getProjectedStopTimestampSecs ()D public fun getStartTimestamp ()Lio/sentry/SentryDate; public fun getStartTimestampMs ()J - public fun getStartTimestampS ()D + public fun getStartTimestampSecs ()D public fun getStartUptimeMs ()J public fun hasNotStarted ()Z public fun hasNotStopped ()Z diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 474bf34d1f..e40b6a5a60 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -691,7 +691,7 @@ private void finishAppStartSpan() { final @NotNull TimeSpan appStartTimeSpan = options.isEnableStarfish() ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); return appStartTimeSpan; } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 93c1b65e96..9f392853de 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -204,7 +204,7 @@ private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { final @NotNull TimeSpan appStartTimeSpan = options.isEnableStarfish() ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); if (appStartTimeSpan.hasStarted()) { app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp())); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index b89940be63..7b9c33753c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -105,7 +105,7 @@ public static Map serializeScope( final @NotNull TimeSpan appStartTimeSpan = options.isEnableStarfish() ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); if (appStartTimeSpan.hasStarted()) { app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp())); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 3cf159de9d..3ed7968e3a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -73,7 +73,7 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { final @NotNull TimeSpan appStartTimeSpan = options.isEnableStarfish() ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); final long appStartUpInterval = appStartTimeSpan.getDurationMs(); // if appStartUpInterval is 0, metrics are not ready to be sent @@ -155,10 +155,10 @@ private void attachColdAppStartSpans( if (!contentProviderOnCreates.isEmpty()) { final @NotNull SentrySpan contentProviderRootSpan = new SentrySpan( - contentProviderOnCreates.get(0).getStartTimestampS(), + contentProviderOnCreates.get(0).getStartTimestampSecs(), contentProviderOnCreates .get(contentProviderOnCreates.size() - 1) - .getProjectedStopTimestampS(), + .getProjectedStopTimestampSecs(), traceId, new SpanId(), null, @@ -183,11 +183,11 @@ private void attachColdAppStartSpans( if (!activityLifecycleTimeSpans.isEmpty()) { final SentrySpan activityRootSpan = new SentrySpan( - activityLifecycleTimeSpans.get(0).onCreate.getStartTimestampS(), + activityLifecycleTimeSpans.get(0).getOnCreate().getStartTimestampSecs(), activityLifecycleTimeSpans .get(activityLifecycleTimeSpans.size() - 1) - .onStart - .getProjectedStopTimestampS(), + .getOnStart() + .getProjectedStopTimestampSecs(), traceId, new SpanId(), null, @@ -200,17 +200,19 @@ private void attachColdAppStartSpans( txn.getSpans().add(activityRootSpan); for (ActivityLifecycleTimeSpan activityTimeSpan : activityLifecycleTimeSpans) { - if (activityTimeSpan.onCreate.hasStarted() && activityTimeSpan.onCreate.hasStopped()) { + if (activityTimeSpan.getOnCreate().hasStarted() + && activityTimeSpan.getOnCreate().hasStopped()) { txn.getSpans() .add( timeSpanToSentrySpan( - activityTimeSpan.onCreate, activityRootSpan.getSpanId(), traceId)); + activityTimeSpan.getOnCreate(), activityRootSpan.getSpanId(), traceId)); } - if (activityTimeSpan.onStart.hasStarted() && activityTimeSpan.onStart.hasStopped()) { + if (activityTimeSpan.getOnStart().hasStarted() + && activityTimeSpan.getOnStart().hasStopped()) { txn.getSpans() .add( timeSpanToSentrySpan( - activityTimeSpan.onStart, activityRootSpan.getSpanId(), traceId)); + activityTimeSpan.getOnStart(), activityRootSpan.getSpanId(), traceId)); } } } @@ -222,8 +224,8 @@ private static SentrySpan timeSpanToSentrySpan( final @Nullable SpanId parentSpanId, final @NotNull SentryId traceId) { return new SentrySpan( - span.getStartTimestampS(), - span.getProjectedStopTimestampS(), + span.getStartTimestampSecs(), + span.getProjectedStopTimestampSecs(), traceId, new SpanId(), parentSpanId, diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 41a44386f7..53b7288bb2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -1,5 +1,6 @@ package io.sentry.android.core; +import android.annotation.SuppressLint; import android.content.Context; import android.os.Process; import android.os.SystemClock; @@ -76,6 +77,7 @@ public static void init( * @param logger your custom logger that implements ILogger * @param configuration Sentry.OptionsConfiguration configuration handler */ + @SuppressLint("NewApi") public static synchronized void init( @NotNull final Context context, @NotNull ILogger logger, @@ -127,15 +129,14 @@ public static synchronized void init( final @NotNull TimeSpan appStartTimeSpan = AppStartMetrics.getInstance().getAppStartTimeSpan(); if (appStartTimeSpan.hasNotStarted() - && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + && buildInfoProvider.getSdkInfoVersion() >= android.os.Build.VERSION_CODES.N) { appStartTimeSpan.setStartedAt(Process.getStartUptimeMillis()); } - } else { - final @NotNull TimeSpan appStartTime = - AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); - if (appStartTime.hasNotStarted()) { - appStartTime.setStartedAt(appStart); - } + } + final @NotNull TimeSpan sdkAppStartTime = + AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); + if (sdkAppStartTime.hasNotStarted()) { + sdkAppStartTime.setStartedAt(appStart); } AndroidOptionsInitializer.initializeIntegrationsAndProcessors( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index f2fc4c4ae3..d8611925f8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -592,12 +592,12 @@ public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) { this.attachAnrThreadDump = attachAnrThreadDump; } - @ApiStatus.Internal + @ApiStatus.Experimental public boolean isEnableStarfish() { return enableStarfish; } - @ApiStatus.Internal + @ApiStatus.Experimental public void setEnableStarfish(final boolean enableStarfish) { this.enableStarfish = enableStarfish; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 77859067c6..1518218f4b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -11,10 +11,11 @@ import android.os.Process; import android.os.SystemClock; import androidx.annotation.NonNull; +import io.sentry.NoOpLogger; +import io.sentry.android.core.internal.util.FirstDrawDoneListener; import io.sentry.android.core.performance.ActivityLifecycleCallbacksAdapter; import io.sentry.android.core.performance.ActivityLifecycleTimeSpan; import io.sentry.android.core.performance.AppStartMetrics; -import io.sentry.android.core.performance.NextDrawListener; import io.sentry.android.core.performance.TimeSpan; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicBoolean; @@ -28,7 +29,7 @@ public final class SentryPerformanceProvider extends EmptySecureContentProvider // static to rely on Class load // SystemClock.uptimeMillis() isn't affected by phone provider or clock changes. - private static final long legacyAppStartMillis = SystemClock.uptimeMillis(); + private static final long sdkAppStartMillis = SystemClock.uptimeMillis(); private @Nullable Application app; private @Nullable Application.ActivityLifecycleCallbacks activityCallback; @@ -59,8 +60,8 @@ public String getType(@NotNull Uri uri) { public void onAppLaunched() { // pre-starfish: use static field init as app start time final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); - final @NotNull TimeSpan legacyAppStartSpan = appStartMetrics.getLegacyAppStartTimeSpan(); - legacyAppStartSpan.setStartedAt(legacyAppStartMillis); + final @NotNull TimeSpan sdkAppStartTimeSpan = appStartMetrics.getSdkAppStartTimeSpan(); + sdkAppStartTimeSpan.setStartedAt(sdkAppStartMillis); // starfish: Use Process.getStartUptimeMillis() // Process.getStartUptimeMillis() requires API level 24+ @@ -99,7 +100,7 @@ public void onActivityPreCreated( } final ActivityLifecycleTimeSpan timeSpan = new ActivityLifecycleTimeSpan(); - timeSpan.onCreate.setStartedAt(now); + timeSpan.getOnCreate().setStartedAt(now); activityLifecycleMap.put(activity, timeSpan); } @@ -123,8 +124,8 @@ public void onActivityPostCreated( final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity); if (timeSpan != null) { - timeSpan.onCreate.stop(); - timeSpan.onCreate.setDescription(activity.getClass().getName() + ".onCreate"); + timeSpan.getOnCreate().stop(); + timeSpan.getOnCreate().setDescription(activity.getClass().getName() + ".onCreate"); } } @@ -136,7 +137,7 @@ public void onActivityPreStarted(@NonNull Activity activity) { } final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity); if (timeSpan != null) { - timeSpan.onStart.setStartedAt(now); + timeSpan.getOnStart().setStartedAt(now); } } @@ -145,16 +146,17 @@ public void onActivityStarted(@NonNull Activity activity) { if (firstDrawDone.get()) { return; } - NextDrawListener.forActivity( + FirstDrawDoneListener.registerForNextDraw( activity, - () -> { - handler.postAtFrontOfQueue( - () -> { - if (firstDrawDone.compareAndSet(false, true)) { - onAppStartDone(); - } - }); - }); + () -> + handler.postAtFrontOfQueue( + () -> { + if (firstDrawDone.compareAndSet(false, true)) { + onAppStartDone(); + } + }), + // as the SDK isn't initialized yet, we don't have access to SentryOptions + new BuildInfoProvider(NoOpLogger.getInstance())); } @Override @@ -165,8 +167,8 @@ public void onActivityPostStarted(@NonNull Activity activity) { return; } if (timeSpan != null) { - timeSpan.onStart.stop(); - timeSpan.onStart.setDescription(activity.getClass().getName() + ".onStart"); + timeSpan.getOnStart().stop(); + timeSpan.getOnStart().setDescription(activity.getClass().getName() + ".onStart"); appStartMetrics.addActivityLifecycleTimeSpans(timeSpan); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java index bf83bbb116..02d1eb21bf 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java @@ -56,7 +56,7 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) { final TimeSpan appStartTimeSpan = options.isEnableStarfish() ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getLegacyAppStartTimeSpan(); + : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); if (HintUtils.hasType(hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class) && appStartTimeSpan.hasStarted()) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java index 280ef3b63e..f2612b4aa8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java @@ -18,12 +18,17 @@ package io.sentry.android.core.internal.util; +import android.app.Activity; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.View; import android.view.ViewTreeObserver; +import android.view.Window; +import androidx.annotation.Nullable; import io.sentry.android.core.BuildInfoProvider; +import io.sentry.android.core.internal.gestures.NoOpWindowCallback; +import io.sentry.android.core.performance.WindowContentChangedCallback; import java.util.concurrent.atomic.AtomicReference; import org.jetbrains.annotations.NotNull; @@ -36,6 +41,33 @@ public class FirstDrawDoneListener implements ViewTreeObserver.OnDrawListener { private final @NotNull AtomicReference viewReference; private final @NotNull Runnable callback; + public static void registerForNextDraw( + final @NotNull Activity activity, + final @NotNull Runnable drawDoneCallback, + final @NotNull BuildInfoProvider buildInfoProvider) { + + @Nullable Window window = activity.getWindow(); + if (window != null) { + @Nullable View decorView = window.peekDecorView(); + if (decorView != null) { + registerForNextDraw(decorView, drawDoneCallback, buildInfoProvider); + } else { + final @Nullable Window.Callback oldCallback = window.getCallback(); + window.setCallback( + new WindowContentChangedCallback( + oldCallback != null ? oldCallback : new NoOpWindowCallback(), + () -> { + @Nullable View newDecorView = window.peekDecorView(); + if (newDecorView != null) { + // let's set the old callback again, so we don't intercept anymore + window.setCallback(oldCallback); + registerForNextDraw(newDecorView, drawDoneCallback, buildInfoProvider); + } + })); + } + } + } + /** Registers a post-draw callback for the next draw of a view. */ public static void registerForNextDraw( final @NotNull View view, diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpan.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpan.java index 1d7f4eb2aa..52688a2856 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpan.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpan.java @@ -5,11 +5,25 @@ @ApiStatus.Internal public class ActivityLifecycleTimeSpan implements Comparable { - public final @NotNull TimeSpan onCreate = new TimeSpan(); - public final @NotNull TimeSpan onStart = new TimeSpan(); + private final @NotNull TimeSpan onCreate = new TimeSpan(); + private final @NotNull TimeSpan onStart = new TimeSpan(); + + public final @NotNull TimeSpan getOnCreate() { + return onCreate; + } + + public final @NotNull TimeSpan getOnStart() { + return onStart; + } @Override public int compareTo(ActivityLifecycleTimeSpan o) { - return Long.compare(onCreate.getStartUptimeMs(), o.onCreate.getStartUptimeMs()); + final int onCreateDiff = + Long.compare(onCreate.getStartUptimeMs(), o.onCreate.getStartUptimeMs()); + if (onCreateDiff == 0) { + return Long.compare(onStart.getStartUptimeMs(), o.onStart.getStartUptimeMs()); + } else { + return onCreateDiff; + } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index f5c183acc3..6cebbeacda 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** @@ -17,6 +18,7 @@ * that early, we can't use transactions or spans directly. Thus simple TimeSpans are used and later * transformed into SDK specific txn/span data structures. */ +@ApiStatus.Internal public class AppStartMetrics { public enum AppStartType { @@ -31,7 +33,7 @@ public enum AppStartType { private boolean appLaunchedInForeground = false; private final @NotNull TimeSpan appStartSpan; - private final @NotNull TimeSpan legacyAppStartSpan; + private final @NotNull TimeSpan sdkAppStartSpan; private final @NotNull TimeSpan applicationOnCreate; private final @NotNull Map contentProviderOnCreates; private final @NotNull List activityLifecycles; @@ -51,7 +53,7 @@ public enum AppStartType { public AppStartMetrics() { appStartSpan = new TimeSpan(); - legacyAppStartSpan = new TimeSpan(); + sdkAppStartSpan = new TimeSpan(); applicationOnCreate = new TimeSpan(); contentProviderOnCreates = new HashMap<>(); activityLifecycles = new ArrayList<>(); @@ -69,8 +71,8 @@ public AppStartMetrics() { * @return the app start time span, as measured pre-starfish Uses ContentProvider/Sdk init time as * start timestamp */ - public @NotNull TimeSpan getLegacyAppStartTimeSpan() { - return legacyAppStartSpan; + public @NotNull TimeSpan getSdkAppStartTimeSpan() { + return sdkAppStartSpan; } public @NotNull TimeSpan getApplicationOnCreateTimeSpan() { @@ -113,9 +115,10 @@ public void addActivityLifecycleTimeSpans(final @NotNull ActivityLifecycleTimeSp public void clear() { appStartType = AppStartType.UNKNOWN; appStartSpan.reset(); - legacyAppStartSpan.reset(); + sdkAppStartSpan.reset(); applicationOnCreate.reset(); contentProviderOnCreates.clear(); + activityLifecycles.clear(); } /** diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/NextDrawListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/NextDrawListener.java deleted file mode 100644 index 48b669f1c7..0000000000 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/NextDrawListener.java +++ /dev/null @@ -1,138 +0,0 @@ -package io.sentry.android.core.performance; - -import android.app.Activity; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.view.View; -import android.view.ViewTreeObserver; -import android.view.Window; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.view.ViewCompat; -import io.sentry.android.core.internal.gestures.NoOpWindowCallback; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -/** - * Inspired by https://blog.p-y.wtf/tracking-android-app-launch-in-production Adapted from: - * https://github.com/square/papa/blob/31eebb3d70908bcb1209d82f066ec4d4377183ee/papa/src/main/java/papa/internal/ViewTreeObservers.kt - * - *

Copyright 2021 Square Inc. - * - *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - *

http://www.apache.org/licenses/LICENSE-2.0 - * - *

Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ -@ApiStatus.Internal -public class NextDrawListener - implements ViewTreeObserver.OnDrawListener, View.OnAttachStateChangeListener { - - private @NotNull final Runnable onDrawCallback; - private @NotNull final Handler mainHandler; - private boolean invoked; - - private @Nullable View view; - - protected NextDrawListener( - final @NotNull Handler handler, final @NotNull Runnable onDrawCallback) { - this.mainHandler = handler; - this.onDrawCallback = onDrawCallback; - } - - public static NextDrawListener forActivity( - final @NotNull Activity activity, final @NotNull Runnable onDrawCallback) { - final NextDrawListener listener = - new NextDrawListener(new Handler(Looper.getMainLooper()), onDrawCallback); - - @Nullable Window window = activity.getWindow(); - if (window != null) { - @Nullable View decorView = window.peekDecorView(); - if (decorView != null) { - listener.safelyRegisterForNextDraw(decorView); - } else { - @Nullable Window.Callback oldCallback = window.getCallback(); - if (oldCallback == null) { - oldCallback = new NoOpWindowCallback(); - } - window.setCallback( - new WindowContentChangedCallback( - oldCallback, - () -> { - @Nullable View newDecorView = window.peekDecorView(); - if (newDecorView != null) { - listener.safelyRegisterForNextDraw(newDecorView); - } - })); - } - } - return listener; - } - - @Override - public void onDraw() { - if (invoked) { - return; - } - invoked = true; - // ViewTreeObserver.removeOnDrawListener() throws if called from the onDraw() callback - mainHandler.post( - () -> { - final ViewTreeObserver observer = view.getViewTreeObserver(); - if (observer != null && observer.isAlive()) { - observer.removeOnDrawListener(NextDrawListener.this); - } - }); - onDrawCallback.run(); - } - - private void safelyRegisterForNextDraw(final @NotNull View view) { - this.view = view; - // Prior to API 26, OnDrawListener wasn't merged back from the floating ViewTreeObserver into - // the real ViewTreeObserver. - // https://android.googlesource.com/platform/frameworks/base/+/9f8ec54244a5e0343b9748db3329733f259604f3 - final @Nullable ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); - if (Build.VERSION.SDK_INT >= 26 - && viewTreeObserver != null - && (viewTreeObserver.isAlive() && ViewCompat.isAttachedToWindow(view))) { - viewTreeObserver.addOnDrawListener(this); - } else { - view.addOnAttachStateChangeListener(this); - } - } - - @Override - public void onViewAttachedToWindow(@NonNull View v) { - if (view != null) { - // Backed by CopyOnWriteArrayList, ok to self remove from onViewDetachedFromWindow() - view.removeOnAttachStateChangeListener(this); - - final @Nullable ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); - if (viewTreeObserver != null && viewTreeObserver.isAlive()) { - viewTreeObserver.addOnDrawListener(this); - } - } - } - - @Override - public void onViewDetachedFromWindow(@NonNull View v) { - unregister(); - } - - public void unregister() { - if (view != null) { - view.removeOnAttachStateChangeListener(this); - - final @Nullable ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); - if (viewTreeObserver != null && viewTreeObserver.isAlive()) { - viewTreeObserver.removeOnDrawListener(this); - } - } - } -} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java index 946cdc1179..0ea334c291 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java @@ -4,6 +4,7 @@ import io.sentry.DateUtils; import io.sentry.SentryDate; import io.sentry.SentryLongDate; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -15,6 +16,7 @@ * time. The stop unix time is artificial, it gets projected based on the start time + duration of * the time span. */ +@ApiStatus.Internal public class TimeSpan implements Comparable { private @Nullable String description; @@ -94,9 +96,9 @@ public long getStartTimestampMs() { } /** - * @return the start timestamp of this measurement, unix time, in ms + * @return the start timestamp of this measurement, unix time, in seconds */ - public double getStartTimestampS() { + public double getStartTimestampSecs() { return (double) startUnixTimeMs / 1000.0d; } @@ -112,12 +114,17 @@ public long getProjectedStopTimestampMs() { return 0; } - public double getProjectedStopTimestampS() { + /** + * @return the projected stop timestamp + * @see #getProjectedStopTimestampMs() + */ + public double getProjectedStopTimestampSecs() { return (double) getProjectedStopTimestampMs() / 1000.0d; } /** - * @return the start timestamp of this measurement, unix time + * @return the projected stop timestamp + * @see #getProjectedStopTimestampMs() */ public @Nullable SentryDate getProjectedStopTimestamp() { if (hasStopped()) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 4895ffa24b..eb054a2117 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -857,7 +857,7 @@ class ActivityLifecycleIntegrationTest { // usually set by SentryPerformanceProvider val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) - AppStartMetrics.getInstance().legacyAppStartTimeSpan.setStoppedAt(2) + AppStartMetrics.getInstance().sdkAppStartTimeSpan.setStoppedAt(2) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -877,9 +877,9 @@ class ActivityLifecycleIntegrationTest { setAppStartTime(startDate) val appStartMetrics = AppStartMetrics.getInstance() appStartMetrics.appStartType = AppStartType.WARM - appStartMetrics.legacyAppStartTimeSpan.setStoppedAt(2) + appStartMetrics.sdkAppStartTimeSpan.setStoppedAt(2) - val endDate = appStartMetrics.legacyAppStartTimeSpan.projectedStopTimestamp + val endDate = appStartMetrics.sdkAppStartTimeSpan.projectedStopTimestamp val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -910,14 +910,14 @@ class ActivityLifecycleIntegrationTest { whenever(activity.findViewById(any())).thenReturn(view) sut.onActivityCreated(activity, fixture.bundle) // then app-start end time should still be null - assertTrue(AppStartMetrics.getInstance().legacyAppStartTimeSpan.hasNotStopped()) + assertTrue(AppStartMetrics.getInstance().sdkAppStartTimeSpan.hasNotStopped()) // when activity is resumed sut.onActivityResumed(activity) Thread.sleep(1) runFirstDraw(view) // end-time should be set - assertTrue(AppStartMetrics.getInstance().legacyAppStartTimeSpan.hasStopped()) + assertTrue(AppStartMetrics.getInstance().sdkAppStartTimeSpan.hasStopped()) } @Test @@ -930,7 +930,7 @@ class ActivityLifecycleIntegrationTest { val startDate = SentryNanotimeDate(Date(1), 0) setAppStartTime(startDate) AppStartMetrics.getInstance().appStartType = AppStartType.WARM - AppStartMetrics.getInstance().legacyAppStartTimeSpan.setStoppedAt(1234) + AppStartMetrics.getInstance().sdkAppStartTimeSpan.setStoppedAt(1234) // when activity is created and resumed val activity = mock() @@ -940,7 +940,7 @@ class ActivityLifecycleIntegrationTest { // then the end time should not be overwritten assertEquals( DateUtils.millisToNanos(1234), - AppStartMetrics.getInstance().legacyAppStartTimeSpan.projectedStopTimestamp!!.nanoTimestamp() + AppStartMetrics.getInstance().sdkAppStartTimeSpan.projectedStopTimestamp!!.nanoTimestamp() ) } @@ -965,7 +965,7 @@ class ActivityLifecycleIntegrationTest { Thread.sleep(1) runFirstDraw(view) - val firstAppStartEndTime = AppStartMetrics.getInstance().legacyAppStartTimeSpan.projectedStopTimestamp + val firstAppStartEndTime = AppStartMetrics.getInstance().sdkAppStartTimeSpan.projectedStopTimestamp Thread.sleep(1) sut.onActivityPaused(activity) @@ -978,7 +978,7 @@ class ActivityLifecycleIntegrationTest { // then the end time should not be overwritten assertEquals( firstAppStartEndTime!!.nanoTimestamp(), - AppStartMetrics.getInstance().legacyAppStartTimeSpan.projectedStopTimestamp!!.nanoTimestamp() + AppStartMetrics.getInstance().sdkAppStartTimeSpan.projectedStopTimestamp!!.nanoTimestamp() ) } @@ -1496,7 +1496,7 @@ class ActivityLifecycleIntegrationTest { private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(1), 0)) { // set by SentryPerformanceProvider so forcing it here - val appStartTimeSpan = AppStartMetrics.getInstance().legacyAppStartTimeSpan + val appStartTimeSpan = AppStartMetrics.getInstance().sdkAppStartTimeSpan val millis = DateUtils.nanosToMillis(date.nanoTimestamp().toDouble()).toLong() appStartTimeSpan.setStartedAt(millis) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 0c76fa59f9..143847ebd7 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -261,7 +261,7 @@ class PerformanceAndroidEventProcessorTest { false -> AppStartType.WARM } val timeSpan = - if (options.isEnableStarfish) appStartTimeSpan else legacyAppStartTimeSpan + if (options.isEnableStarfish) appStartTimeSpan else sdkAppStartTimeSpan timeSpan.apply { setStartedAt(1) setStoppedAt(2) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index cea2ae7f76..95dad15fe7 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -4,6 +4,7 @@ import android.app.ActivityManager import android.app.Application import android.app.ApplicationExitInfo import android.content.Context +import android.os.Build import android.os.Bundle import android.os.SystemClock import androidx.test.core.app.ApplicationProvider @@ -68,6 +69,10 @@ import kotlin.test.assertNull import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) +@Config( + sdk = [Build.VERSION_CODES.N], + shadows = [SentryShadowProcess::class] +) class SentryAndroidTest { @get:Rule @@ -428,6 +433,19 @@ class SentryAndroidTest { assertEquals(0, optionsRef.integrations.size) } + @Test + fun `init sets app start times span if starfish is enabled`() { + AppStartMetrics.getInstance().clear() + SentryShadowProcess.setStartUptimeMillis(42) + + fixture.initSut(context = mock()) { options -> + options.dsn = "https://key@sentry.io/123" + options.isEnableStarfish = true + } + assertEquals(42, AppStartMetrics.getInstance().appStartTimeSpan.startUptimeMs) + assertTrue(AppStartMetrics.getInstance().sdkAppStartTimeSpan.hasStarted()) + } + private fun prefillScopeCache(cacheDir: String) { val scopeDir = File(cacheDir, SCOPE_CACHE).also { it.mkdirs() } File(scopeDir, BREADCRUMBS_FILENAME).writeText( diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index 81195370a8..08ecf1670b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -14,33 +14,11 @@ import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.robolectric.Shadows import org.robolectric.annotation.Config -import org.robolectric.annotation.Implementation -import org.robolectric.annotation.Implements import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -@Implements(android.os.Process::class) -class SentryShadowProcess { - - companion object { - - private var startupTimeMillis: Long = 0 - - fun setStartUptimeMillis(value: Long) { - startupTimeMillis = value - } - - @Suppress("unused") - @Implementation - @JvmStatic - fun getStartUptimeMillis(): Long { - return startupTimeMillis - } - } -} - @RunWith(AndroidJUnit4::class) @Config( sdk = [Build.VERSION_CODES.N], diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryShadowProcess.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryShadowProcess.kt new file mode 100644 index 0000000000..2f3958817b --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryShadowProcess.kt @@ -0,0 +1,24 @@ +package io.sentry.android.core + +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements + +@Implements(android.os.Process::class) +class SentryShadowProcess { + + companion object { + + private var startupTimeMillis: Long = 0 + + fun setStartUptimeMillis(value: Long) { + startupTimeMillis = value + } + + @Suppress("unused") + @Implementation + @JvmStatic + fun getStartUptimeMillis(): Long { + return startupTimeMillis + } + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index 7448c215c7..50ae8f3970 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -53,8 +53,8 @@ class AndroidEnvelopeCacheTest { appStartTimeSpan.setStartedAt(appStartMillis) appStartTimeSpan.setStartUnixTimeMs(appStartMillis) } else { - legacyAppStartTimeSpan.setStartedAt(appStartMillis) - legacyAppStartTimeSpan.setStartUnixTimeMs(appStartMillis) + sdkAppStartTimeSpan.setStartedAt(appStartMillis) + sdkAppStartTimeSpan.setStartUnixTimeMs(appStartMillis) } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt index a7b4cc3f8c..c26c09d191 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt @@ -1,16 +1,19 @@ package io.sentry.android.core.internal.util +import android.app.Activity import android.content.Context import android.os.Build import android.os.Handler import android.os.Looper import android.view.View import android.view.ViewTreeObserver +import android.view.Window import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.android.core.BuildInfoProvider import io.sentry.test.getProperty import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -22,6 +25,7 @@ import java.util.concurrent.CopyOnWriteArrayList import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertIs +import kotlin.test.assertNull import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) @@ -136,4 +140,50 @@ class FirstDrawDoneListenerTest { Shadows.shadowOf(Looper.getMainLooper()).idle() verify(r).run() } + + @Test + fun `registerForNextDraw uses the activity decor view`() { + val view = fixture.getSut() + + val activity = mock() + val window = mock() + whenever(activity.window).thenReturn(window) + whenever(window.peekDecorView()).thenReturn(view) + + val r: Runnable = mock() + FirstDrawDoneListener.registerForNextDraw(activity, r, fixture.buildInfo) + + assertFalse(fixture.onDrawListeners.isEmpty()) + } + + @Test + fun `registerForNextDraw uses the activity decor view once it's available`() { + val view = fixture.getSut() + + val activity = mock() + val window = mock() + whenever(activity.window).thenReturn(window) + whenever(window.peekDecorView()).thenReturn(null) + val callbackCapture = argumentCaptor() + + // when registerForNextDraw is called, but the activity has no window yet + val r: Runnable = mock() + FirstDrawDoneListener.registerForNextDraw(activity, r, fixture.buildInfo) + + // then a window callback is installed + verify(window).callback = callbackCapture.capture() + + // once the window is available + whenever(window.peekDecorView()).thenReturn(view) + callbackCapture.firstValue.onContentChanged() + + // then a window callback should be set back to the original one + verify(window, times(2)).callback = callbackCapture.capture() + assertNull(callbackCapture.lastValue) + + // and the onDrawListener should be registered + assertFalse(fixture.onDrawListeners.isEmpty()) + + listOf(1, 2).isNotEmpty() + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpanTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpanTest.kt index beeed5e9f1..d22b376cdf 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpanTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleTimeSpanTest.kt @@ -34,4 +34,21 @@ class ActivityLifecycleTimeSpanTest { assertEquals(spanB, spans[1]) assertEquals(spanC, spans[2]) } + + @Test + fun `if two activity spans have same onCreate, they're sorted by onstart`() { + // given span A and B with same onCreate + val spanA = ActivityLifecycleTimeSpan() + spanA.onCreate.setStartedAt(1) + val spanB = ActivityLifecycleTimeSpan() + spanB.onCreate.setStartedAt(1) + + // when span A starts after span B + spanA.onStart.setStartedAt(20) + spanB.onStart.setStartedAt(10) + + // then they still should be properly sorted + val sortedSpans = listOf(spanA, spanB).sorted() + assertEquals(spanB, sortedSpans[0]) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt new file mode 100644 index 0000000000..24ae78bc97 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -0,0 +1,57 @@ +package io.sentry.android.core.performance + +import android.app.Application +import android.content.ContentProvider +import android.os.Build +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.android.core.SentryShadowProcess +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.robolectric.annotation.Config +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +@Config( + sdk = [Build.VERSION_CODES.N], + shadows = [SentryShadowProcess::class] +) +class AppStartMetricsTest { + + @Before + fun setup() { + AppStartMetrics.getInstance().clear() + SentryShadowProcess.setStartUptimeMillis(42) + } + + @Test + fun `getInstance returns a singelton`() { + assertSame(AppStartMetrics.getInstance(), AppStartMetrics.getInstance()) + } + + @Test + fun `metrics are properly cleared`() { + val metrics = AppStartMetrics.getInstance() + metrics.appStartTimeSpan.start() + metrics.sdkAppStartTimeSpan.start() + metrics.appStartType = AppStartMetrics.AppStartType.WARM + metrics.applicationOnCreateTimeSpan.start() + metrics.addActivityLifecycleTimeSpans(ActivityLifecycleTimeSpan()) + AppStartMetrics.onApplicationCreate(mock()) + AppStartMetrics.onContentProviderCreate(mock()) + + metrics.clear() + + assertTrue(metrics.appStartTimeSpan.hasNotStarted()) + assertTrue(metrics.sdkAppStartTimeSpan.hasNotStarted()) + assertTrue(metrics.applicationOnCreateTimeSpan.hasNotStarted()) + assertEquals(AppStartMetrics.AppStartType.UNKNOWN, metrics.appStartType) + assertTrue(metrics.applicationOnCreateTimeSpan.hasNotStarted()) + + assertTrue(metrics.activityLifecycleTimeSpans.isEmpty()) + assertTrue(metrics.contentProviderOnCreateTimeSpans.isEmpty()) + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt index adf4b1bd17..b60a619031 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt @@ -93,7 +93,7 @@ class TimeSpanTest { fun `span stop time is 0 if not started`() { val span = TimeSpan() assertEquals(0, span.projectedStopTimestampMs) - assertEquals(0.0, span.projectedStopTimestampS) + assertEquals(0.0, span.projectedStopTimestampSecs) } @Test @@ -102,8 +102,8 @@ class TimeSpanTest { span.setStartedAt(1234) span.setStoppedAt(1234) - assertEquals(span.startTimestampMs / 1000.0, span.startTimestampS, 0.001) - assertEquals(span.projectedStopTimestampMs / 1000.0, span.projectedStopTimestampS, 0.001) + assertEquals(span.startTimestampMs / 1000.0, span.startTimestampSecs, 0.001) + assertEquals(span.projectedStopTimestampMs / 1000.0, span.projectedStopTimestampSecs, 0.001) } @Test From 1141aed70798814f9410995c338f7b55b7e575cb Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 30 Nov 2023 22:22:23 +0100 Subject: [PATCH 07/15] Update Changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd2bcc1317..e51ce4345d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,12 @@ # Changelog -## 7.0.0 +## Unreleased ### Features - (Internal, Experimental) Attach spans for Application, ContentProvider, and Activities to app-start ([#3057](https://github.com/getsentry/sentry-java/pull/3057)) -## Unreleased +## 7.0.0 Version 7 of the Sentry Android/Java SDK brings a variety of features and fixes. The most notable changes are: - Bumping `minSdk` level to 19 (Android 4.4) From 1a66cce92cde4e2d296345aa40b5fc957f3ab1a1 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 5 Dec 2023 09:02:23 +0100 Subject: [PATCH 08/15] Implement PR feedback --- .../android/core/SentryPerformanceProvider.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 1518218f4b..d750e5d308 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -148,13 +148,11 @@ public void onActivityStarted(@NonNull Activity activity) { } FirstDrawDoneListener.registerForNextDraw( activity, - () -> - handler.postAtFrontOfQueue( - () -> { - if (firstDrawDone.compareAndSet(false, true)) { - onAppStartDone(); - } - }), + () -> { + if (firstDrawDone.compareAndSet(false, true)) { + onAppStartDone(); + } + }, // as the SDK isn't initialized yet, we don't have access to SentryOptions new BuildInfoProvider(NoOpLogger.getInstance())); } From d43311af4de9697012a932ff860c9f2b2ff3d3e0 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 5 Dec 2023 09:02:37 +0100 Subject: [PATCH 09/15] Rename starfish to performance-v2 --- sentry-android-core/api/sentry-android-core.api | 4 ++-- .../android/core/ActivityLifecycleIntegration.java | 6 +++--- .../android/core/DefaultAndroidEventProcessor.java | 2 +- .../io/sentry/android/core/InternalSentrySdk.java | 2 +- .../sentry/android/core/ManifestMetadataReader.java | 6 +++--- .../core/PerformanceAndroidEventProcessor.java | 2 +- .../java/io/sentry/android/core/SentryAndroid.java | 4 ++-- .../io/sentry/android/core/SentryAndroidOptions.java | 10 +++++----- .../android/core/SentryPerformanceProvider.java | 7 ++----- .../android/core/cache/AndroidEnvelopeCache.java | 2 +- .../android/core/performance/AppStartMetrics.java | 4 ++-- .../android/core/ManifestMetadataReaderTest.kt | 10 +++++----- .../core/PerformanceAndroidEventProcessorTest.kt | 12 ++++++------ .../sentry/android/core/SentryAndroidOptionsTest.kt | 10 +++++----- .../java/io/sentry/android/core/SentryAndroidTest.kt | 4 ++-- .../android/core/cache/AndroidEnvelopeCacheTest.kt | 2 +- .../src/main/AndroidManifest.xml | 2 +- 17 files changed, 43 insertions(+), 46 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index bfa3a0bbe4..b520b3a7f7 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -264,9 +264,9 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableFramesTracking ()Z public fun isEnableNdk ()Z public fun isEnableNetworkEventBreadcrumbs ()Z + public fun isEnablePerformanceV2 ()Z public fun isEnableRootCheck ()Z public fun isEnableScopeSync ()Z - public fun isEnableStarfish ()Z public fun isEnableSystemEventBreadcrumbs ()Z public fun isReportHistoricalAnrs ()Z public fun setAnrEnabled (Z)V @@ -287,9 +287,9 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableFramesTracking (Z)V public fun setEnableNdk (Z)V public fun setEnableNetworkEventBreadcrumbs (Z)V + public fun setEnablePerformanceV2 (Z)V public fun setEnableRootCheck (Z)V public fun setEnableScopeSync (Z)V - public fun setEnableStarfish (Z)V public fun setEnableSystemEventBreadcrumbs (Z)V public fun setNativeSdkName (Ljava/lang/String;)V public fun setProfilingTracesHz (I)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index e40b6a5a60..3f5bd39c13 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -637,8 +637,8 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) { // if Activity has savedInstanceState then its a warm start // https://developer.android.com/topic/performance/vitals/launch-time#warm // SentryPerformanceProvider sets this already - // pre-starfish: back-fill with best guess - if (options != null && !options.isEnableStarfish()) { + // pre-performance-v2: back-fill with best guess + if (options != null && !options.isEnablePerformanceV2()) { AppStartMetrics.getInstance() .setAppStartType( savedInstanceState == null @@ -689,7 +689,7 @@ private void finishAppStartSpan() { private @NotNull TimeSpan getAppStartTimeSpan() { final @NotNull TimeSpan appStartTimeSpan = - options.isEnableStarfish() + options.isEnablePerformanceV2() ? AppStartMetrics.getInstance().getAppStartTimeSpan() : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); return appStartTimeSpan; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 9f392853de..82aee30edf 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -202,7 +202,7 @@ private void setDist(final @NotNull SentryBaseEvent event, final @NotNull String private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); final @NotNull TimeSpan appStartTimeSpan = - options.isEnableStarfish() + options.isEnablePerformanceV2() ? AppStartMetrics.getInstance().getAppStartTimeSpan() : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); if (appStartTimeSpan.hasStarted()) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index 7b9c33753c..89d6e59277 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -103,7 +103,7 @@ public static Map serializeScope( app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); final @NotNull TimeSpan appStartTimeSpan = - options.isEnableStarfish() + options.isEnablePerformanceV2() ? AppStartMetrics.getInstance().getAppStartTimeSpan() : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); if (appStartTimeSpan.hasStarted()) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 533757a29c..5551a54d3c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -96,7 +96,7 @@ final class ManifestMetadataReader { static final String SEND_MODULES = "io.sentry.send-modules"; - static final String ENABLE_STARFISH = "io.sentry.starfish.enable"; + static final String ENABLE_PERFORMANCE_V2 = "io.sentry.performance-v2.enable"; /** ManifestMetadataReader ctor */ private ManifestMetadataReader() {} @@ -363,8 +363,8 @@ static void applyMetadata( options.setSendModules(readBool(metadata, logger, SEND_MODULES, options.isSendModules())); - options.setEnableStarfish( - readBool(metadata, logger, ENABLE_STARFISH, options.isEnableStarfish())); + options.setEnablePerformanceV2( + readBool(metadata, logger, ENABLE_PERFORMANCE_V2, options.isEnablePerformanceV2())); } options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 3ed7968e3a..89103b645c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -71,7 +71,7 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { // the app.start span, which is automatically created by the SDK. if (!sentStartMeasurement && hasAppStartSpan(transaction)) { final @NotNull TimeSpan appStartTimeSpan = - options.isEnableStarfish() + options.isEnablePerformanceV2() ? AppStartMetrics.getInstance().getAppStartTimeSpan() : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); final long appStartUpInterval = appStartTimeSpan.getDurationMs(); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 53b7288bb2..6f4b356b66 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -124,8 +124,8 @@ public static synchronized void init( // if SentryPerformanceProvider was disabled or removed, we set the App Start when // the SDK is called. - // pre-starfish: fill-back the app start time to the SDK init time - if (options.isEnableStarfish()) { + // pre-performance-v2: fill-back the app start time to the SDK init time + if (options.isEnablePerformanceV2()) { final @NotNull TimeSpan appStartTimeSpan = AppStartMetrics.getInstance().getAppStartTimeSpan(); if (appStartTimeSpan.hasNotStarted() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index d8611925f8..b8cae90d4c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -212,7 +212,7 @@ public interface BeforeCaptureCallback { */ private boolean attachAnrThreadDump = false; - private boolean enableStarfish; + private boolean enablePerformanceV2 = false; public SentryAndroidOptions() { setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME); @@ -593,12 +593,12 @@ public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) { } @ApiStatus.Experimental - public boolean isEnableStarfish() { - return enableStarfish; + public boolean isEnablePerformanceV2() { + return enablePerformanceV2; } @ApiStatus.Experimental - public void setEnableStarfish(final boolean enableStarfish) { - this.enableStarfish = enableStarfish; + public void setEnablePerformanceV2(final boolean enablePerformanceV2) { + this.enablePerformanceV2 = enablePerformanceV2; } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index d750e5d308..f46c315219 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -6,8 +6,6 @@ import android.content.pm.ProviderInfo; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.os.Process; import android.os.SystemClock; import androidx.annotation.NonNull; @@ -58,12 +56,12 @@ public String getType(@NotNull Uri uri) { @ApiStatus.Internal public void onAppLaunched() { - // pre-starfish: use static field init as app start time + // pre-performance-v2: use static field init as app start time final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); final @NotNull TimeSpan sdkAppStartTimeSpan = appStartMetrics.getSdkAppStartTimeSpan(); sdkAppStartTimeSpan.setStartedAt(sdkAppStartMillis); - // starfish: Use Process.getStartUptimeMillis() + // performance v2: Use Process.getStartUptimeMillis() // Process.getStartUptimeMillis() requires API level 24+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N) { return; @@ -84,7 +82,6 @@ public void onAppLaunched() { appStartTimespan.setStartedAt(Process.getStartUptimeMillis()); final AtomicBoolean firstDrawDone = new AtomicBoolean(false); - final Handler handler = new Handler(Looper.getMainLooper()); activityCallback = new ActivityLifecycleCallbacksAdapter() { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java index 02d1eb21bf..9c2f994a58 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java @@ -54,7 +54,7 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) { final SentryAndroidOptions options = (SentryAndroidOptions) this.options; final TimeSpan appStartTimeSpan = - options.isEnableStarfish() + options.isEnablePerformanceV2() ? AppStartMetrics.getInstance().getAppStartTimeSpan() : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 6cebbeacda..a749c3ee60 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -68,8 +68,8 @@ public AppStartMetrics() { } /** - * @return the app start time span, as measured pre-starfish Uses ContentProvider/Sdk init time as - * start timestamp + * @return the app start time span, as measured pre-performance-v2 Uses ContentProvider/Sdk init + * time as start timestamp */ public @NotNull TimeSpan getSdkAppStartTimeSpan() { return sdkAppStartSpan; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index f979a825b5..acc62be070 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -1320,20 +1320,20 @@ class ManifestMetadataReaderTest { } @Test - fun `applyMetadata reads starfish flag to options`() { + fun `applyMetadata reads performance-v2 flag to options`() { // Arrange - val bundle = bundleOf(ManifestMetadataReader.ENABLE_STARFISH to true) + val bundle = bundleOf(ManifestMetadataReader.ENABLE_PERFORMANCE_V2 to true) val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertTrue(fixture.options.isEnableStarfish) + assertTrue(fixture.options.isEnablePerformanceV2) } @Test - fun `applyMetadata reads starfish flag to options and keeps default if not found`() { + fun `applyMetadata reads performance-v2 flag to options and keeps default if not found`() { // Arrange val context = fixture.getContext() @@ -1341,6 +1341,6 @@ class ManifestMetadataReaderTest { ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertFalse(fixture.options.isEnableStarfish) + assertFalse(fixture.options.isEnablePerformanceV2) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 143847ebd7..936f8b6661 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -37,10 +37,10 @@ class PerformanceAndroidEventProcessorTest { fun getSut( tracesSampleRate: Double? = 1.0, - enableStarfish: Boolean = false + enablePerformanceV2: Boolean = false ): PerformanceAndroidEventProcessor { options.tracesSampleRate = tracesSampleRate - options.isEnableStarfish = enableStarfish + options.isEnablePerformanceV2 = enablePerformanceV2 whenever(hub.options).thenReturn(options) tracer = SentryTracer(context, hub) return PerformanceAndroidEventProcessor(options, activityFramesTracker) @@ -67,9 +67,9 @@ class PerformanceAndroidEventProcessorTest { } @Test - fun `add cold start measurement for starfish`() { + fun `add cold start measurement for performance-v2`() { val sut = fixture.getSut() - fixture.options.isEnableStarfish = true + fixture.options.isEnablePerformanceV2 = true var tr = getTransaction(AppStartType.COLD) setAppStart(fixture.options) @@ -212,7 +212,7 @@ class PerformanceAndroidEventProcessorTest { appStartMetrics.addActivityLifecycleTimeSpans(activityTimeSpan) // when an activity transaction is created - val sut = fixture.getSut(enableStarfish = true) + val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) val tracer = SentryTracer(context, fixture.hub) var tr = SentryTransaction(tracer) @@ -261,7 +261,7 @@ class PerformanceAndroidEventProcessorTest { false -> AppStartType.WARM } val timeSpan = - if (options.isEnableStarfish) appStartTimeSpan else sdkAppStartTimeSpan + if (options.isEnablePerformanceV2) appStartTimeSpan else sdkAppStartTimeSpan timeSpan.apply { setStartedAt(1) setStoppedAt(2) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt index c003e57ca0..3ee8ad3fc5 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt @@ -142,16 +142,16 @@ class SentryAndroidOptionsTest { } @Test - fun `starfish is disabled by default`() { + fun `performance v2 is disabled by default`() { val sentryOptions = SentryAndroidOptions() - assertFalse(sentryOptions.isEnableStarfish) + assertFalse(sentryOptions.isEnablePerformanceV2) } @Test - fun `starfish can be enabled`() { + fun `performance v2 can be enabled`() { val sentryOptions = SentryAndroidOptions() - sentryOptions.isEnableStarfish = true - assertTrue(sentryOptions.isEnableStarfish) + sentryOptions.isEnablePerformanceV2 = true + assertTrue(sentryOptions.isEnablePerformanceV2) } fun `when options is initialized, enableScopeSync is enabled by default`() { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 95dad15fe7..0a823227d7 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -434,13 +434,13 @@ class SentryAndroidTest { } @Test - fun `init sets app start times span if starfish is enabled`() { + fun `init sets app start times span if performance-v2 is enabled`() { AppStartMetrics.getInstance().clear() SentryShadowProcess.setStartUptimeMillis(42) fixture.initSut(context = mock()) { options -> options.dsn = "https://key@sentry.io/123" - options.isEnableStarfish = true + options.isEnablePerformanceV2 = true } assertEquals(42, AppStartMetrics.getInstance().appStartTimeSpan.startUptimeMs) assertTrue(AppStartMetrics.getInstance().sdkAppStartTimeSpan.hasStarted()) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index 50ae8f3970..3637a8a7d5 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -49,7 +49,7 @@ class AndroidEnvelopeCacheTest { if (appStartMillis != null) { AppStartMetrics.getInstance().apply { - if (options.isEnableStarfish) { + if (options.isEnablePerformanceV2) { appStartTimeSpan.setStartedAt(appStartMillis) appStartTimeSpan.setStartUnixTimeMs(appStartMillis) } else { diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index e98065be6d..ad6889e253 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -151,7 +151,7 @@ - + From 3257c660655c7e221bff3622bb7e8a6c4eb7b1b8 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 6 Dec 2023 10:20:07 +0100 Subject: [PATCH 10/15] Address PR feedback --- .../api/sentry-android-core.api | 4 +- .../core/ActivityLifecycleIntegration.java | 17 ++++-- .../core/DefaultAndroidEventProcessor.java | 6 +- .../android/core/InternalSentrySdk.java | 6 +- .../PerformanceAndroidEventProcessor.java | 6 +- .../io/sentry/android/core/SentryAndroid.java | 24 ++++---- .../core/SentryPerformanceProvider.java | 32 +++++----- .../core/cache/AndroidEnvelopeCache.java | 10 +--- .../core/performance/AppStartMetrics.java | 15 +++-- .../android/core/performance/TimeSpan.java | 4 +- .../core/ActivityLifecycleIntegrationTest.kt | 20 +++---- .../PerformanceAndroidEventProcessorTest.kt | 5 +- .../sentry/android/core/SentryAndroidTest.kt | 20 ++++++- .../core/SentryPerformanceProviderTest.kt | 58 ++++++++++--------- .../core/cache/AndroidEnvelopeCacheTest.kt | 4 +- .../core/performance/AppStartMetricsTest.kt | 4 +- .../android/core/performance/TimeSpanTest.kt | 1 + 17 files changed, 131 insertions(+), 105 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index b520b3a7f7..2a01d12c40 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -330,9 +330,7 @@ public final class io/sentry/android/core/SentryLogcatAdapter { public final class io/sentry/android/core/SentryPerformanceProvider { public fun ()V public fun attachInfo (Landroid/content/Context;Landroid/content/pm/ProviderInfo;)V - public fun getActivityCallback ()Landroid/app/Application$ActivityLifecycleCallbacks; public fun getType (Landroid/net/Uri;)Ljava/lang/String; - public fun onAppLaunched ()V public fun onCreate ()Z } @@ -413,7 +411,7 @@ public class io/sentry/android/core/performance/AppStartMetrics { public fun getApplicationOnCreateTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; public fun getContentProviderOnCreateTimeSpans ()Ljava/util/List; public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics; - public fun getSdkAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; + public fun getSdkInitTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; public fun isAppLaunchedInForeground ()Z public static fun onApplicationCreate (Landroid/app/Application;)V public static fun onApplicationPostCreate (Landroid/app/Application;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 3f5bd39c13..4795b4a959 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -546,14 +546,18 @@ private void cancelTtfdAutoClose() { private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable ISpan ttidSpan) { // app start span - final @NotNull TimeSpan appStartTimeSpan = getAppStartTimeSpan(); + final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); + 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 times, - // and we need to set the end time manually here, - // the start time gets set manually in SentryAndroid.init() + // 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 if (appStartTimeSpan.hasStarted() && appStartTimeSpan.hasNotStopped()) { appStartTimeSpan.stop(); } + if (sdkInitTimeSpan.hasStarted() && sdkInitTimeSpan.hasNotStopped()) { + sdkInitTimeSpan.stop(); + } finishAppStartSpan(); if (options != null && ttidSpan != null) { @@ -689,9 +693,10 @@ private void finishAppStartSpan() { private @NotNull TimeSpan getAppStartTimeSpan() { final @NotNull TimeSpan appStartTimeSpan = - options.isEnablePerformanceV2() + (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N + && options.isEnablePerformanceV2()) ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); + : AppStartMetrics.getInstance().getSdkInitTimeSpan(); return appStartTimeSpan; } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 82aee30edf..4b4bba5aca 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -3,6 +3,7 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.os.Build; import io.sentry.DateUtils; import io.sentry.EventProcessor; import io.sentry.Hint; @@ -202,9 +203,10 @@ private void setDist(final @NotNull SentryBaseEvent event, final @NotNull String private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); final @NotNull TimeSpan appStartTimeSpan = - options.isEnablePerformanceV2() + (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N + && options.isEnablePerformanceV2()) ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); + : AppStartMetrics.getInstance().getSdkInitTimeSpan(); if (appStartTimeSpan.hasStarted()) { app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp())); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index 89d6e59277..88df7373e0 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -3,6 +3,7 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.os.Build; import io.sentry.DateUtils; import io.sentry.HubAdapter; import io.sentry.IHub; @@ -103,9 +104,10 @@ public static Map serializeScope( app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); final @NotNull TimeSpan appStartTimeSpan = - options.isEnablePerformanceV2() + (new BuildInfoProvider(options.getLogger()).getSdkInfoVersion() >= Build.VERSION_CODES.N + && options.isEnablePerformanceV2()) ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); + : AppStartMetrics.getInstance().getSdkInitTimeSpan(); if (appStartTimeSpan.hasStarted()) { app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp())); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 89103b645c..2c61777112 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -4,6 +4,7 @@ import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM; import static io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP; +import android.os.Build; import io.sentry.EventProcessor; import io.sentry.Hint; import io.sentry.MeasurementUnit; @@ -71,9 +72,10 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { // the app.start span, which is automatically created by the SDK. if (!sentStartMeasurement && hasAppStartSpan(transaction)) { final @NotNull TimeSpan appStartTimeSpan = - options.isEnablePerformanceV2() + (new BuildInfoProvider(options.getLogger()).getSdkInfoVersion() >= Build.VERSION_CODES.N + && options.isEnablePerformanceV2()) ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); + : AppStartMetrics.getInstance().getSdkInitTimeSpan(); final long appStartUpInterval = appStartTimeSpan.getDurationMs(); // if appStartUpInterval is 0, metrics are not ready to be sent diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 6f4b356b66..af68a026fb 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -25,7 +25,7 @@ public final class SentryAndroid { // SystemClock.uptimeMillis() isn't affected by phone provider or clock changes. - private static final long appStart = SystemClock.uptimeMillis(); + private static final long sdkInitMillis = SystemClock.uptimeMillis(); static final String SENTRY_FRAGMENT_INTEGRATION_CLASS_NAME = "io.sentry.android.fragment.FragmentLifecycleIntegration"; @@ -122,21 +122,19 @@ public static synchronized void init( configuration.configure(options); - // if SentryPerformanceProvider was disabled or removed, we set the App Start when - // the SDK is called. - // pre-performance-v2: fill-back the app start time to the SDK init time - if (options.isEnablePerformanceV2()) { - final @NotNull TimeSpan appStartTimeSpan = - AppStartMetrics.getInstance().getAppStartTimeSpan(); - if (appStartTimeSpan.hasNotStarted() - && buildInfoProvider.getSdkInfoVersion() >= android.os.Build.VERSION_CODES.N) { + // if SentryPerformanceProvider was disabled or removed, + // we set the app start / sdk init time here instead + final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); + if (options.isEnablePerformanceV2() + && buildInfoProvider.getSdkInfoVersion() >= android.os.Build.VERSION_CODES.N) { + final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan(); + if (appStartTimeSpan.hasNotStarted()) { appStartTimeSpan.setStartedAt(Process.getStartUptimeMillis()); } } - final @NotNull TimeSpan sdkAppStartTime = - AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); - if (sdkAppStartTime.hasNotStarted()) { - sdkAppStartTime.setStartedAt(appStart); + final @NotNull TimeSpan sdkInitTimeSpan = appStartMetrics.getSdkInitTimeSpan(); + if (sdkInitTimeSpan.hasNotStarted()) { + sdkInitTimeSpan.setStartedAt(sdkInitMillis); } AndroidOptionsInitializer.initializeIntegrationsAndProcessors( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index f46c315219..0cdc076037 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -27,14 +27,14 @@ public final class SentryPerformanceProvider extends EmptySecureContentProvider // static to rely on Class load // SystemClock.uptimeMillis() isn't affected by phone provider or clock changes. - private static final long sdkAppStartMillis = SystemClock.uptimeMillis(); + private static final long sdkInitMillis = SystemClock.uptimeMillis(); private @Nullable Application app; private @Nullable Application.ActivityLifecycleCallbacks activityCallback; @Override public boolean onCreate() { - onAppLaunched(); + onAppLaunched(getContext()); return true; } @@ -54,23 +54,19 @@ public String getType(@NotNull Uri uri) { return null; } - @ApiStatus.Internal - public void onAppLaunched() { - // pre-performance-v2: use static field init as app start time + private void onAppLaunched(final @Nullable Context context) { final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); - final @NotNull TimeSpan sdkAppStartTimeSpan = appStartMetrics.getSdkAppStartTimeSpan(); - sdkAppStartTimeSpan.setStartedAt(sdkAppStartMillis); - // performance v2: Use Process.getStartUptimeMillis() - // Process.getStartUptimeMillis() requires API level 24+ + // sdk-init uses static field init as start time + final @NotNull TimeSpan sdkInitTimeSpan = appStartMetrics.getSdkInitTimeSpan(); + sdkInitTimeSpan.setStartedAt(sdkInitMillis); + + // performance v2: Uses Process.getStartUptimeMillis() + // requires API level 24+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N) { return; } - @Nullable Context context = getContext(); - if (context != null) { - context = context.getApplicationContext(); - } if (context instanceof Application) { app = (Application) context; } @@ -179,8 +175,11 @@ public void onActivityDestroyed(@NonNull Activity activity) { app.registerActivityLifecycleCallbacks(activityCallback); } - private synchronized void onAppStartDone() { - AppStartMetrics.getInstance().getAppStartTimeSpan().stop(); + @TestOnly + synchronized void onAppStartDone() { + final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); + appStartMetrics.getSdkInitTimeSpan().stop(); + appStartMetrics.getAppStartTimeSpan().stop(); if (app != null) { if (activityCallback != null) { @@ -190,7 +189,8 @@ private synchronized void onAppStartDone() { } @TestOnly - public @Nullable Application.ActivityLifecycleCallbacks getActivityCallback() { + @Nullable + Application.ActivityLifecycleCallbacks getActivityCallback() { return activityCallback; } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java index 9c2f994a58..28090ef6c7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java @@ -52,16 +52,12 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) { super.store(envelope, hint); final SentryAndroidOptions options = (SentryAndroidOptions) this.options; - - final TimeSpan appStartTimeSpan = - options.isEnablePerformanceV2() - ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getSdkAppStartTimeSpan(); + final TimeSpan sdkInitTimeSpan = AppStartMetrics.getInstance().getSdkInitTimeSpan(); if (HintUtils.hasType(hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class) - && appStartTimeSpan.hasStarted()) { + && sdkInitTimeSpan.hasStarted()) { long timeSinceSdkInit = - currentDateProvider.getCurrentTimeMillis() - appStartTimeSpan.getStartTimestampMs(); + currentDateProvider.getCurrentTimeMillis() - sdkInitTimeSpan.getStartTimestampMs(); if (timeSinceSdkInit <= options.getStartupCrashDurationThresholdMillis()) { options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index a749c3ee60..8a161baf5b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -33,7 +33,7 @@ public enum AppStartType { private boolean appLaunchedInForeground = false; private final @NotNull TimeSpan appStartSpan; - private final @NotNull TimeSpan sdkAppStartSpan; + private final @NotNull TimeSpan sdkInitTimeSpan; private final @NotNull TimeSpan applicationOnCreate; private final @NotNull Map contentProviderOnCreates; private final @NotNull List activityLifecycles; @@ -53,7 +53,7 @@ public enum AppStartType { public AppStartMetrics() { appStartSpan = new TimeSpan(); - sdkAppStartSpan = new TimeSpan(); + sdkInitTimeSpan = new TimeSpan(); applicationOnCreate = new TimeSpan(); contentProviderOnCreates = new HashMap<>(); activityLifecycles = new ArrayList<>(); @@ -68,11 +68,14 @@ public AppStartMetrics() { } /** - * @return the app start time span, as measured pre-performance-v2 Uses ContentProvider/Sdk init + * @return the SDK init time span, as measured pre-performance-v2 Uses ContentProvider/Sdk init * time as start timestamp + *

Data is filled by either {@link io.sentry.android.core.SentryPerformanceProvider} with a + * fallback to {@link io.sentry.android.core.SentryAndroid}. At leas the start timestamp + * should always be set. */ - public @NotNull TimeSpan getSdkAppStartTimeSpan() { - return sdkAppStartSpan; + public @NotNull TimeSpan getSdkInitTimeSpan() { + return sdkInitTimeSpan; } public @NotNull TimeSpan getApplicationOnCreateTimeSpan() { @@ -115,7 +118,7 @@ public void addActivityLifecycleTimeSpans(final @NotNull ActivityLifecycleTimeSp public void clear() { appStartType = AppStartType.UNKNOWN; appStartSpan.reset(); - sdkAppStartSpan.reset(); + sdkInitTimeSpan.reset(); applicationOnCreate.reset(); contentProviderOnCreates.clear(); activityLifecycles.clear(); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java index 0ea334c291..5397790588 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java @@ -99,7 +99,7 @@ public long getStartTimestampMs() { * @return the start timestamp of this measurement, unix time, in seconds */ public double getStartTimestampSecs() { - return (double) startUnixTimeMs / 1000.0d; + return DateUtils.millisToSeconds(startUnixTimeMs); } /** @@ -119,7 +119,7 @@ public long getProjectedStopTimestampMs() { * @see #getProjectedStopTimestampMs() */ public double getProjectedStopTimestampSecs() { - return (double) getProjectedStopTimestampMs() / 1000.0d; + return DateUtils.millisToSeconds(getProjectedStopTimestampMs()); } /** diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index eb054a2117..abd89dc40b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -857,7 +857,7 @@ class ActivityLifecycleIntegrationTest { // usually set by SentryPerformanceProvider val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) - AppStartMetrics.getInstance().sdkAppStartTimeSpan.setStoppedAt(2) + AppStartMetrics.getInstance().sdkInitTimeSpan.setStoppedAt(2) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -877,9 +877,9 @@ class ActivityLifecycleIntegrationTest { setAppStartTime(startDate) val appStartMetrics = AppStartMetrics.getInstance() appStartMetrics.appStartType = AppStartType.WARM - appStartMetrics.sdkAppStartTimeSpan.setStoppedAt(2) + appStartMetrics.sdkInitTimeSpan.setStoppedAt(2) - val endDate = appStartMetrics.sdkAppStartTimeSpan.projectedStopTimestamp + val endDate = appStartMetrics.sdkInitTimeSpan.projectedStopTimestamp val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -910,14 +910,14 @@ class ActivityLifecycleIntegrationTest { whenever(activity.findViewById(any())).thenReturn(view) sut.onActivityCreated(activity, fixture.bundle) // then app-start end time should still be null - assertTrue(AppStartMetrics.getInstance().sdkAppStartTimeSpan.hasNotStopped()) + assertTrue(AppStartMetrics.getInstance().sdkInitTimeSpan.hasNotStopped()) // when activity is resumed sut.onActivityResumed(activity) Thread.sleep(1) runFirstDraw(view) // end-time should be set - assertTrue(AppStartMetrics.getInstance().sdkAppStartTimeSpan.hasStopped()) + assertTrue(AppStartMetrics.getInstance().sdkInitTimeSpan.hasStopped()) } @Test @@ -930,7 +930,7 @@ class ActivityLifecycleIntegrationTest { val startDate = SentryNanotimeDate(Date(1), 0) setAppStartTime(startDate) AppStartMetrics.getInstance().appStartType = AppStartType.WARM - AppStartMetrics.getInstance().sdkAppStartTimeSpan.setStoppedAt(1234) + AppStartMetrics.getInstance().sdkInitTimeSpan.setStoppedAt(1234) // when activity is created and resumed val activity = mock() @@ -940,7 +940,7 @@ class ActivityLifecycleIntegrationTest { // then the end time should not be overwritten assertEquals( DateUtils.millisToNanos(1234), - AppStartMetrics.getInstance().sdkAppStartTimeSpan.projectedStopTimestamp!!.nanoTimestamp() + AppStartMetrics.getInstance().sdkInitTimeSpan.projectedStopTimestamp!!.nanoTimestamp() ) } @@ -965,7 +965,7 @@ class ActivityLifecycleIntegrationTest { Thread.sleep(1) runFirstDraw(view) - val firstAppStartEndTime = AppStartMetrics.getInstance().sdkAppStartTimeSpan.projectedStopTimestamp + val firstAppStartEndTime = AppStartMetrics.getInstance().sdkInitTimeSpan.projectedStopTimestamp Thread.sleep(1) sut.onActivityPaused(activity) @@ -978,7 +978,7 @@ class ActivityLifecycleIntegrationTest { // then the end time should not be overwritten assertEquals( firstAppStartEndTime!!.nanoTimestamp(), - AppStartMetrics.getInstance().sdkAppStartTimeSpan.projectedStopTimestamp!!.nanoTimestamp() + AppStartMetrics.getInstance().sdkInitTimeSpan.projectedStopTimestamp!!.nanoTimestamp() ) } @@ -1496,7 +1496,7 @@ class ActivityLifecycleIntegrationTest { private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(1), 0)) { // set by SentryPerformanceProvider so forcing it here - val appStartTimeSpan = AppStartMetrics.getInstance().sdkAppStartTimeSpan + val appStartTimeSpan = AppStartMetrics.getInstance().sdkInitTimeSpan val millis = DateUtils.nanosToMillis(date.nanoTimestamp().toDouble()).toLong() appStartTimeSpan.setStartedAt(millis) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 936f8b6661..0512d03469 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -68,8 +68,7 @@ class PerformanceAndroidEventProcessorTest { @Test fun `add cold start measurement for performance-v2`() { - val sut = fixture.getSut() - fixture.options.isEnablePerformanceV2 = true + val sut = fixture.getSut(enablePerformanceV2 = true) var tr = getTransaction(AppStartType.COLD) setAppStart(fixture.options) @@ -261,7 +260,7 @@ class PerformanceAndroidEventProcessorTest { false -> AppStartType.WARM } val timeSpan = - if (options.isEnablePerformanceV2) appStartTimeSpan else sdkAppStartTimeSpan + if (options.isEnablePerformanceV2) appStartTimeSpan else sdkInitTimeSpan timeSpan.apply { setStartedAt(1) setStoppedAt(2) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 0a823227d7..f48868ad72 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -434,7 +434,7 @@ class SentryAndroidTest { } @Test - fun `init sets app start times span if performance-v2 is enabled`() { + fun `init backfills sdk init and app start time`() { AppStartMetrics.getInstance().clear() SentryShadowProcess.setStartUptimeMillis(42) @@ -443,7 +443,23 @@ class SentryAndroidTest { options.isEnablePerformanceV2 = true } assertEquals(42, AppStartMetrics.getInstance().appStartTimeSpan.startUptimeMs) - assertTrue(AppStartMetrics.getInstance().sdkAppStartTimeSpan.hasStarted()) + assertTrue(AppStartMetrics.getInstance().sdkInitTimeSpan.hasStarted()) + } + + @Test + fun `init does not backfill sdk init and app start times if already set`() { + AppStartMetrics.getInstance().clear() + AppStartMetrics.getInstance().sdkInitTimeSpan.setStartedAt(99) + AppStartMetrics.getInstance().appStartTimeSpan.setStartedAt(99) + + SentryShadowProcess.setStartUptimeMillis(42) + fixture.initSut(context = mock()) { options -> + options.dsn = "https://key@sentry.io/123" + options.isEnablePerformanceV2 = true + } + + assertEquals(99, AppStartMetrics.getInstance().sdkInitTimeSpan.startUptimeMs) + assertEquals(99, AppStartMetrics.getInstance().appStartTimeSpan.startUptimeMs) } private fun prefillScopeCache(cacheDir: String) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index 08ecf1670b..5ad892e454 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -1,18 +1,17 @@ package io.sentry.android.core +import android.app.Application +import android.content.Context import android.content.pm.ProviderInfo import android.os.Build import android.os.Bundle -import android.os.Looper -import android.view.View -import android.view.ViewTreeObserver -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.android.core.performance.AppStartMetrics import io.sentry.android.core.performance.AppStartMetrics.AppStartType import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.mock -import org.robolectric.Shadows +import org.mockito.kotlin.verify import org.robolectric.annotation.Config import kotlin.test.BeforeTest import kotlin.test.Test @@ -34,8 +33,10 @@ class SentryPerformanceProviderTest { @Test fun `provider starts appStartTimeSpan`() { + assertTrue(AppStartMetrics.getInstance().sdkInitTimeSpan.hasNotStarted()) assertTrue(AppStartMetrics.getInstance().appStartTimeSpan.hasNotStarted()) setupProvider() + assertTrue(AppStartMetrics.getInstance().sdkInitTimeSpan.hasStarted()) assertTrue(AppStartMetrics.getInstance().appStartTimeSpan.hasStarted()) } @@ -67,7 +68,7 @@ class SentryPerformanceProviderTest { } @Test - fun `provider sets keeps startup state even if multiple activities are launched`() { + fun `provider keeps startup state even if multiple activities are launched`() { val provider = setupProvider() // when there's a saved state @@ -83,34 +84,37 @@ class SentryPerformanceProviderTest { assertEquals(AppStartType.WARM, AppStartMetrics.getInstance().appStartType) } - private fun setupProvider(): SentryPerformanceProvider { - val providerInfo = ProviderInfo() + @Test + fun `provider sets both appstart and sdk init start + end times`() { + val provider = setupProvider() + provider.onAppStartDone() - val mockContext = ContextUtilsTestHelper.createMockContext(true) - providerInfo.authority = AUTHORITY + val metrics = AppStartMetrics.getInstance() + assertTrue(metrics.appStartTimeSpan.hasStarted()) + assertTrue(metrics.appStartTimeSpan.hasStopped()) - // calls onCreate - val provider = SentryPerformanceProvider() - provider.attachInfo(mockContext, providerInfo) - return provider + assertTrue(metrics.sdkInitTimeSpan.hasStarted()) + assertTrue(metrics.sdkInitTimeSpan.hasStopped()) } - private fun createView(): View { - val view = View(ApplicationProvider.getApplicationContext()) - - // Adding a listener forces ViewTreeObserver.mOnDrawListeners to be initialized and non-null. - val dummyListener = ViewTreeObserver.OnDrawListener {} - view.viewTreeObserver.addOnDrawListener(dummyListener) - view.viewTreeObserver.removeOnDrawListener(dummyListener) + @Test + fun `provider properly registers and unregisters ActivityLifecycleCallbacks`() { + val context = mock() + val provider = setupProvider(context) - return view + verify(context).registerActivityLifecycleCallbacks(any()) + provider.onAppStartDone() + verify(context).unregisterActivityLifecycleCallbacks(any()) } - private fun runFirstDraw(view: View) { - // Removes OnDrawListener in the next OnGlobalLayout after onDraw - view.viewTreeObserver.dispatchOnDraw() - view.viewTreeObserver.dispatchOnGlobalLayout() - Shadows.shadowOf(Looper.getMainLooper()).idle() + private fun setupProvider(context: Context = mock()): SentryPerformanceProvider { + val providerInfo = ProviderInfo() + providerInfo.authority = AUTHORITY + + // calls onCreate under the hood + val provider = SentryPerformanceProvider() + provider.attachInfo(context, providerInfo) + return provider } companion object { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index 3637a8a7d5..92b5d773fd 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -53,8 +53,8 @@ class AndroidEnvelopeCacheTest { appStartTimeSpan.setStartedAt(appStartMillis) appStartTimeSpan.setStartUnixTimeMs(appStartMillis) } else { - sdkAppStartTimeSpan.setStartedAt(appStartMillis) - sdkAppStartTimeSpan.setStartUnixTimeMs(appStartMillis) + sdkInitTimeSpan.setStartedAt(appStartMillis) + sdkInitTimeSpan.setStartUnixTimeMs(appStartMillis) } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt index 24ae78bc97..4fc5b16915 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -36,7 +36,7 @@ class AppStartMetricsTest { fun `metrics are properly cleared`() { val metrics = AppStartMetrics.getInstance() metrics.appStartTimeSpan.start() - metrics.sdkAppStartTimeSpan.start() + metrics.sdkInitTimeSpan.start() metrics.appStartType = AppStartMetrics.AppStartType.WARM metrics.applicationOnCreateTimeSpan.start() metrics.addActivityLifecycleTimeSpans(ActivityLifecycleTimeSpan()) @@ -46,7 +46,7 @@ class AppStartMetricsTest { metrics.clear() assertTrue(metrics.appStartTimeSpan.hasNotStarted()) - assertTrue(metrics.sdkAppStartTimeSpan.hasNotStarted()) + assertTrue(metrics.sdkInitTimeSpan.hasNotStarted()) assertTrue(metrics.applicationOnCreateTimeSpan.hasNotStarted()) assertEquals(AppStartMetrics.AppStartType.UNKNOWN, metrics.appStartType) assertTrue(metrics.applicationOnCreateTimeSpan.hasNotStarted()) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt index b60a619031..7094835d10 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/TimeSpanTest.kt @@ -38,6 +38,7 @@ class TimeSpanTest { @Test fun `spans reset`() { val span = TimeSpan().apply { + description = "Hello World" setStartedAt(1) setStoppedAt(2) } From a93402a3ab18847e6b9bb2c920f35a480cf50037 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 6 Dec 2023 13:19:35 +0100 Subject: [PATCH 11/15] Address PR feedback --- .../api/sentry-android-core.api | 1 + .../core/ActivityLifecycleIntegration.java | 16 ++++---------- .../core/DefaultAndroidEventProcessor.java | 6 +----- .../android/core/InternalSentrySdk.java | 6 +----- .../PerformanceAndroidEventProcessor.java | 6 +----- .../core/performance/AppStartMetrics.java | 15 +++++++++++++ .../core/performance/AppStartMetricsTest.kt | 21 ++++++++++++++++++- 7 files changed, 43 insertions(+), 28 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 2a01d12c40..b83f5e353c 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -407,6 +407,7 @@ public class io/sentry/android/core/performance/AppStartMetrics { public fun clear ()V public fun getActivityLifecycleTimeSpans ()Ljava/util/List; public fun getAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; + public fun getAppStartTimeSpanWithFallback ()Lio/sentry/android/core/performance/TimeSpan; public fun getAppStartType ()Lio/sentry/android/core/performance/AppStartMetrics$AppStartType; public fun getApplicationOnCreateTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; public fun getContentProviderOnCreateTimeSpans ()Ljava/util/List; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 4795b4a959..1b2e20095b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -187,7 +187,8 @@ private void startTracing(final @NotNull Activity activity) { final @Nullable SentryDate appStartTime; final @Nullable Boolean coldStart; - final TimeSpan appStartTimeSpan = getAppStartTimeSpan(); + final TimeSpan appStartTimeSpan = + AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(); // we only track app start for processes that will show an Activity (full launch). // Here we check the process importance which will tell us that. @@ -684,19 +685,10 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) { } private void finishAppStartSpan() { - final @NotNull TimeSpan appStartTimeSpan = getAppStartTimeSpan(); - final @Nullable SentryDate appStartEndTime = appStartTimeSpan.getProjectedStopTimestamp(); + final @Nullable SentryDate appStartEndTime = + AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback().getProjectedStopTimestamp(); if (performanceEnabled && appStartEndTime != null) { finishSpan(appStartSpan, appStartEndTime); } } - - private @NotNull TimeSpan getAppStartTimeSpan() { - final @NotNull TimeSpan appStartTimeSpan = - (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N - && options.isEnablePerformanceV2()) - ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getSdkInitTimeSpan(); - return appStartTimeSpan; - } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 4b4bba5aca..f1c8e5d6a6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -3,7 +3,6 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.os.Build; import io.sentry.DateUtils; import io.sentry.EventProcessor; import io.sentry.Hint; @@ -203,10 +202,7 @@ private void setDist(final @NotNull SentryBaseEvent event, final @NotNull String private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); final @NotNull TimeSpan appStartTimeSpan = - (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N - && options.isEnablePerformanceV2()) - ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getSdkInitTimeSpan(); + AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(); if (appStartTimeSpan.hasStarted()) { app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp())); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index 88df7373e0..b75b42eba7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -3,7 +3,6 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.os.Build; import io.sentry.DateUtils; import io.sentry.HubAdapter; import io.sentry.IHub; @@ -104,10 +103,7 @@ public static Map serializeScope( app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); final @NotNull TimeSpan appStartTimeSpan = - (new BuildInfoProvider(options.getLogger()).getSdkInfoVersion() >= Build.VERSION_CODES.N - && options.isEnablePerformanceV2()) - ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getSdkInitTimeSpan(); + AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(); if (appStartTimeSpan.hasStarted()) { app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp())); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 2c61777112..577ac50d4f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -4,7 +4,6 @@ import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM; import static io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP; -import android.os.Build; import io.sentry.EventProcessor; import io.sentry.Hint; import io.sentry.MeasurementUnit; @@ -72,10 +71,7 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { // the app.start span, which is automatically created by the SDK. if (!sentStartMeasurement && hasAppStartSpan(transaction)) { final @NotNull TimeSpan appStartTimeSpan = - (new BuildInfoProvider(options.getLogger()).getSdkInfoVersion() >= Build.VERSION_CODES.N - && options.isEnablePerformanceV2()) - ? AppStartMetrics.getInstance().getAppStartTimeSpan() - : AppStartMetrics.getInstance().getSdkInitTimeSpan(); + AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(); final long appStartUpInterval = appStartTimeSpan.getDurationMs(); // if appStartUpInterval is 0, metrics are not ready to be sent diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 8a161baf5b..7d84a40bd6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -115,6 +115,21 @@ public void addActivityLifecycleTimeSpans(final @NotNull ActivityLifecycleTimeSp activityLifecycles.add(timeSpan); } + /** + * @return the app start time span, in case it's was never started, the sdk init time span is + * returned instead + */ + public @NotNull TimeSpan getAppStartTimeSpanWithFallback() { + // should only be started if performance v2 is enabled and the sdk version is >= N + final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan(); + if (appStartSpan.hasStarted()) { + return appStartSpan; + } + + // fallback: use sdk init time span, as it will always have a start time set + return getSdkInitTimeSpan(); + } + public void clear() { appStartType = AppStartType.UNKNOWN; appStartSpan.reset(); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt index 4fc5b16915..472491ad11 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -28,7 +28,7 @@ class AppStartMetricsTest { } @Test - fun `getInstance returns a singelton`() { + fun `getInstance returns a singleton`() { assertSame(AppStartMetrics.getInstance(), AppStartMetrics.getInstance()) } @@ -54,4 +54,23 @@ class AppStartMetricsTest { assertTrue(metrics.activityLifecycleTimeSpans.isEmpty()) assertTrue(metrics.contentProviderOnCreateTimeSpans.isEmpty()) } + + @Test + fun `if app start time span is started, appStartTimeSpanWithFallback returns it`() { + val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan + appStartTimeSpan.start() + + val timeSpan = AppStartMetrics.getInstance().appStartTimeSpanWithFallback + assertSame(appStartTimeSpan, timeSpan) + } + + @Test + fun `if app start time span is not started, appStartTimeSpanWithFallback returns the sdk init span instead`() { + val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan + assertTrue(appStartTimeSpan.hasNotStarted()) + + val timeSpan = AppStartMetrics.getInstance().appStartTimeSpanWithFallback + val sdkInitSpan = AppStartMetrics.getInstance().sdkInitTimeSpan + assertSame(sdkInitSpan, timeSpan) + } } From b8a1b57e792a35b742eb2fb96256a7893e8819c8 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 12 Dec 2023 08:03:11 +0100 Subject: [PATCH 12/15] [Starfish] Attach app start spans to app.start.cold txn (#3067) Co-authored-by: Sentry Github Bot --- .../PerformanceAndroidEventProcessor.java | 83 +++++------ .../core/ActivityLifecycleIntegrationTest.kt | 15 +- .../PerformanceAndroidEventProcessorTest.kt | 134 +++++++++++++++--- 3 files changed, 162 insertions(+), 70 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 577ac50d4f..e8ed25c209 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -28,7 +28,12 @@ /** Event Processor responsible for adding Android metrics to transactions */ final class PerformanceAndroidEventProcessor implements EventProcessor { - private static final String APP_METRICS_ORIGN = "auto.ui"; + private static final String APP_METRICS_ORIGIN = "auto.ui"; + + private static final String APP_METRICS_CONTENT_PROVIDER_OP = "contentprovider.load"; + private static final String APP_METRICS_ACTIVITIES_OP = "activity.load"; + private static final String APP_METRICS_APPLICATION_OP = "application.load"; + private boolean sentStartMeasurement = false; private final @NotNull ActivityFramesTracker activityFramesTracker; @@ -140,77 +145,60 @@ private void attachColdAppStartSpans( } final @NotNull SentryId traceId = traceContext.getTraceId(); - // Application.onCreate - final @NotNull TimeSpan appOnCreate = appStartMetrics.getApplicationOnCreateTimeSpan(); - if (appOnCreate.hasStopped()) { - final SentrySpan span = timeSpanToSentrySpan(appOnCreate, null, traceId); - txn.getSpans().add(span); + // determine the app.start.cold span, where all other spans will be attached to + @Nullable SpanId parentSpanId = null; + final @NotNull List spans = txn.getSpans(); + for (final @NotNull SentrySpan span : spans) { + if (span.getOp().contentEquals(APP_START_COLD)) { + parentSpanId = span.getSpanId(); + break; + } } - // Content Provider + // Content Providers final @NotNull List contentProviderOnCreates = appStartMetrics.getContentProviderOnCreateTimeSpans(); if (!contentProviderOnCreates.isEmpty()) { - final @NotNull SentrySpan contentProviderRootSpan = - new SentrySpan( - contentProviderOnCreates.get(0).getStartTimestampSecs(), - contentProviderOnCreates - .get(contentProviderOnCreates.size() - 1) - .getProjectedStopTimestampSecs(), - traceId, - new SpanId(), - null, - UI_LOAD_OP, - "Content Providers", - SpanStatus.OK, - APP_METRICS_ORIGN, - new HashMap<>(), - null); - txn.getSpans().add(contentProviderRootSpan); for (final @NotNull TimeSpan contentProvider : contentProviderOnCreates) { txn.getSpans() .add( timeSpanToSentrySpan( - contentProvider, contentProviderRootSpan.getSpanId(), traceId)); + contentProvider, parentSpanId, traceId, APP_METRICS_CONTENT_PROVIDER_OP)); } } + // Application.onCreate + final @NotNull TimeSpan appOnCreate = appStartMetrics.getApplicationOnCreateTimeSpan(); + if (appOnCreate.hasStopped()) { + txn.getSpans() + .add( + timeSpanToSentrySpan(appOnCreate, parentSpanId, traceId, APP_METRICS_APPLICATION_OP)); + } + // Activities final @NotNull List activityLifecycleTimeSpans = appStartMetrics.getActivityLifecycleTimeSpans(); if (!activityLifecycleTimeSpans.isEmpty()) { - final SentrySpan activityRootSpan = - new SentrySpan( - activityLifecycleTimeSpans.get(0).getOnCreate().getStartTimestampSecs(), - activityLifecycleTimeSpans - .get(activityLifecycleTimeSpans.size() - 1) - .getOnStart() - .getProjectedStopTimestampSecs(), - traceId, - new SpanId(), - null, - UI_LOAD_OP, - "Activities", - SpanStatus.OK, - APP_METRICS_ORIGN, - new HashMap<>(), - null); - txn.getSpans().add(activityRootSpan); - for (ActivityLifecycleTimeSpan activityTimeSpan : activityLifecycleTimeSpans) { if (activityTimeSpan.getOnCreate().hasStarted() && activityTimeSpan.getOnCreate().hasStopped()) { txn.getSpans() .add( timeSpanToSentrySpan( - activityTimeSpan.getOnCreate(), activityRootSpan.getSpanId(), traceId)); + activityTimeSpan.getOnCreate(), + parentSpanId, + traceId, + APP_METRICS_ACTIVITIES_OP)); } if (activityTimeSpan.getOnStart().hasStarted() && activityTimeSpan.getOnStart().hasStopped()) { txn.getSpans() .add( timeSpanToSentrySpan( - activityTimeSpan.getOnStart(), activityRootSpan.getSpanId(), traceId)); + activityTimeSpan.getOnStart(), + parentSpanId, + traceId, + APP_METRICS_ACTIVITIES_OP)); } } } @@ -220,17 +208,18 @@ private void attachColdAppStartSpans( private static SentrySpan timeSpanToSentrySpan( final @NotNull TimeSpan span, final @Nullable SpanId parentSpanId, - final @NotNull SentryId traceId) { + final @NotNull SentryId traceId, + final @NotNull String operation) { return new SentrySpan( span.getStartTimestampSecs(), span.getProjectedStopTimestampSecs(), traceId, new SpanId(), parentSpanId, - ActivityLifecycleIntegration.UI_LOAD_OP, + operation, span.getDescription(), SpanStatus.OK, - APP_METRICS_ORIGN, + APP_METRICS_ORIGIN, new HashMap<>(), null); } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index abd89dc40b..f5f482ad43 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -78,7 +78,6 @@ class ActivityLifecycleIntegrationTest { dsn = "https://key@sentry.io/proj" } val bundle = mock() - val context = TransactionContext("name", "op") val activityFramesTracker = mock() val fullyDisplayedReporter = FullyDisplayedReporter.getInstance() val transactionFinishedCallback = mock() @@ -98,9 +97,10 @@ class ActivityLifecycleIntegrationTest { whenever(hub.options).thenReturn(options) // We let the ActivityLifecycleIntegration create the proper transaction here - val argumentCaptor = argumentCaptor() - whenever(hub.startTransaction(any(), argumentCaptor.capture())).thenAnswer { - val t = SentryTracer(context, hub, argumentCaptor.lastValue) + val optionCaptor = argumentCaptor() + val contextCaptor = argumentCaptor() + whenever(hub.startTransaction(contextCaptor.capture(), optionCaptor.capture())).thenAnswer { + val t = SentryTracer(contextCaptor.lastValue, hub, optionCaptor.lastValue) transaction = t return@thenAnswer t } @@ -1496,9 +1496,14 @@ class ActivityLifecycleIntegrationTest { private fun setAppStartTime(date: SentryDate = SentryNanotimeDate(Date(1), 0)) { // set by SentryPerformanceProvider so forcing it here - val appStartTimeSpan = AppStartMetrics.getInstance().sdkInitTimeSpan + val sdkAppStartTimeSpan = AppStartMetrics.getInstance().sdkInitTimeSpan + val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan val millis = DateUtils.nanosToMillis(date.nanoTimestamp().toDouble()).toLong() + sdkAppStartTimeSpan.setStartedAt(millis) + sdkAppStartTimeSpan.setStartUnixTimeMs(millis) + sdkAppStartTimeSpan.setStoppedAt(0) + appStartTimeSpan.setStartedAt(millis) appStartTimeSpan.setStartUnixTimeMs(millis) appStartTimeSpan.setStoppedAt(0) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 0512d03469..1d98aae539 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -1,5 +1,6 @@ package io.sentry.android.core +import android.content.ContentProvider import io.sentry.Hint import io.sentry.IHub import io.sentry.MeasurementUnit @@ -23,6 +24,7 @@ import org.mockito.kotlin.whenever import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue class PerformanceAndroidEventProcessorTest { @@ -200,6 +202,15 @@ class PerformanceAndroidEventProcessorTest { appStartMetrics.appStartTimeSpan.setStartedAt(123) appStartMetrics.appStartTimeSpan.setStoppedAt(456) + val contentProvider = mock() + AppStartMetrics.onContentProviderCreate(contentProvider) + AppStartMetrics.onContentProviderPostCreate(contentProvider) + + appStartMetrics.applicationOnCreateTimeSpan.apply { + setStartedAt(10) + setStoppedAt(42) + } + val activityTimeSpan = ActivityLifecycleTimeSpan() activityTimeSpan.onCreate.description = "MainActivity.onCreate" activityTimeSpan.onStart.description = "MainActivity.onStart" @@ -216,39 +227,126 @@ class PerformanceAndroidEventProcessorTest { val tracer = SentryTracer(context, fixture.hub) var tr = SentryTransaction(tracer) - // and it contains an app start signal - tr.spans.add( - SentrySpan( - 0.0, - 1.0, - tr.contexts.trace!!.traceId, - SpanId(), - null, - APP_START_COLD, - "App Start", - SpanStatus.OK, - null, - emptyMap(), - null - ) + // and it contains an app.start.cold span + val appStartSpan = SentrySpan( + 0.0, + 1.0, + tr.contexts.trace!!.traceId, + SpanId(), + null, + APP_START_COLD, + "App Start", + SpanStatus.OK, + null, + emptyMap(), + null ) + tr.spans.add(appStartSpan) // then the app start metrics should be attached tr = sut.process(tr, Hint()) assertTrue( tr.spans.any { - UI_LOAD_OP == it.op && "Activity" == it.description + "contentprovider.load" == it.op && + appStartSpan.spanId == it.parentSpanId + } + ) + + assertTrue( + tr.spans.any { + "application.load" == it.op + } + ) + + assertTrue( + tr.spans.any { + "activity.load" == it.op && "MainActivity.onCreate" == it.description } ) assertTrue( tr.spans.any { - UI_LOAD_OP == it.op && "MainActivity.onCreate" == it.description + "activity.load" == it.op && "MainActivity.onStart" == it.description + } + ) + } + + @Test + fun `does not add app start metrics to app start txn when it is not a cold start`() { + // given some WARM app start metrics + val appStartMetrics = AppStartMetrics.getInstance() + appStartMetrics.appStartType = AppStartType.WARM + appStartMetrics.appStartTimeSpan.setStartedAt(123) + appStartMetrics.appStartTimeSpan.setStoppedAt(456) + appStartMetrics.applicationOnCreateTimeSpan.apply { + setStartedAt(10) + setStoppedAt(42) + } + // when an activity transaction is created + val sut = fixture.getSut(enablePerformanceV2 = true) + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.hub) + var tr = SentryTransaction(tracer) + + // then the app start metrics should not be attached + tr = sut.process(tr, Hint()) + + assertFalse( + tr.spans.any { + "application.load" == it.op } ) + } + + @Test + fun `does not add app start metrics more than once`() { + // given some WARM app start metrics + val appStartMetrics = AppStartMetrics.getInstance() + appStartMetrics.appStartType = AppStartType.COLD + appStartMetrics.appStartTimeSpan.setStartedAt(123) + appStartMetrics.appStartTimeSpan.setStoppedAt(456) + + appStartMetrics.applicationOnCreateTimeSpan.apply { + setStartedAt(10) + setStoppedAt(42) + } + + // when the first activity transaction is created + val sut = fixture.getSut(enablePerformanceV2 = true) + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.hub) + var tr = SentryTransaction(tracer) + val appStartSpan = SentrySpan( + 0.0, + 1.0, + tr.contexts.trace!!.traceId, + SpanId(), + null, + APP_START_COLD, + "App Start", + SpanStatus.OK, + null, + emptyMap(), + null + ) + tr.spans.add(appStartSpan) + + // then the app start metrics should not be attached + tr = sut.process(tr, Hint()) + assertTrue( tr.spans.any { - UI_LOAD_OP == it.op && "MainActivity.onStart" == it.description + "application.load" == it.op + } + ) + + // but not on the second activity transaction + var tr2 = SentryTransaction(tracer) + tr2.spans.add(appStartSpan) + tr2 = sut.process(tr2, Hint()) + assertFalse( + tr2.spans.any { + "application.load" == it.op } ) } From cadcad4b20143827515a05dd5d13b69ba77f4091 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 12 Dec 2023 12:01:36 +0100 Subject: [PATCH 13/15] Fix tests --- .../api/sentry-android-core.api | 2 +- .../core/ActivityLifecycleIntegration.java | 6 ++-- .../core/DefaultAndroidEventProcessor.java | 2 +- .../android/core/InternalSentrySdk.java | 2 +- .../PerformanceAndroidEventProcessor.java | 2 +- .../core/performance/AppStartMetrics.java | 20 +++++++----- .../core/performance/AppStartMetricsTest.kt | 31 ++++++++++++++++--- 7 files changed, 47 insertions(+), 18 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index b83f5e353c..e6f49c5904 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -407,7 +407,7 @@ public class io/sentry/android/core/performance/AppStartMetrics { public fun clear ()V public fun getActivityLifecycleTimeSpans ()Ljava/util/List; public fun getAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; - public fun getAppStartTimeSpanWithFallback ()Lio/sentry/android/core/performance/TimeSpan; + public fun getAppStartTimeSpanWithFallback (Lio/sentry/android/core/SentryAndroidOptions;)Lio/sentry/android/core/performance/TimeSpan; public fun getAppStartType ()Lio/sentry/android/core/performance/AppStartMetrics$AppStartType; public fun getApplicationOnCreateTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; public fun getContentProviderOnCreateTimeSpans ()Ljava/util/List; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 1b2e20095b..87771da8e0 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -188,7 +188,7 @@ private void startTracing(final @NotNull Activity activity) { final @Nullable SentryDate appStartTime; final @Nullable Boolean coldStart; final TimeSpan appStartTimeSpan = - AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(); + AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options); // we only track app start for processes that will show an Activity (full launch). // Here we check the process importance which will tell us that. @@ -686,7 +686,9 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) { private void finishAppStartSpan() { final @Nullable SentryDate appStartEndTime = - AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback().getProjectedStopTimestamp(); + AppStartMetrics.getInstance() + .getAppStartTimeSpanWithFallback(options) + .getProjectedStopTimestamp(); if (performanceEnabled && appStartEndTime != null) { finishSpan(appStartSpan, appStartEndTime); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index f1c8e5d6a6..8dcfe196c2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -202,7 +202,7 @@ private void setDist(final @NotNull SentryBaseEvent event, final @NotNull String private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); final @NotNull TimeSpan appStartTimeSpan = - AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(); + AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options); if (appStartTimeSpan.hasStarted()) { app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp())); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index b75b42eba7..9bdbe86a77 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -103,7 +103,7 @@ public static Map serializeScope( app.setAppName(ContextUtils.getApplicationName(context, options.getLogger())); final @NotNull TimeSpan appStartTimeSpan = - AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(); + AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options); if (appStartTimeSpan.hasStarted()) { app.setAppStartTime(DateUtils.toUtilDate(appStartTimeSpan.getStartTimestamp())); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index e8ed25c209..ec7e3b74d9 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -76,7 +76,7 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { // the app.start span, which is automatically created by the SDK. if (!sentStartMeasurement && hasAppStartSpan(transaction)) { final @NotNull TimeSpan appStartTimeSpan = - AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(); + AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options); final long appStartUpInterval = appStartTimeSpan.getDurationMs(); // if appStartUpInterval is 0, metrics are not ready to be sent diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 7d84a40bd6..41f9feecd7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -5,6 +5,7 @@ import android.os.SystemClock; import androidx.annotation.Nullable; import io.sentry.android.core.ContextUtils; +import io.sentry.android.core.SentryAndroidOptions; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -71,7 +72,7 @@ public AppStartMetrics() { * @return the SDK init time span, as measured pre-performance-v2 Uses ContentProvider/Sdk init * time as start timestamp *

Data is filled by either {@link io.sentry.android.core.SentryPerformanceProvider} with a - * fallback to {@link io.sentry.android.core.SentryAndroid}. At leas the start timestamp + * fallback to {@link io.sentry.android.core.SentryAndroid}. At least the start timestamp * should always be set. */ public @NotNull TimeSpan getSdkInitTimeSpan() { @@ -116,14 +117,17 @@ public void addActivityLifecycleTimeSpans(final @NotNull ActivityLifecycleTimeSp } /** - * @return the app start time span, in case it's was never started, the sdk init time span is - * returned instead + * @return the app start time span if it was started and perf-2 is enabled, falls back to the sdk + * init time span otherwise */ - public @NotNull TimeSpan getAppStartTimeSpanWithFallback() { - // should only be started if performance v2 is enabled and the sdk version is >= N - final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan(); - if (appStartSpan.hasStarted()) { - return appStartSpan; + public @NotNull TimeSpan getAppStartTimeSpanWithFallback( + final @NotNull SentryAndroidOptions options) { + if (options.isEnablePerformanceV2()) { + // Only started when sdk version is >= N + final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan(); + if (appStartSpan.hasStarted()) { + return appStartSpan; + } } // fallback: use sdk init time span, as it will always have a start time set diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt index 472491ad11..bec98f04fc 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.ContentProvider import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.android.core.SentryAndroidOptions import io.sentry.android.core.SentryShadowProcess import org.junit.Before import org.junit.runner.RunWith @@ -56,20 +57,42 @@ class AppStartMetricsTest { } @Test - fun `if app start time span is started, appStartTimeSpanWithFallback returns it`() { + fun `if perf-2 is enabled and app start time span is started, appStartTimeSpanWithFallback returns it`() { val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan appStartTimeSpan.start() - val timeSpan = AppStartMetrics.getInstance().appStartTimeSpanWithFallback + val options = SentryAndroidOptions().apply { + isEnablePerformanceV2 = true + } + + val timeSpan = AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options) assertSame(appStartTimeSpan, timeSpan) } @Test - fun `if app start time span is not started, appStartTimeSpanWithFallback returns the sdk init span instead`() { + fun `if perf-2 is disabled but app start time span has started, appStartTimeSpanWithFallback returns the sdk init span instead`() { + val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan + appStartTimeSpan.start() + + val options = SentryAndroidOptions().apply { + isEnablePerformanceV2 = false + } + + val timeSpan = AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options) + val sdkInitSpan = AppStartMetrics.getInstance().sdkInitTimeSpan + assertSame(sdkInitSpan, timeSpan) + } + + @Test + fun `if perf-2 is enabled but app start time span has not started, appStartTimeSpanWithFallback returns the sdk init span instead`() { val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan assertTrue(appStartTimeSpan.hasNotStarted()) - val timeSpan = AppStartMetrics.getInstance().appStartTimeSpanWithFallback + val options = SentryAndroidOptions().apply { + isEnablePerformanceV2 = true + } + + val timeSpan = AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options) val sdkInitSpan = AppStartMetrics.getInstance().sdkInitTimeSpan assertSame(sdkInitSpan, timeSpan) } From fb0e5e7a71982bcd688f321646db89569063e696 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 12 Dec 2023 20:57:08 +0100 Subject: [PATCH 14/15] Improve docs and changelog --- CHANGELOG.md | 5 ++++- .../io/sentry/android/core/SentryAndroidOptions.java | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2f25d73fb..ba5982758f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,10 @@ ### Features - Support multiple debug-metadata.properties ([#3024](https://github.com/getsentry/sentry-java/pull/3024)) -- (Internal, Experimental) Attach spans for Application, ContentProvider, and Activities to app-start ([#3057](https://github.com/getsentry/sentry-java/pull/3057)) +- (Android) Experimental: Provide more detailed cold app start information ([#3057](https://github.com/getsentry/sentry-java/pull/3057)) + - Attaches spans for Application, ContentProvider, and Activities to app-start timings + - Uses Process.startUptimeMillis to calculate app-start timings + - To enable this feature set `options.isEnablePerformanceV2 = true` ### Fixes diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index b8cae90d4c..275dfa3d98 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -592,11 +592,23 @@ public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) { this.attachAnrThreadDump = attachAnrThreadDump; } + /** + * @return true if performance-v2 is enabled. See {@link #setEnablePerformanceV2(boolean)} for + * more details. + */ @ApiStatus.Experimental public boolean isEnablePerformanceV2() { return enablePerformanceV2; } + /** + * Experimental: Enables or disables the Performance V2 SDK features. + * + *

With this change - Cold app start spans will provide more accurate timings - Cold app start + * spans will be enriched with detailed ContentProvider, Application and Activity startup times + * + * @param enablePerformanceV2 true if enabled or false otherwise + */ @ApiStatus.Experimental public void setEnablePerformanceV2(final boolean enablePerformanceV2) { this.enablePerformanceV2 = enablePerformanceV2; From 064cae62502aec5658a948b6357e0f3d7bad4004 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 13 Dec 2023 09:46:42 +0100 Subject: [PATCH 15/15] Add more logging to better identify failing E2E tests --- .../io/sentry/uitest/android/mockservers/RelayAsserter.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/RelayAsserter.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/RelayAsserter.kt index a11f8566ba..47e8f09da4 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/RelayAsserter.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/mockservers/RelayAsserter.kt @@ -5,6 +5,8 @@ import io.sentry.Sentry import io.sentry.SentryEnvelope import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest +import okio.GzipSource +import okio.buffer import java.io.IOException import java.util.zip.GZIPInputStream @@ -98,5 +100,9 @@ class RelayAsserter( assertion(EnvelopeAsserter(it, response)) } ?: throw AssertionError("Was unable to parse the request as an envelope: $request") } + + override fun toString(): String { + return "RelayResponse(request=${request.requestLine}\n${GzipSource(request.body).buffer().readUtf8()}\n, response=$response)" + } } }