Skip to content

Commit 48f18b6

Browse files
authored
Merge 8823143 into 373370e
2 parents 373370e + 8823143 commit 48f18b6

File tree

6 files changed

+145
-30
lines changed

6 files changed

+145
-30
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Fix Ensure app start type is set, even when ActivityLifecycleIntegration is not running ([#4216](https://github.com/getsentry/sentry-java/pull/4216))
8+
39
## 7.22.0
410

511
### Fixes

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
462462
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V
463463
public fun setClassLoadedUptimeMs (J)V
464464
public fun shouldSendStartMeasurements ()Z
465+
public fun updateAppStartType (ZJ)V
465466
}
466467

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

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

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ public synchronized void onActivityCreated(
397397
if (!isAllActivityCallbacksAvailable) {
398398
onActivityPreCreated(activity, savedInstanceState);
399399
}
400-
setColdStart(savedInstanceState);
400+
setColdStart(savedInstanceState != null);
401401
if (hub != null && options != null && options.isEnableScreenTracking()) {
402402
final @Nullable String activityClassName = ClassUtil.getClassName(activity);
403403
hub.configureScope(scope -> scope.setScreen(activityClassName));
@@ -705,24 +705,9 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() {
705705
return ttfdSpanMap;
706706
}
707707

708-
private void setColdStart(final @Nullable Bundle savedInstanceState) {
708+
private void setColdStart(final boolean hasBundle) {
709709
if (!firstActivityCreated) {
710-
final @NotNull TimeSpan appStartSpan = AppStartMetrics.getInstance().getAppStartTimeSpan();
711-
// If the app start span already started and stopped, it means the app restarted without
712-
// killing the process, so we are in a warm start
713-
// If the app has an invalid cold start, it means it was started in the background, like
714-
// via BroadcastReceiver, so we consider it a warm start
715-
if ((appStartSpan.hasStarted() && appStartSpan.hasStopped())
716-
|| (!AppStartMetrics.getInstance().isColdStartValid())) {
717-
AppStartMetrics.getInstance().restartAppStart(lastPausedUptimeMillis);
718-
AppStartMetrics.getInstance().setAppStartType(AppStartMetrics.AppStartType.WARM);
719-
} else {
720-
AppStartMetrics.getInstance()
721-
.setAppStartType(
722-
savedInstanceState == null
723-
? AppStartMetrics.AppStartType.COLD
724-
: AppStartMetrics.AppStartType.WARM);
725-
}
710+
AppStartMetrics.getInstance().updateAppStartType(hasBundle, lastPausedUptimeMillis);
726711
}
727712
}
728713

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

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
import android.content.pm.ProviderInfo;
1010
import android.net.Uri;
1111
import android.os.Build;
12+
import android.os.Bundle;
1213
import android.os.Handler;
1314
import android.os.Looper;
1415
import android.os.Process;
1516
import android.os.SystemClock;
17+
import android.util.Log;
1618
import androidx.annotation.NonNull;
1719
import io.sentry.ILogger;
1820
import io.sentry.ITransactionProfiler;
@@ -34,6 +36,7 @@
3436
import java.io.InputStreamReader;
3537
import java.io.Reader;
3638
import java.util.concurrent.atomic.AtomicBoolean;
39+
import java.util.concurrent.atomic.AtomicInteger;
3740
import org.jetbrains.annotations.ApiStatus;
3841
import org.jetbrains.annotations.NotNull;
3942
import org.jetbrains.annotations.Nullable;
@@ -51,6 +54,8 @@ public final class SentryPerformanceProvider extends EmptySecureContentProvider
5154

5255
private final @NotNull ILogger logger;
5356
private final @NotNull BuildInfoProvider buildInfoProvider;
57+
private final AtomicInteger activeActivitiesCounter = new AtomicInteger();
58+
private final AtomicBoolean firstDrawDone = new AtomicBoolean(false);
5459

5560
@TestOnly
5661
SentryPerformanceProvider(
@@ -200,12 +205,27 @@ private void onAppLaunched(
200205
appStartTimespan.setStartedAt(Process.getStartUptimeMillis());
201206
appStartMetrics.registerApplicationForegroundCheck(app);
202207

203-
final AtomicBoolean firstDrawDone = new AtomicBoolean(false);
204-
205208
activityCallback =
206209
new ActivityLifecycleCallbacksAdapter() {
210+
207211
@Override
208-
public void onActivityStarted(@NonNull Activity activity) {
212+
public void onActivityCreated(
213+
@NotNull Activity activity, @Nullable Bundle savedInstanceState) {
214+
Log.d("TAG", "onActivityCreated");
215+
activeActivitiesCounter.incrementAndGet();
216+
217+
// In case the SDK gets initialized async or the
218+
// ActivityLifecycleIntegration is not enabled (e.g on RN due to Context not being
219+
// instanceof Application)
220+
// the app start type never gets set
221+
if (!firstDrawDone.get()) {
222+
final long now = SystemClock.uptimeMillis();
223+
AppStartMetrics.getInstance().updateAppStartType(savedInstanceState != null, now);
224+
}
225+
}
226+
227+
@Override
228+
public void onActivityStarted(@NotNull Activity activity) {
209229
if (firstDrawDone.get()) {
210230
return;
211231
}
@@ -216,20 +236,26 @@ public void onActivityStarted(@NonNull Activity activity) {
216236
new Handler(Looper.getMainLooper()).post(() -> onAppStartDone());
217237
}
218238
}
239+
240+
@Override
241+
public void onActivityDestroyed(@NonNull Activity activity) {
242+
final int remainingActivities = activeActivitiesCounter.decrementAndGet();
243+
// if the app is moving into background, reset firstDrawDone
244+
// as the next Activity is considered like a new app start
245+
if (remainingActivities == 0 && !activity.isChangingConfigurations()) {
246+
firstDrawDone.set(false);
247+
}
248+
}
219249
};
220250

221251
app.registerActivityLifecycleCallbacks(activityCallback);
222252
}
223253

224254
synchronized void onAppStartDone() {
225-
final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
226-
appStartMetrics.getSdkInitTimeSpan().stop();
227-
appStartMetrics.getAppStartTimeSpan().stop();
228-
229-
if (app != null) {
230-
if (activityCallback != null) {
231-
app.unregisterActivityLifecycleCallbacks(activityCallback);
232-
}
255+
if (!firstDrawDone.getAndSet(true)) {
256+
final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
257+
appStartMetrics.getSdkInitTimeSpan().stop();
258+
appStartMetrics.getAppStartTimeSpan().stop();
233259
}
234260
}
235261
}

sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,26 @@ public static void onContentProviderPostCreate(final @NotNull ContentProvider co
354354
measurement.setStoppedAt(now);
355355
}
356356
}
357+
358+
/**
359+
* @param hasBundle true if the activity onCreate had a non-null bundle
360+
* @param lastKnownStart in case the app start is too long, resets the app start timestamp to this
361+
* value
362+
*/
363+
public void updateAppStartType(final boolean hasBundle, final long lastKnownStart) {
364+
final @NotNull TimeSpan appStartSpan = getInstance().getAppStartTimeSpan();
365+
// If the app start span already started and stopped, it means the app restarted without
366+
// killing the process, so we are in a warm start
367+
// If the app has an invalid cold start, it means it was started in the background, like
368+
// via BroadcastReceiver, so we consider it a warm start
369+
if ((appStartSpan.hasStarted() && appStartSpan.hasStopped())
370+
|| (!getInstance().isColdStartValid())) {
371+
getInstance().restartAppStart(lastKnownStart);
372+
getInstance().setAppStartType(AppStartMetrics.AppStartType.WARM);
373+
} else {
374+
getInstance()
375+
.setAppStartType(
376+
hasBundle ? AppStartMetrics.AppStartType.WARM : AppStartMetrics.AppStartType.COLD);
377+
}
378+
}
357379
}

sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package io.sentry.android.core
22

3+
import android.app.Activity
34
import android.app.Application
5+
import android.app.Application.ActivityLifecycleCallbacks
46
import android.content.pm.ProviderInfo
57
import android.os.Build
8+
import android.os.Bundle
69
import androidx.test.ext.junit.runners.AndroidJUnit4
710
import io.sentry.ILogger
811
import io.sentry.JsonSerializer
@@ -26,6 +29,7 @@ import java.nio.file.Files
2629
import kotlin.test.AfterTest
2730
import kotlin.test.BeforeTest
2831
import kotlin.test.Test
32+
import kotlin.test.assertEquals
2933
import kotlin.test.assertFailsWith
3034
import kotlin.test.assertFalse
3135
import kotlin.test.assertNotNull
@@ -48,6 +52,7 @@ class SentryPerformanceProviderTest {
4852
val providerInfo = ProviderInfo()
4953
val logger = mock<ILogger>()
5054
lateinit var configFile: File
55+
var activityLifecycleCallback: ActivityLifecycleCallbacks? = null
5156

5257
fun getSut(sdkVersion: Int = Build.VERSION_CODES.S, authority: String = AUTHORITY, handleFile: ((config: File) -> Unit)? = null): SentryPerformanceProvider {
5358
val buildInfoProvider: BuildInfoProvider = mock()
@@ -56,7 +61,10 @@ class SentryPerformanceProviderTest {
5661
whenever(mockContext.applicationContext).thenReturn(mockContext)
5762
configFile = File(sentryCache, Sentry.APP_START_PROFILING_CONFIG_FILE_NAME)
5863
handleFile?.invoke(configFile)
59-
64+
whenever(mockContext.registerActivityLifecycleCallbacks(any())).then {
65+
activityLifecycleCallback = it.arguments[0] as ActivityLifecycleCallbacks
66+
return@then Unit
67+
}
6068
providerInfo.authority = authority
6169
return SentryPerformanceProvider(logger, buildInfoProvider).apply {
6270
attachInfo(mockContext, providerInfo)
@@ -232,6 +240,73 @@ class SentryPerformanceProviderTest {
232240
assertFalse(AppStartMetrics.getInstance().appStartProfiler!!.isRunning)
233241
}
234242

243+
@Test
244+
fun `Sets app launch type to cold`() {
245+
val provider = fixture.getSut()
246+
val activity = mock<Activity>()
247+
provider.onCreate()
248+
249+
assertEquals(AppStartMetrics.AppStartType.UNKNOWN, AppStartMetrics.getInstance().appStartType)
250+
251+
// when the first activity has no bundle
252+
fixture.activityLifecycleCallback!!.onActivityCreated(activity, null)
253+
254+
// then the app start is considered cold
255+
assertEquals(AppStartMetrics.AppStartType.COLD, AppStartMetrics.getInstance().appStartType)
256+
257+
// when any subsequent activity launches
258+
fixture.activityLifecycleCallback!!.onActivityCreated(activity, mock<Bundle>())
259+
260+
// then the app start is still considered cold
261+
assertEquals(AppStartMetrics.AppStartType.COLD, AppStartMetrics.getInstance().appStartType)
262+
}
263+
264+
@Test
265+
fun `Sets app launch type to warm if process init is too old`() {
266+
val provider = fixture.getSut()
267+
val activity = mock<Activity>()
268+
provider.onCreate()
269+
270+
assertEquals(AppStartMetrics.AppStartType.UNKNOWN, AppStartMetrics.getInstance().appStartType)
271+
272+
AppStartMetrics.getInstance().appStartTimeSpan.setStartedAt(
273+
AppStartMetrics.getInstance().appStartTimeSpan.startUptimeMs - 20000
274+
)
275+
276+
// when the first activity has no bundle
277+
fixture.activityLifecycleCallback!!.onActivityCreated(activity, null)
278+
279+
// then the app start is considered warm
280+
assertEquals(AppStartMetrics.AppStartType.WARM, AppStartMetrics.getInstance().appStartType)
281+
282+
// when any subsequent activity launches
283+
fixture.activityLifecycleCallback!!.onActivityCreated(activity, mock<Bundle>())
284+
285+
// then the app start is still considered warm
286+
assertEquals(AppStartMetrics.AppStartType.WARM, AppStartMetrics.getInstance().appStartType)
287+
}
288+
289+
@Test
290+
fun `Sets app launch type to warm`() {
291+
val provider = fixture.getSut()
292+
val activity = mock<Activity>()
293+
provider.onCreate()
294+
295+
assertEquals(AppStartMetrics.AppStartType.UNKNOWN, AppStartMetrics.getInstance().appStartType)
296+
297+
// when the first activity has a bundle
298+
fixture.activityLifecycleCallback!!.onActivityCreated(activity, mock<Bundle>())
299+
300+
// then the app start is considered WARM
301+
assertEquals(AppStartMetrics.AppStartType.WARM, AppStartMetrics.getInstance().appStartType)
302+
303+
// when any subsequent activity launches
304+
fixture.activityLifecycleCallback!!.onActivityCreated(activity, null)
305+
306+
// then the app start is still considered warm
307+
assertEquals(AppStartMetrics.AppStartType.WARM, AppStartMetrics.getInstance().appStartType)
308+
}
309+
235310
private fun writeConfig(
236311
configFile: File,
237312
profilingEnabled: Boolean = true,

0 commit comments

Comments
 (0)