Skip to content

Commit 12d79b2

Browse files
authored
Merge b0722d3 into e59e22a
2 parents e59e22a + b0722d3 commit 12d79b2

File tree

7 files changed

+513
-37
lines changed

7 files changed

+513
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use `options.isCollectExternalStorageContext = true` or `<meta-data android:name="io.sentry.external-storage-context" android:value="true" />`
1616
- Fix `NullPointerException` when reading ANR marker ([#4979](https://github.com/getsentry/sentry-java/pull/4979))
1717
- Report discarded log in batch processor as `log_byte` ([#4971](https://github.com/getsentry/sentry-java/pull/4971))
18+
- Fix warm app start type detection for edge cases ([#4999](https://github.com/getsentry/sentry-java/pull/4999))
1819

1920
### Improvements
2021

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

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.sentry.android.core.SentryAndroidOptions;
2222
import io.sentry.android.core.internal.util.FirstDrawDoneListener;
2323
import io.sentry.util.AutoClosableReentrantLock;
24+
import io.sentry.util.LazyEvaluator;
2425
import java.util.ArrayList;
2526
import java.util.Collections;
2627
import java.util.HashMap;
@@ -56,7 +57,15 @@ public enum AppStartType {
5657
new AutoClosableReentrantLock();
5758

5859
private @NotNull AppStartType appStartType = AppStartType.UNKNOWN;
59-
private boolean appLaunchedInForeground;
60+
private final LazyEvaluator<Boolean> appLaunchedInForeground =
61+
new LazyEvaluator<>(
62+
new LazyEvaluator.Evaluator<Boolean>() {
63+
@Override
64+
public @NotNull Boolean evaluate() {
65+
return ContextUtils.isForegroundImportance();
66+
}
67+
});
68+
private volatile long firstPostUptimeMillis = -1;
6069

6170
private final @NotNull TimeSpan appStartSpan;
6271
private final @NotNull TimeSpan sdkInitTimeSpan;
@@ -89,7 +98,6 @@ public AppStartMetrics() {
8998
applicationOnCreate = new TimeSpan();
9099
contentProviderOnCreates = new HashMap<>();
91100
activityLifecycles = new ArrayList<>();
92-
appLaunchedInForeground = ContextUtils.isForegroundImportance();
93101
}
94102

95103
/**
@@ -140,12 +148,12 @@ public void setAppStartType(final @NotNull AppStartType appStartType) {
140148
}
141149

142150
public boolean isAppLaunchedInForeground() {
143-
return appLaunchedInForeground;
151+
return appLaunchedInForeground.getValue();
144152
}
145153

146154
@VisibleForTesting
147155
public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) {
148-
this.appLaunchedInForeground = appLaunchedInForeground;
156+
this.appLaunchedInForeground.setValue(appLaunchedInForeground);
149157
}
150158

151159
/**
@@ -176,7 +184,7 @@ public void onAppStartSpansSent() {
176184
}
177185

178186
public boolean shouldSendStartMeasurements() {
179-
return shouldSendStartMeasurements && appLaunchedInForeground;
187+
return shouldSendStartMeasurements && appLaunchedInForeground.getValue();
180188
}
181189

182190
public long getClassLoadedUptimeMs() {
@@ -191,7 +199,7 @@ public long getClassLoadedUptimeMs() {
191199
final @NotNull SentryAndroidOptions options) {
192200
// If the app start type was never determined or app wasn't launched in foreground,
193201
// the app start is considered invalid
194-
if (appStartType != AppStartType.UNKNOWN && appLaunchedInForeground) {
202+
if (appStartType != AppStartType.UNKNOWN && appLaunchedInForeground.getValue()) {
195203
if (options.isEnablePerformanceV2()) {
196204
// Only started when sdk version is >= N
197205
final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan();
@@ -212,6 +220,16 @@ public long getClassLoadedUptimeMs() {
212220
return new TimeSpan();
213221
}
214222

223+
@TestOnly
224+
void setFirstPostUptimeMillis(final long firstPostUptimeMillis) {
225+
this.firstPostUptimeMillis = firstPostUptimeMillis;
226+
}
227+
228+
@TestOnly
229+
long getFirstPostUptimeMillis() {
230+
return firstPostUptimeMillis;
231+
}
232+
215233
@TestOnly
216234
public void clear() {
217235
appStartType = AppStartType.UNKNOWN;
@@ -229,11 +247,12 @@ public void clear() {
229247
}
230248
appStartContinuousProfiler = null;
231249
appStartSamplingDecision = null;
232-
appLaunchedInForeground = false;
250+
appLaunchedInForeground.setValue(false);
233251
isCallbackRegistered = false;
234252
shouldSendStartMeasurements = true;
235253
firstDrawDone.set(false);
236254
activeActivitiesCounter.set(0);
255+
firstPostUptimeMillis = -1;
237256
}
238257

239258
public @Nullable ITransactionProfiler getAppStartProfiler() {
@@ -310,13 +329,21 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
310329
return;
311330
}
312331
isCallbackRegistered = true;
313-
appLaunchedInForeground = appLaunchedInForeground || ContextUtils.isForegroundImportance();
332+
appLaunchedInForeground.resetValue();
314333
application.registerActivityLifecycleCallbacks(instance);
315334
// We post on the main thread a task to post a check on the main thread. On Pixel devices
316335
// (possibly others) the first task posted on the main thread is called before the
317336
// Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate
318337
// callback is called before the application one.
319-
new Handler(Looper.getMainLooper()).post(() -> checkCreateTimeOnMain());
338+
new Handler(Looper.getMainLooper())
339+
.post(
340+
new Runnable() {
341+
@Override
342+
public void run() {
343+
firstPostUptimeMillis = SystemClock.uptimeMillis();
344+
checkCreateTimeOnMain();
345+
}
346+
});
320347
}
321348

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

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

343370
@Override
344371
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
372+
final long activityCreatedUptimeMillis = SystemClock.uptimeMillis();
345373
CurrentActivityHolder.getInstance().setActivity(activity);
346374

347375
// the first activity determines the app start type
348376
if (activeActivitiesCounter.incrementAndGet() == 1 && !firstDrawDone.get()) {
349377
final long nowUptimeMs = SystemClock.uptimeMillis();
350378

351-
// If the app (process) was launched more than 1 minute ago, it's likely wrong
379+
// If the app (process) was launched more than 1 minute ago, consider it a warm start
352380
final long durationSinceAppStartMillis = nowUptimeMs - appStartSpan.getStartUptimeMs();
353-
if (!appLaunchedInForeground || durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) {
381+
if (!appLaunchedInForeground.getValue()
382+
|| durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) {
354383
appStartType = AppStartType.WARM;
355-
356384
shouldSendStartMeasurements = true;
357385
appStartSpan.reset();
358-
appStartSpan.start();
359-
appStartSpan.setStartedAt(nowUptimeMs);
360-
CLASS_LOADED_UPTIME_MS = nowUptimeMs;
386+
appStartSpan.setStartedAt(activityCreatedUptimeMillis);
387+
CLASS_LOADED_UPTIME_MS = activityCreatedUptimeMillis;
361388
contentProviderOnCreates.clear();
362389
applicationOnCreate.reset();
390+
} else if (savedInstanceState != null) {
391+
appStartType = AppStartType.WARM;
392+
} else if (firstPostUptimeMillis != -1
393+
&& activityCreatedUptimeMillis > firstPostUptimeMillis) {
394+
// Application creation always queues Activity creation
395+
// So if Activity is created after our first measured post, it's a warm start
396+
appStartType = AppStartType.WARM;
363397
} else {
364-
appStartType = savedInstanceState == null ? AppStartType.COLD : AppStartType.WARM;
398+
appStartType = AppStartType.COLD;
365399
}
366400
}
367-
appLaunchedInForeground = true;
401+
appLaunchedInForeground.setValue(true);
368402
}
369403

370404
@Override
@@ -403,9 +437,9 @@ public void onActivityDestroyed(@NonNull Activity activity) {
403437

404438
final int remainingActivities = activeActivitiesCounter.decrementAndGet();
405439
// if the app is moving into background
406-
// as the next Activity is considered like a new app start
440+
// as the next onActivityCreated will treat it as a new warm app start
407441
if (remainingActivities == 0 && !activity.isChangingConfigurations()) {
408-
appLaunchedInForeground = false;
442+
appLaunchedInForeground.setValue(true);
409443
shouldSendStartMeasurements = true;
410444
firstDrawDone.set(false);
411445
}

0 commit comments

Comments
 (0)