Skip to content

Commit 484b50c

Browse files
committed
Add getPreviousRunInfo
1 parent c098172 commit 484b50c

File tree

19 files changed

+402
-76
lines changed

19 files changed

+402
-76
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
**Added**
99

10-
- Nothing yet!
10+
- Added experimental `Capture.Logger.getPreviousRunInfo()` API to get detailed previous run information.
1111

1212
**Changed**
1313

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import io.bitdrift.capture.providers.DateProvider
2626
import io.bitdrift.capture.providers.FieldProvider
2727
import io.bitdrift.capture.providers.SystemDateProvider
2828
import io.bitdrift.capture.providers.session.SessionStrategy
29+
import io.bitdrift.capture.reports.exitinfo.PreviousRunInfo
2930
import io.bitdrift.capture.utils.BuildTypeChecker
3031
import okhttp3.HttpUrl
3132
import java.util.UUID
@@ -253,6 +254,14 @@ object Capture {
253254
}
254255
}
255256

257+
/**
258+
* Returns a snapshot of the previous app run status.
259+
*
260+
*/
261+
@JvmStatic
262+
@ExperimentalBitdriftApi
263+
fun getPreviousRunInfo(): PreviousRunInfo? = (logger() as? LoggerImpl)?.getPreviousRunInfo()
264+
256265
/**
257266
* Adds a field that should be attached to all logs emitted by the logger going forward.
258267
* If a field with a given key has already been registered with the logger, its value is

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ import io.bitdrift.capture.providers.toFields
5050
import io.bitdrift.capture.providers.toLegacyJniFields
5151
import io.bitdrift.capture.reports.IssueCallbackConfiguration
5252
import io.bitdrift.capture.reports.IssueReporter
53+
import io.bitdrift.capture.reports.exitinfo.LatestAppExitInfoProvider
54+
import io.bitdrift.capture.reports.exitinfo.PreviousRunInfo
55+
import io.bitdrift.capture.reports.exitinfo.PreviousRunInfoProvider
5356
import io.bitdrift.capture.reports.processor.ICompletedReportsProcessor
5457
import io.bitdrift.capture.reports.processor.IIssueReporterProcessor
5558
import io.bitdrift.capture.reports.processor.ReportProcessingSession
@@ -85,7 +88,7 @@ internal class LoggerImpl(
8588
sharedOkHttpClient: OkHttpClient = OkHttpClient(),
8689
private val apiClient: OkHttpApiClient = OkHttpApiClient(apiUrl, apiKey, client = sharedOkHttpClient),
8790
private var deviceCodeService: DeviceCodeService = DeviceCodeService(apiClient),
88-
activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager,
91+
private val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager,
8992
bridge: IBridge = CaptureJniLibrary,
9093
private val eventListenerDispatcher: CaptureDispatchers.CommonBackground = CaptureDispatchers.CommonBackground,
9194
windowManager: IWindowManager = WindowManager(errorHandler),
@@ -670,6 +673,12 @@ internal class LoggerImpl(
670673

671674
internal fun getIssueProcessor(): IIssueReporterProcessor? = issueReporter?.getIssueReporterProcessor()
672675

676+
internal fun getPreviousRunInfo(): PreviousRunInfo? =
677+
PreviousRunInfoProvider.get(
678+
activityManager,
679+
LatestAppExitInfoProvider,
680+
)
681+
673682
private fun startDebugOperationsAsNeeded(context: Context) {
674683
if (!BuildTypeChecker.isDebuggable(context)) {
675684
return

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/events/lifecycle/AppExitLogger.kt

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import io.bitdrift.capture.reports.IssueReporterState
2929
import io.bitdrift.capture.reports.exitinfo.ILatestAppExitInfoProvider
3030
import io.bitdrift.capture.reports.exitinfo.LatestAppExitInfoProvider
3131
import io.bitdrift.capture.reports.exitinfo.LatestAppExitReasonResult
32+
import io.bitdrift.capture.reports.exitinfo.toExitReason
3233
import io.bitdrift.capture.reports.jvmcrash.CaptureUncaughtExceptionHandler
3334
import io.bitdrift.capture.reports.jvmcrash.ICaptureUncaughtExceptionHandler
3435
import io.bitdrift.capture.reports.jvmcrash.IJvmCrashListener
@@ -170,7 +171,7 @@ internal class AppExitLogger(
170171
return fieldsOf(
171172
APP_EXIT_SOURCE_KEY to "ApplicationExitInfo",
172173
APP_EXIT_PROCESS_NAME_KEY to this.processName,
173-
APP_EXIT_REASON_KEY to this.reason.toReasonText(),
174+
APP_EXIT_REASON_KEY to this.reason.toExitReason().value,
174175
APP_EXIT_IMPORTANCE_KEY to this.importance.toImportanceText(),
175176
APP_EXIT_STATUS_KEY to this.status.toString(),
176177
APP_EXIT_PSS_KEY to this.pss.toString(),
@@ -179,25 +180,6 @@ internal class AppExitLogger(
179180
)
180181
}
181182

182-
private fun Int.toReasonText(): String =
183-
when (this) {
184-
ApplicationExitInfo.REASON_EXIT_SELF -> "EXIT_SELF"
185-
ApplicationExitInfo.REASON_SIGNALED -> "SIGNALED"
186-
ApplicationExitInfo.REASON_LOW_MEMORY -> "LOW_MEMORY"
187-
ApplicationExitInfo.REASON_CRASH -> "CRASH"
188-
ApplicationExitInfo.REASON_CRASH_NATIVE -> "CRASH_NATIVE"
189-
ApplicationExitInfo.REASON_ANR -> "ANR"
190-
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> "INITIALIZATION_FAILURE"
191-
ApplicationExitInfo.REASON_PERMISSION_CHANGE -> "PERMISSION_CHANGE"
192-
ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> "EXCESSIVE_RESOURCE_USAGE"
193-
ApplicationExitInfo.REASON_USER_REQUESTED -> "USER_REQUESTED"
194-
ApplicationExitInfo.REASON_USER_STOPPED -> "USER_STOPPED"
195-
ApplicationExitInfo.REASON_DEPENDENCY_DIED -> "DEPENDENCY_DIED"
196-
ApplicationExitInfo.REASON_OTHER -> "OTHER"
197-
ApplicationExitInfo.REASON_FREEZER -> "FREEZER"
198-
else -> "UNKNOWN"
199-
}
200-
201183
private fun Int.toImportanceText(): String =
202184
when (this) {
203185
RunningAppProcessInfo.IMPORTANCE_CACHED -> "CACHED"
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
package io.bitdrift.capture.reports.exitinfo
9+
10+
import android.app.ApplicationExitInfo
11+
12+
/**
13+
* Application termination reason values.
14+
*/
15+
enum class ExitReason(
16+
/** Stable text representation safe for serialization/logging. */
17+
val value: String,
18+
) {
19+
/** App exited itself. */
20+
ExitSelf("EXIT_SELF"),
21+
22+
/** App was terminated by a signal. */
23+
Signaled("SIGNALED"),
24+
25+
/** App exited due to low memory. */
26+
LowMemory("LOW_MEMORY"),
27+
28+
/** App exited due to a JVM crash. */
29+
JvmCrash("CRASH"),
30+
31+
/** App exited due to a native crash. */
32+
NativeCrash("CRASH_NATIVE"),
33+
34+
/** App exited due to ANR. */
35+
AppNotResponding("ANR"),
36+
37+
/** App exited due to initialization failure. */
38+
InitializationFailure("INITIALIZATION_FAILURE"),
39+
40+
/** App exited due to permission changes. */
41+
PermissionChange("PERMISSION_CHANGE"),
42+
43+
/** App exited due to excessive resource usage. */
44+
ExcessiveResourceUsage("EXCESSIVE_RESOURCE_USAGE"),
45+
46+
/** App exited due to a user request. */
47+
UserRequested("USER_REQUESTED"),
48+
49+
/** App was stopped by the user. */
50+
UserStopped("USER_STOPPED"),
51+
52+
/** App exited because a dependency died. */
53+
DependencyDied("DEPENDENCY_DIED"),
54+
55+
/** Other OS exit reason. */
56+
Other("OTHER"),
57+
58+
/** App was frozen by the OS. */
59+
Freezer("FREEZER"),
60+
61+
/** Unknown or unsupported reason. */
62+
Unknown("UNKNOWN"),
63+
}
64+
65+
internal fun Int.toExitReason(): ExitReason =
66+
when (this) {
67+
ApplicationExitInfo.REASON_EXIT_SELF -> ExitReason.ExitSelf
68+
ApplicationExitInfo.REASON_SIGNALED -> ExitReason.Signaled
69+
ApplicationExitInfo.REASON_LOW_MEMORY -> ExitReason.LowMemory
70+
ApplicationExitInfo.REASON_CRASH -> ExitReason.JvmCrash
71+
ApplicationExitInfo.REASON_CRASH_NATIVE -> ExitReason.NativeCrash
72+
ApplicationExitInfo.REASON_ANR -> ExitReason.AppNotResponding
73+
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> ExitReason.InitializationFailure
74+
ApplicationExitInfo.REASON_PERMISSION_CHANGE -> ExitReason.PermissionChange
75+
ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> ExitReason.ExcessiveResourceUsage
76+
ApplicationExitInfo.REASON_USER_REQUESTED -> ExitReason.UserRequested
77+
ApplicationExitInfo.REASON_USER_STOPPED -> ExitReason.UserStopped
78+
ApplicationExitInfo.REASON_DEPENDENCY_DIED -> ExitReason.DependencyDied
79+
ApplicationExitInfo.REASON_OTHER -> ExitReason.Other
80+
ApplicationExitInfo.REASON_FREEZER -> ExitReason.Freezer
81+
else -> ExitReason.Unknown
82+
}

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/exitinfo/ILatestAppExitInfoProvider.kt

Lines changed: 0 additions & 54 deletions
This file was deleted.

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/exitinfo/LatestAppExitInfoProvider.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,43 @@ internal object LatestAppExitInfoProvider : ILatestAppExitInfoProvider {
5454
else -> null
5555
}
5656
}
57+
58+
/**
59+
* Retrieves the latest [ApplicationExitInfo] if available.
60+
*/
61+
fun interface ILatestAppExitInfoProvider {
62+
/**
63+
* Returns the latest [ApplicationExitInfo] when present.
64+
*/
65+
@RequiresApi(Build.VERSION_CODES.R)
66+
fun get(activityManager: ActivityManager): LatestAppExitReasonResult
67+
}
68+
69+
/**
70+
* The [ApplicationExitInfo] lookup result.
71+
*/
72+
sealed class LatestAppExitReasonResult {
73+
/**
74+
* Returns the latest [ApplicationExitInfo] when available.
75+
*/
76+
data class Valid(
77+
/** Latest [ApplicationExitInfo] for the current process. */
78+
val applicationExitInfo: ApplicationExitInfo,
79+
) : LatestAppExitReasonResult()
80+
81+
/**
82+
* No [ApplicationExitInfo] was available.
83+
* (e.g. this is expected on first app installation)
84+
*/
85+
data object None : LatestAppExitReasonResult()
86+
87+
/**
88+
* Returns the detailed error while trying to determine prior reasons.
89+
*/
90+
data class Error(
91+
/** Human-readable message describing the lookup failure. */
92+
val message: String,
93+
/** Optional throwable associated with the lookup failure. */
94+
val throwable: Throwable? = null,
95+
) : LatestAppExitReasonResult()
96+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
package io.bitdrift.capture.reports.exitinfo
9+
10+
import android.app.ActivityManager
11+
import android.app.ApplicationExitInfo
12+
import android.os.Build
13+
import androidx.annotation.RequiresApi
14+
15+
/**
16+
* Gets the [PreviousRunInfo] from Android's [LatestAppExitReasonResult].
17+
*/
18+
internal object PreviousRunInfoProvider : IPreviousRunInfoProvider {
19+
/**
20+
* Returns previous run status derived from [android.app.ApplicationExitInfo].
21+
*/
22+
override fun get(
23+
activityManager: ActivityManager,
24+
latestAppExitInfoProvider: ILatestAppExitInfoProvider,
25+
): PreviousRunInfo? {
26+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
27+
// TODO: Fran will use a separate signal (e.g. persisted file created via UncaughtCrashHandler) to determine prior run info
28+
return null
29+
} else {
30+
return when (val lastReasonResult = latestAppExitInfoProvider.get(activityManager)) {
31+
is LatestAppExitReasonResult.Valid -> {
32+
val reason = lastReasonResult.applicationExitInfo.reason.toExitReason()
33+
val hasFatallyTerminated =
34+
when (reason) {
35+
ExitReason.JvmCrash,
36+
ExitReason.NativeCrash,
37+
ExitReason.AppNotResponding,
38+
-> true
39+
40+
else -> false
41+
}
42+
PreviousRunInfo(hasFatallyTerminated = hasFatallyTerminated, reason = reason)
43+
}
44+
45+
is LatestAppExitReasonResult.None -> PreviousRunInfo(hasFatallyTerminated = false)
46+
is LatestAppExitReasonResult.Error -> null
47+
}
48+
}
49+
}
50+
}
51+
52+
/**
53+
* Snapshot of the previous app run status.
54+
*
55+
* @property hasFatallyTerminated Whether the previous run ended in a fatal termination (for example,
56+
* crash or ANR).
57+
* @property reason Platform exit reason enum when available.
58+
*/
59+
data class PreviousRunInfo(
60+
val hasFatallyTerminated: Boolean,
61+
val reason: ExitReason? = null,
62+
)
63+
64+
/**
65+
* Contract for producing [PreviousRunInfo] from app exit signals.
66+
*/
67+
interface IPreviousRunInfoProvider {
68+
/**
69+
* Returns previous run status, or `null` when lookup fails.
70+
*/
71+
@RequiresApi(Build.VERSION_CODES.R)
72+
fun get(
73+
activityManager: ActivityManager,
74+
latestAppExitInfoProvider: ILatestAppExitInfoProvider,
75+
): PreviousRunInfo?
76+
}

0 commit comments

Comments
 (0)