Skip to content

Commit

Permalink
Merge pull request #1159 from bugsnag/PLAT-5974/launch-crash-interface
Browse files Browse the repository at this point in the history
Add public API for crash-on-launch detection
  • Loading branch information
fractalwrench authored Feb 24, 2021
2 parents 0f2ae06 + aae54c2 commit f41e0cf
Show file tree
Hide file tree
Showing 27 changed files with 187 additions and 19 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
### Deprecations

* `Configuration#launchCrashThresholdMs` is deprecated in favour of `Configuration#launchDurationMillis`

### Enhancements

* Add public API for crash-on-launch detection
[#1157](https://github.com/bugsnag/bugsnag-android/pull/1157)
[#1159](https://github.com/bugsnag/bugsnag-android/pull/1159)

## 5.7.0 (2021-02-18)

Expand Down
6 changes: 2 additions & 4 deletions bugsnag-android-core/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>LongParameterList:App.kt$App$( /** * The architecture of the running application binary */ var binaryArch: String?, /** * The package name of the application */ var id: String?, /** * The release stage set in [Configuration.releaseStage] */ var releaseStage: String?, /** * The version of the application set in [Configuration.version] */ var version: String?, /** The revision ID from the manifest (React Native apps only) */ var codeBundleId: String?, /** * The unique identifier for the build of the application set in [Configuration.buildUuid] */ var buildUuid: String?, /** * The application type set in [Configuration#version] */ var type: String?, /** * The version code of the application set in [Configuration.versionCode] */ var versionCode: Number? )</ID>
<ID>LongParameterList:AppWithState.kt$AppWithState$( binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, buildUuid: String?, type: String?, versionCode: Number?, /** * The number of milliseconds the application was running before the event occurred */ var duration: Number?, /** * The number of milliseconds the application was running in the foreground before the * event occurred */ var durationInForeground: Number?, /** * Whether the application was in the foreground when the event occurred */ var inForeground: Boolean? )</ID>
<ID>LongParameterList:AppWithState.kt$AppWithState$( config: ImmutableConfig, binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, duration: Number?, durationInForeground: Number?, inForeground: Boolean? )</ID>
<ID>LongParameterList:AppWithState.kt$AppWithState$( binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, buildUuid: String?, type: String?, versionCode: Number?, /** * The number of milliseconds the application was running before the event occurred */ var duration: Number?, /** * The number of milliseconds the application was running in the foreground before the * event occurred */ var durationInForeground: Number?, /** * Whether the application was in the foreground when the event occurred */ var inForeground: Boolean?, /** * Whether the application was launching when the event occurred */ var isLaunching: Boolean? )</ID>
<ID>LongParameterList:AppWithState.kt$AppWithState$( config: ImmutableConfig, binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, duration: Number?, durationInForeground: Number?, inForeground: Boolean?, isLaunching: Boolean? )</ID>
<ID>LongParameterList:Device.kt$Device$( buildInfo: DeviceBuildInfo, /** * The Application Binary Interface used */ var cpuAbi: Array&lt;String&gt;?, /** * Whether the device has been jailbroken */ var jailbroken: Boolean?, /** * A UUID generated by Bugsnag and used for the individual application on a device */ var id: String?, /** * The IETF language tag of the locale used */ var locale: String?, /** * The total number of bytes of memory on the device */ var totalMemory: Long?, /** * A collection of names and their versions of the primary languages, frameworks or * runtimes that the application is running on */ var runtimeVersions: MutableMap&lt;String, Any&gt;? )</ID>
<ID>LongParameterList:DeviceBuildInfo.kt$DeviceBuildInfo$( val manufacturer: String?, val model: String?, val osVersion: String?, val apiLevel: Int?, val osBuild: String?, val fingerprint: String?, val tags: String?, val brand: String?, val cpuAbis: Array&lt;String&gt;? )</ID>
<ID>LongParameterList:DeviceDataCollector.kt$DeviceDataCollector$( private val connectivity: Connectivity, private val appContext: Context, private val resources: Resources?, private val deviceId: String?, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, private val logger: Logger )</ID>
<ID>LongParameterList:DeviceWithState.kt$DeviceWithState$( buildInfo: DeviceBuildInfo, jailbroken: Boolean?, id: String?, locale: String?, totalMemory: Long?, runtimeVersions: MutableMap&lt;String, Any&gt;, /** * The number of free bytes of storage available on the device */ var freeDisk: Long?, /** * The number of free bytes of memory available on the device */ var freeMemory: Long?, /** * The orientation of the device when the event occurred: either portrait or landscape */ var orientation: String?, /** * The timestamp on the device when the event occurred */ var time: Date? )</ID>
<ID>LongParameterList:NativeStackframe.kt$NativeStackframe$( /** * The name of the method that was being executed */ var method: String?, /** * The location of the source file */ var file: String?, /** * The line number within the source file this stackframe refers to */ var lineNumber: Number?, /** * The address of the instruction where the event occurred. */ var frameAddress: Long?, /** * The address of the function where the event occurred. */ var symbolAddress: Long?, /** * The address of the library where the event occurred. */ var loadAddress: Long?, /** * The type of the error */ var type: ErrorType = ErrorType.C )</ID>
<ID>LongParameterList:ThreadState.kt$ThreadState$( exc: Throwable?, isUnhandled: Boolean, sendThreads: ThreadSendPolicy, projectPackages: Collection&lt;String&gt;, logger: Logger, currentThread: java.lang.Thread = java.lang.Thread.currentThread(), stackTraces: MutableMap&lt;java.lang.Thread, Array&lt;StackTraceElement&gt;&gt; = java.lang.Thread.getAllStackTraces() )</ID>
<ID>LongParameterList:ThreadState.kt$ThreadState$( stackTraces: MutableMap&lt;java.lang.Thread, Array&lt;StackTraceElement&gt;&gt;, currentThread: java.lang.Thread, exc: Throwable?, isUnhandled: Boolean, projectPackages: Collection&lt;String&gt;, logger: Logger )</ID>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$299</ID>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$429</ID>
<ID>MagicNumber:DefaultDelivery.kt$DefaultDelivery$499</ID>
<ID>MatchingDeclarationName:Breadcrumb.kt$BreadcrumbInternal : Streamable</ID>
<ID>ProtectedMemberInFinalClass:ConfigInternal.kt$ConfigInternal$protected val plugins = mutableSetOf&lt;Plugin&gt;()</ID>
<ID>ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun isAnr(event: Event): Boolean</ID>
<ID>ProtectedMemberInFinalClass:EventInternal.kt$EventInternal$protected fun shouldDiscardClass(): Boolean</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public DeliveryStatus deliver(@NonNull Session payload,

public static AppWithState generateAppWithState() {
return new AppWithState(generateImmutableConfig(), null, null, null,
null, null, null, null, null);
null, null, null, null, null, null);
}

public static App generateApp() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class ManifestConfigLoaderTest {
assertEquals(maxBreadcrumbs, 25)
assertEquals(maxPersistedEvents, 32)
assertEquals(maxPersistedSessions, 128)
assertTrue(sendLaunchCrashesSynchronously)
@Suppress("DEPRECATION")
assertEquals(launchCrashThresholdMs, 5000)
assertEquals(launchDurationMillis, 5000)
Expand Down Expand Up @@ -86,6 +87,7 @@ class ManifestConfigLoaderTest {
putInt("com.bugsnag.android.MAX_PERSISTED_EVENTS", 52)
putInt("com.bugsnag.android.MAX_PERSISTED_SESSIONS", 64)
putInt("com.bugsnag.android.LAUNCH_DURATION_MILLIS", 7000)
putBoolean("com.bugsnag.android.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY", false)
putString("com.bugsnag.android.APP_TYPE", "react-native")
putString("com.bugsnag.android.CODE_BUNDLE_ID", "123")
}
Expand Down Expand Up @@ -121,6 +123,7 @@ class ManifestConfigLoaderTest {
@Suppress("DEPRECATION")
assertEquals(launchCrashThresholdMs, 7000)
assertEquals(launchDurationMillis, 7000)
assertFalse(sendLaunchCrashesSynchronously)
assertEquals("react-native", appType)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ internal class AppDataCollector(

fun generateAppWithState(): AppWithState = AppWithState(
config, binaryArch, packageName, releaseStage, versionName, codeBundleId,
getDurationMs(), calculateDurationInForeground(), sessionTracker.isInForeground
getDurationMs(), calculateDurationInForeground(), sessionTracker.isInForeground,
false
)

fun getAppDataMetadata(): MutableMap<String, Any?> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ class AppWithState(
/**
* Whether the application was in the foreground when the event occurred
*/
var inForeground: Boolean?
var inForeground: Boolean?,

/**
* Whether the application was launching when the event occurred
*/
var isLaunching: Boolean?
) : App(binaryArch, id, releaseStage, version, codeBundleId, buildUuid, type, versionCode) {

internal constructor(
Expand All @@ -40,7 +45,8 @@ class AppWithState(
codeBundleId: String?,
duration: Number?,
durationInForeground: Number?,
inForeground: Boolean?
inForeground: Boolean?,
isLaunching: Boolean?
) : this(
binaryArch,
id,
Expand All @@ -52,13 +58,15 @@ class AppWithState(
config.versionCode,
duration,
durationInForeground,
inForeground
inForeground,
isLaunching
)

override fun serialiseFields(writer: JsonStream) {
super.serialiseFields(writer)
writer.name("duration").value(duration)
writer.name("durationInForeground").value(durationInForeground)
writer.name("inForeground").value(inForeground)
writer.name("isLaunching").value(inForeground)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,31 @@ public static List<Breadcrumb> getBreadcrumbs() {
return getClient().getBreadcrumbs();
}

/**
* Retrieves information about the last launch of the application, if it has been run before.
*
* For example, this allows checking whether the app crashed on its last launch, which could
* be used to perform conditional behaviour to recover from crashes, such as clearing the
* app data cache.
*/
@Nullable
public static LastRunInfo getLastRunInfo() {
return getClient().getLastRunInfo();
}

/**
* Informs Bugsnag that the application has finished launching. Once this has been called
* {@link AppWithState#isLaunching()} will always be false in any new error reports,
* and synchronous delivery will not be attempted on the next launch for any fatal crashes.
*
* By default this method will be called after Bugsnag is initialized when
* {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually
* has precedence over the value supplied via the launchDurationMillis configuration option.
*/
public static void markLaunchCompleted() {
getClient().markLaunchCompleted();
}

/**
* Get the current Bugsnag Client instance.
*/
Expand Down
25 changes: 25 additions & 0 deletions bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,31 @@ void leaveAutoBreadcrumb(@NonNull String message,
}
}

/**
* Retrieves information about the last launch of the application, if it has been run before.
*
* For example, this allows checking whether the app crashed on its last launch, which could
* be used to perform conditional behaviour to recover from crashes, such as clearing the
* app data cache.
*/
@Nullable
public LastRunInfo getLastRunInfo() {
return null;
}

/**
* Informs Bugsnag that the application has finished launching. Once this has been called
* {@link AppWithState#isLaunching()} will always be false in any new error reports,
* and synchronous delivery will not be attempted on the next launch for any fatal crashes.
*
* By default this method will be called after Bugsnag is initialized when
* {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually
* has precedence over the value supplied via the launchDurationMillis configuration option.
*/
public void markLaunchCompleted() {

}

SessionTracker getSessionTracker() {
return sessionTracker;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware
var launchDurationMillis: Long = DEFAULT_LAUNCH_CRASH_THRESHOLD_MS

var autoTrackSessions: Boolean = true
var sendLaunchCrashesSynchronously: Boolean = true
var enabledErrorTypes: ErrorTypes = ErrorTypes()
var autoDetectErrors: Boolean = true
var appType: String? = "android"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ public long getLaunchCrashThresholdMs() {
}

/**
* Deprecated. Use {@link #setLaunchDurationMillis(long)} ()} instead.
* Deprecated. Use {@link #setLaunchDurationMillis(long)} instead.
*/
@Deprecated
public void setLaunchCrashThresholdMs(long launchCrashThresholdMs) {
Expand All @@ -250,6 +250,26 @@ public void setLaunchCrashThresholdMs(long launchCrashThresholdMs) {
setLaunchDurationMillis(launchCrashThresholdMs);
}

/**
* Sets whether or not Bugsnag should send crashes synchronously that occurred during
* the application's launch period. By default this behavior is enabled.
*
* See {@link #setLaunchDurationMillis(long)}
*/
public boolean getSendLaunchCrashesSynchronously() {
return impl.getSendLaunchCrashesSynchronously();
}

/**
* Sets whether or not Bugsnag should send crashes synchronously that occurred during
* the application's launch period. By default this behavior is enabled.
*
* See {@link #setLaunchDurationMillis(long)}
*/
public void setSendLaunchCrashesSynchronously(boolean sendLaunchCrashesSynchronously) {
impl.setSendLaunchCrashesSynchronously(sendLaunchCrashesSynchronously);
}

/**
* Sets the threshold in milliseconds for an uncaught error to be considered as a crash on
* launch. If a crash is detected on launch, Bugsnag will attempt to send the most recent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ internal data class ImmutableConfig(
val maxBreadcrumbs: Int,
val maxPersistedEvents: Int,
val maxPersistedSessions: Int,
val persistenceDirectory: File
val persistenceDirectory: File,
val sendLaunchCrashesSynchronously: Boolean
) {

/**
Expand Down Expand Up @@ -85,7 +86,8 @@ internal fun convertToImmutableConfig(
maxPersistedEvents = config.maxPersistedEvents,
maxPersistedSessions = config.maxPersistedSessions,
enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(),
persistenceDirectory = config.persistenceDirectory!!
persistenceDirectory = config.persistenceDirectory!!,
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.bugsnag.android

/**
* Provides information about the last launch of the application, if there was one.
*/
class LastRunInfo(

/**
* The number times the app has consecutively crashed during its launch period.
*/
val consecutiveLaunchCrashes: Int,

/**
* Whether the last app run ended with a crash, or was abnormally terminated by the system.
*/
val crashed: Boolean,

/**
* True if the previous app run ended with a crash during its launch period.
*/
val crashedDuringLaunch: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal class ManifestConfigLoader {
private const val MAX_PERSISTED_SESSIONS = "$BUGSNAG_NS.MAX_PERSISTED_SESSIONS"
private const val LAUNCH_CRASH_THRESHOLD_MS = "$BUGSNAG_NS.LAUNCH_CRASH_THRESHOLD_MS"
private const val LAUNCH_DURATION_MILLIS = "$BUGSNAG_NS.LAUNCH_DURATION_MILLIS"
private const val SEND_LAUNCH_CRASHES_SYNCHRONOUSLY = "$BUGSNAG_NS.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY"
private const val APP_TYPE = "$BUGSNAG_NS.APP_TYPE"
}

Expand Down Expand Up @@ -84,6 +85,10 @@ internal class ManifestConfigLoader {
LAUNCH_DURATION_MILLIS,
launchDurationMillis.toInt()
).toLong()
sendLaunchCrashesSynchronously = data.getBoolean(
SEND_LAUNCH_CRASHES_SYNCHRONOUSLY,
sendLaunchCrashesSynchronously
)
}
}
return config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public static Map<String,Object> getApp() {
data.put("durationInForeground", app.getDurationInForeground());
data.put("versionCode", app.getVersionCode());
data.put("inForeground", app.getInForeground());
data.put("isLaunching", app.isLaunching());
data.put("binaryArch", app.getBinaryArch());
data.putAll(source.getAppDataMetadata());
return data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,16 @@ class BugsnagApiTest {
Bugsnag.client = null
Bugsnag.getClient()
}

@Test
fun lastRunInfo() {
Bugsnag.getLastRunInfo()
verify(client, times(1)).lastRunInfo
}

@Test
fun markLaunchCompleted() {
Bugsnag.markLaunchCompleted()
verify(client, times(1)).markLaunchCompleted()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public DeliveryStatus deliver(@NotNull Session payload,

public static AppWithState generateAppWithState() {
return new AppWithState(generateImmutableConfig(), null, null, null,
null, null, null, null, null);
null, null, null, null, null, null);
}

public static App generateApp() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ public void autoTrackSessionsValid() {
assertTrue(config.impl.getAutoTrackSessions());
}

@Test
public void sendLaunchCrashesSynchronouslyValid() {
config.setSendLaunchCrashesSynchronously(true);
assertTrue(config.impl.getSendLaunchCrashesSynchronously());
}

@Test
public void errorTypesValid() {
ErrorTypes errorTypes = new ErrorTypes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ internal class ImmutableConfigTest {
@Suppress("DEPRECATION") // tests deprecated option is set via launchDurationMillis
assertEquals(seed.launchCrashThresholdMs, launchDurationMillis)
assertEquals(seed.launchDurationMillis, launchDurationMillis)
assertTrue(sendLaunchCrashesSynchronously)
assertEquals(NoopLogger, seed.logger)
assertEquals(seed.maxBreadcrumbs, maxBreadcrumbs)
assertEquals(seed.maxPersistedEvents, maxPersistedEvents)
Expand Down Expand Up @@ -108,6 +109,7 @@ internal class ImmutableConfigTest {
seed.maxPersistedSessions = 103
seed.persistUser = true
seed.enabledBreadcrumbTypes = emptySet()
seed.sendLaunchCrashesSynchronously = false

// verify overrides are copied across
with(convertToImmutableConfig(seed, "f7ab")) {
Expand Down Expand Up @@ -141,6 +143,7 @@ internal class ImmutableConfigTest {
assertEquals(7000, seed.launchDurationMillis)
@Suppress("DEPRECATION") // should be same as launchDurationMillis
assertEquals(7000, seed.launchCrashThresholdMs)
assertFalse(sendLaunchCrashesSynchronously)
assertEquals(NoopLogger, seed.logger)
assertEquals(37, seed.maxBreadcrumbs)
assertEquals(55, seed.maxPersistedEvents)
Expand Down
Loading

0 comments on commit f41e0cf

Please sign in to comment.