Skip to content

Commit 851f461

Browse files
authored
Add getPreviousRunInfo (#922)
* [Android] Add manufacturer as OOTB field (#920) * Updating android tests * Update docs * Test name * Fix edge case for OS 10 and initial app installation * Update case for OS 10 and initial start call * Prevent a null Capture.logger instance inside onBeforeReportSend callback * Allow to compare bugsnag duration * Update Capture.kt * Cache AppExitReason result to prevent unnecessary IPC binder calls * Remove noisy benchmark tests on emulator * copilot review * Copilot feedback part 2 * Rename previousRunInfo.reason to previousRunInfo.terminationReason * Remove file sentinel logic for below OS 11 * Test fixes
1 parent c098172 commit 851f461

File tree

28 files changed

+703
-123
lines changed

28 files changed

+703
-123
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 API to get detailed previous run information (`Capture.Logger.getPreviousRunInfo()` on Android, `Capture.Logger.previousRunInfo` on iOS/Swift, `previousRunInfo()` in Objective-C).
1111

1212
**Changed**
1313

Cargo.Bazel.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/android/HelloWorldApp.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import android.app.Application
1111
import android.os.Handler
1212
import android.os.Looper
1313
import android.util.Log
14+
import io.bitdrift.capture.Capture
1415
import io.bitdrift.capture.Capture.Logger
1516
import io.bitdrift.capture.Configuration
17+
import io.bitdrift.capture.experimental.ExperimentalBitdriftApi
1618
import io.bitdrift.capture.providers.FieldProvider
1719
import io.bitdrift.capture.providers.session.SessionStrategy
1820
import io.bitdrift.capture.reports.IssueCallbackConfiguration
@@ -69,6 +71,20 @@ class HelloWorldApp : Application() {
6971

7072
Log.v("HelloWorldApp", "Android Bitdrift app launched with session url=${Logger.sessionUrl}")
7173

74+
@OptIn(ExperimentalBitdriftApi::class)
75+
Capture.Logger.getPreviousRunInfo()?.let { previousRunInfo ->
76+
val hasFatallyTerminated = previousRunInfo.hasFatallyTerminated.toString()
77+
val reason = previousRunInfo.terminationReason?.toString() ?: ""
78+
Capture.Logger.logInfo(
79+
mapOf(
80+
"hasFatallyTerminated" to hasFatallyTerminated,
81+
"reason" to reason,
82+
),
83+
) {
84+
"Bitdrift PreviousRunInfo"
85+
}
86+
}
87+
7288
Handler(Looper.getMainLooper()).postDelayed(
7389
{
7490
Log.i("HelloWorldApp", getCaptureSdkInitializedMessage())

examples/swift/hello_world/LoggerCustomer.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ final class LoggerCustomer: NSObject, URLSessionDelegate {
115115
Logger.registerOpaqueUserID(kOpaqueUserID)
116116
Logger.logInfo("App launched. Logger configured.")
117117

118+
if let previousRunInfo = Capture.Logger.previousRunInfo {
119+
Capture.Logger.logInfo(
120+
"Bitdrift PreviousRunInfo",
121+
fields: [
122+
"hasFatallyTerminated": String(previousRunInfo.hasFatallyTerminated),
123+
]
124+
)
125+
}
126+
118127
MXMetricManager.shared.add(self)
119128
}
120129

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

Lines changed: 20 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,21 @@ object Capture {
253254
}
254255
}
255256

257+
/**
258+
* Returns a snapshot of the previous app run status, or `null` if not available.
259+
*
260+
* Must be called after [start]. Returns `null` if the logger is not initialized.
261+
*
262+
* Available on Android API 30+ (OS 11+). On API < 30, this returns `null` at the moment.
263+
*
264+
* Note: on API 30, native crashes are reported as a fatal termination reason but do not
265+
* trigger an `onBeforeSend` callback with the crash report. The `onBeforeSend` callback
266+
* for native crashes is only available on API >= 31.
267+
*/
268+
@JvmStatic
269+
@ExperimentalBitdriftApi
270+
fun getPreviousRunInfo(): PreviousRunInfo? = (logger() as? LoggerImpl)?.getPreviousRunInfo()
271+
256272
/**
257273
* Adds a field that should be attached to all logs emitted by the logger going forward.
258274
* If a field with a given key has already been registered with the logger, its value is
@@ -675,6 +691,10 @@ object Capture {
675691

676692
default.set(LoggerState.Started(loggerImpl))
677693

694+
// Must be initialized right after the logger state is set to avoid a null
695+
// Capture.logger() reference when onBeforeSend callbacks are triggered.
696+
loggerImpl.initIssueReporter()
697+
678698
val sdkConfiguredDuration =
679699
SdkConfiguredDuration(
680700
wholeStartDuration = startSdkTimer.elapsedNow(),

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

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ 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.PreviousRunInfo
54+
import io.bitdrift.capture.reports.exitinfo.PreviousRunInfoResolver
5355
import io.bitdrift.capture.reports.processor.ICompletedReportsProcessor
5456
import io.bitdrift.capture.reports.processor.IIssueReporterProcessor
5557
import io.bitdrift.capture.reports.processor.ReportProcessingSession
@@ -85,7 +87,7 @@ internal class LoggerImpl(
8587
sharedOkHttpClient: OkHttpClient = OkHttpClient(),
8688
private val apiClient: OkHttpApiClient = OkHttpApiClient(apiUrl, apiKey, client = sharedOkHttpClient),
8789
private var deviceCodeService: DeviceCodeService = DeviceCodeService(apiClient),
88-
activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager,
90+
private val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager,
8991
bridge: IBridge = CaptureJniLibrary,
9092
private val eventListenerDispatcher: CaptureDispatchers.CommonBackground = CaptureDispatchers.CommonBackground,
9193
windowManager: IWindowManager = WindowManager(errorHandler),
@@ -95,6 +97,7 @@ internal class LoggerImpl(
9597
internal val webViewConfiguration: WebViewConfiguration? = configuration.webViewConfiguration
9698

9799
private val metadataProvider: MetadataProvider
100+
private val sdkDirectory: String
98101
private val batteryMonitor = BatteryMonitor(context)
99102
private val powerMonitor = PowerMonitor(context)
100103
private val diskUsageMonitor: DiskUsageMonitor
@@ -112,9 +115,14 @@ internal class LoggerImpl(
112115

113116
private val sessionReplayTarget: ISessionReplayTarget
114117

118+
private val previousRunInfoResolver = PreviousRunInfoResolver(activityManager)
119+
115120
private val issueReporter: IssueReporter? =
116121
if (configuration.enableFatalIssueReporting) {
117-
IssueReporter(internalLogger = this, dateProvider = dateProvider)
122+
IssueReporter(
123+
internalLogger = this,
124+
dateProvider = dateProvider,
125+
)
118126
} else {
119127
null
120128
}
@@ -153,7 +161,7 @@ internal class LoggerImpl(
153161
okHttpClient = sharedOkHttpClient,
154162
)
155163

156-
val sdkDirectory = SdkDirectory.getPath(context)
164+
sdkDirectory = SdkDirectory.getPath(context)
157165

158166
val localErrorReporter =
159167
errorReporter ?: ErrorReporterService(
@@ -274,14 +282,6 @@ internal class LoggerImpl(
274282

275283
CaptureJniLibrary.startLogger(this.loggerId)
276284

277-
// issue reporter needs to be initialized after appExitLogger and the jniLogger
278-
issueReporter?.init(
279-
activityManager = activityManager,
280-
sdkDirectory = sdkDirectory,
281-
clientAttributes = clientAttributes,
282-
completedReportsProcessor = this,
283-
)
284-
285285
startDebugOperationsAsNeeded(context)
286286
}
287287

@@ -670,6 +670,21 @@ internal class LoggerImpl(
670670

671671
internal fun getIssueProcessor(): IIssueReporterProcessor? = issueReporter?.getIssueReporterProcessor()
672672

673+
internal fun getPreviousRunInfo(): PreviousRunInfo? = previousRunInfoResolver.get()
674+
675+
/**
676+
* Initializes the issue reporter. Must be called immediately right after the LoggerImpl is created
677+
* so we can guarantee that any calls to Capture.Logger.* are valid within `onBeforeReportSend`
678+
*/
679+
internal fun initIssueReporter() {
680+
issueReporter?.init(
681+
activityManager = activityManager,
682+
sdkDirectory = sdkDirectory,
683+
clientAttributes = clientAttributes,
684+
completedReportsProcessor = this,
685+
)
686+
}
687+
673688
private fun startDebugOperationsAsNeeded(context: Context) {
674689
if (!BuildTypeChecker.isDebuggable(context)) {
675690
return
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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("JVM_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+
66+
/**
67+
* Lookup helpers for [ExitReason].
68+
*/
69+
companion object {
70+
/**
71+
* Returns the enum value matching a stable string representation.
72+
*/
73+
fun fromValue(value: String): ExitReason? = entries.firstOrNull { it.value == value }
74+
}
75+
}
76+
77+
internal fun Int.toExitReason(): ExitReason =
78+
when (this) {
79+
ApplicationExitInfo.REASON_EXIT_SELF -> ExitReason.ExitSelf
80+
ApplicationExitInfo.REASON_SIGNALED -> ExitReason.Signaled
81+
ApplicationExitInfo.REASON_LOW_MEMORY -> ExitReason.LowMemory
82+
ApplicationExitInfo.REASON_CRASH -> ExitReason.JvmCrash
83+
ApplicationExitInfo.REASON_CRASH_NATIVE -> ExitReason.NativeCrash
84+
ApplicationExitInfo.REASON_ANR -> ExitReason.AppNotResponding
85+
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> ExitReason.InitializationFailure
86+
ApplicationExitInfo.REASON_PERMISSION_CHANGE -> ExitReason.PermissionChange
87+
ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> ExitReason.ExcessiveResourceUsage
88+
ApplicationExitInfo.REASON_USER_REQUESTED -> ExitReason.UserRequested
89+
ApplicationExitInfo.REASON_USER_STOPPED -> ExitReason.UserStopped
90+
ApplicationExitInfo.REASON_DEPENDENCY_DIED -> ExitReason.DependencyDied
91+
ApplicationExitInfo.REASON_OTHER -> ExitReason.Other
92+
ApplicationExitInfo.REASON_FREEZER -> ExitReason.Freezer
93+
else -> ExitReason.Unknown
94+
}

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

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

0 commit comments

Comments
 (0)