Skip to content

Commit

Permalink
feat: populate app.isLaunching field in JVM events
Browse files Browse the repository at this point in the history
  • Loading branch information
fractalwrench committed Mar 2, 2021
1 parent 4855410 commit 697da0e
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* 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)
[#1165](https://github.com/bugsnag/bugsnag-android/pull/1165)

## 5.7.0 (2021-02-18)

Expand Down
1 change: 1 addition & 0 deletions bugsnag-android-core/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<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:AppDataCollector.kt$AppDataCollector$( appContext: Context, private val packageManager: PackageManager?, private val config: ImmutableConfig, private val sessionTracker: SessionTracker, private val activityManager: ActivityManager?, private val launchCrashTracker: LaunchCrashTracker, private val logger: Logger )</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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal class AppDataCollector(
private val config: ImmutableConfig,
private val sessionTracker: SessionTracker,
private val activityManager: ActivityManager?,
private val launchCrashTracker: LaunchCrashTracker,
private val logger: Logger
) {
var codeBundleId: String? = null
Expand All @@ -34,7 +35,7 @@ internal class AppDataCollector(
fun generateAppWithState(): AppWithState = AppWithState(
config, binaryArch, packageName, releaseStage, versionName, codeBundleId,
getDurationMs(), calculateDurationInForeground(), sessionTracker.isInForeground,
false
launchCrashTracker.isLaunching()
)

fun getAppDataMetadata(): MutableMap<String, Any?> {
Expand Down
43 changes: 39 additions & 4 deletions bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
private PluginClient pluginClient;

final Notifier notifier = new Notifier();
LastRunInfo lastRunInfo;
final LastRunInfoStore lastRunInfoStore;
final LaunchCrashTracker launchCrashTracker;

/**
* Initialize a Bugsnag client
Expand Down Expand Up @@ -150,8 +153,9 @@ public Unit invoke(Boolean hasConnection, String networkState) {
ActivityManager am =
(ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE);

launchCrashTracker = new LaunchCrashTracker(immutableConfig);
appDataCollector = new AppDataCollector(appContext, appContext.getPackageManager(),
immutableConfig, sessionTracker, am, logger);
immutableConfig, sessionTracker, am, launchCrashTracker, logger);

// load the device + user information
SharedPrefMigrator sharedPrefMigrator = new SharedPrefMigrator(appContext);
Expand Down Expand Up @@ -236,6 +240,9 @@ public void run() {
eventStore.flushOnLaunch();
sessionTracker.flushAsync();

lastRunInfoStore = new LastRunInfoStore(immutableConfig);
loadLastRunInfo();

// leave auto breadcrumb
Map<String, Object> data = Collections.emptyMap();
leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data);
Expand All @@ -262,7 +269,9 @@ public void run() {
Connectivity connectivity,
StorageManager storageManager,
Logger logger,
DeliveryDelegate deliveryDelegate
DeliveryDelegate deliveryDelegate,
LastRunInfoStore lastRunInfoStore,
LaunchCrashTracker launchCrashTracker
) {
this.immutableConfig = immutableConfig;
this.metadataState = metadataState;
Expand All @@ -283,6 +292,25 @@ public void run() {
this.storageManager = storageManager;
this.logger = logger;
this.deliveryDelegate = deliveryDelegate;
this.lastRunInfoStore = lastRunInfoStore;
this.launchCrashTracker = launchCrashTracker;
}

/**
* Load information about the last run, and reset the persisted information to the defaults.
*/
private void loadLastRunInfo() {
try {
Async.run(new Runnable() {
@Override
public void run() {
lastRunInfo = lastRunInfoStore.load();
lastRunInfoStore.persist(new LastRunInfo(0, false, false));
}
});
} catch (RejectedExecutionException exc) {
logger.w("Failed to load last run info", exc);
}
}

private void loadPlugins(@NonNull Configuration configuration) {
Expand Down Expand Up @@ -623,6 +651,13 @@ void notifyUnhandledException(@NonNull Throwable exc, Metadata metadata,
Metadata data = Metadata.Companion.merge(metadataState.getMetadata(), metadata);
Event event = new Event(exc, immutableConfig, handledState, data, logger);
populateAndNotifyAndroidEvent(event, null);

// persist LastRunInfo so that on relaunch users can check the app crashed
int consecutiveLaunchCrashes = lastRunInfo == null ? 0
: lastRunInfo.getConsecutiveLaunchCrashes();
boolean launching = launchCrashTracker.isLaunching();
LastRunInfo runInfo = new LastRunInfo(consecutiveLaunchCrashes, true, launching);
lastRunInfoStore.persist(runInfo);
}

void populateAndNotifyAndroidEvent(@NonNull Event event,
Expand Down Expand Up @@ -856,7 +891,7 @@ void leaveAutoBreadcrumb(@NonNull String message,
*/
@Nullable
public LastRunInfo getLastRunInfo() {
return null;
return lastRunInfo;
}

/**
Expand All @@ -869,7 +904,7 @@ public LastRunInfo getLastRunInfo() {
* has precedence over the value supplied via the launchDurationMillis configuration option.
*/
public void markLaunchCompleted() {

launchCrashTracker.markLaunchCompleted();
}

SessionTracker getSessionTracker() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ internal class LastRunInfoStore(config: ImmutableConfig) {
val crashed = lines[1].asBooleanValue(KEY_CRASHED)
val crashedDuringLaunch = lines[2].asBooleanValue(KEY_CRASHED_DURING_LAUNCH)
LastRunInfo(consecutiveLaunchCrashes, crashed, crashedDuringLaunch)
} catch (ignored: NumberFormatException) {
null // unlikely case where information was serialized incorrectly
} catch (exc: NumberFormatException) {
// unlikely case where information was serialized incorrectly
logger.w("Failed to read consecutiveLaunchCrashes from saved lastRunInfo", exc)
null
}
}

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

class LaunchCrashTracker {
}
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean

/**
* Tracks whether the app is currently in its launch period. This creates a timer of
* configuration.launchDurationMillis, after which which the launch period is considered
* complete. If this value is zero, then the user must manually call markLaunchCompleted().
*/
internal class LaunchCrashTracker(config: ImmutableConfig) {

private val launching = AtomicBoolean(true)
private val executor = ScheduledThreadPoolExecutor(1)
private val logger = config.logger

init {
val delay = config.launchDurationMillis

if (delay > 0) {
executor.executeExistingDelayedTasksAfterShutdownPolicy = false
try {
executor.schedule({ markLaunchCompleted() }, delay, TimeUnit.MILLISECONDS)
} catch (exc: RejectedExecutionException) {
logger.w("Failed to schedule timer for LaunchCrashTracker", exc)
}
}
}

fun markLaunchCompleted() {
executor.shutdown()
launching.set(false)
}

fun isLaunching() = launching.get()
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal class AppDataCollectorSerializationTest {
val pm = mock(PackageManager::class.java)
val am = mock(ActivityManager::class.java)
val sessionTracker = mock(SessionTracker::class.java)
val launchCrashTracker = mock(LaunchCrashTracker::class.java)
val config = BugsnagTestUtils.generateConfiguration()

// populate summary fields
Expand All @@ -49,6 +50,7 @@ internal class AppDataCollectorSerializationTest {
convert(config),
sessionTracker,
am,
launchCrashTracker,
NoopLogger
)
appData.codeBundleId = "foo-99"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal class AppMetadataSerializationTest {
val pm = mock(PackageManager::class.java)
val am = mock(ActivityManager::class.java)
val sessionTracker = mock(SessionTracker::class.java)
val launchCrashTracker = mock(LaunchCrashTracker::class.java)
val config = BugsnagTestUtils.generateConfiguration()

// populate summary fields
Expand All @@ -49,6 +50,7 @@ internal class AppMetadataSerializationTest {
convert(config),
sessionTracker,
am,
launchCrashTracker,
NoopLogger
)
appData.codeBundleId = "foo-99"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ class BugsnagApiTest {
@Test
fun lastRunInfo() {
Bugsnag.getLastRunInfo()
verify(client, times(1)).lastRunInfo
verify(client, times(1)).getLastRunInfo()
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ public class ClientFacadeTest {
@Mock
DeviceWithState device;

@Mock
LastRunInfoStore lastRunInfoStore;

@Mock
LaunchCrashTracker launchCrashTracker;

private Client client;
private InterceptingLogger logger;

Expand Down Expand Up @@ -119,7 +125,9 @@ public void setUp() {
connectivity,
storageManager,
logger,
deliveryDelegate
deliveryDelegate,
lastRunInfoStore,
launchCrashTracker
);

// required fields for generating an event
Expand Down Expand Up @@ -468,4 +476,10 @@ public void addRuntimeVersionInfo() {
client.addRuntimeVersionInfo("foo", "bar");
verify(deviceDataCollector, times(1)).addRuntimeVersionInfo("foo", "bar");
}

@Test
public void markLaunchCompleted() {
client.markLaunchCompleted();
verify(launchCrashTracker, times(1)).markLaunchCompleted();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
package com.bugsnag.android

import org.junit.Assert.*
import com.bugsnag.android.BugsnagTestUtils.convert
import com.bugsnag.android.BugsnagTestUtils.generateConfiguration
import com.bugsnag.android.BugsnagTestUtils.generateImmutableConfig
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class LaunchCrashTrackerTest
class LaunchCrashTrackerTest {

@Test
fun defaultLaunchPeriodManualMark() {
val tracker = LaunchCrashTracker(generateImmutableConfig())
assertTrue(tracker.isLaunching())
tracker.markLaunchCompleted()
assertFalse(tracker.isLaunching())
}

@Test
fun zeroLaunchPeriodManualMark() {
val tracker = LaunchCrashTracker(
convert(
generateConfiguration().apply {
launchDurationMillis = 0
}
)
)
assertTrue(tracker.isLaunching())

java.lang.Thread.sleep(10)
assertTrue(tracker.isLaunching())
tracker.markLaunchCompleted()
assertFalse(tracker.isLaunching())
}

@Test
fun smallLaunchPeriodAutomatic() {
val tracker = LaunchCrashTracker(
convert(
generateConfiguration().apply {
launchDurationMillis = 1
}
)
)
assertTrue(tracker.isLaunching())

java.lang.Thread.sleep(10)
assertFalse(tracker.isLaunching())
}
}
2 changes: 2 additions & 0 deletions features/smoke_tests/handled.feature
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Scenario: Notify caught Java exception with default configuration
And the error payload field "events.0.app.duration" is an integer
And the error payload field "events.0.app.durationInForeground" is an integer
And the event "app.inForeground" is true
And the event "app.isLaunching" is true
And the error payload field "events.0.metaData.app.memoryUsage" is greater than 0
And the event "metaData.app.name" equals "MazeRunner"
And the event "metaData.app.lowMemory" is false
Expand Down Expand Up @@ -180,6 +181,7 @@ Scenario: Handled C functionality
And the error payload field "events.0.app.duration" is an integer
And the error payload field "events.0.app.durationInForeground" is an integer
And the event "app.inForeground" is true
And the event "app.isLaunching" is true

# Device data
And the error payload field "events.0.device.cpuAbi" is a non-empty array
Expand Down
2 changes: 2 additions & 0 deletions features/smoke_tests/unhandled.feature
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Scenario: Unhandled Java Exception with loaded configuration
And the error payload field "events.0.app.duration" is an integer
And the error payload field "events.0.app.durationInForeground" is an integer
And the event "app.inForeground" is true
And the event "app.isLaunching" is true
And the error payload field "events.0.metaData.app.memoryUsage" is greater than 0
And the event "metaData.app.name" equals "MazeRunner"
And the event "metaData.app.lowMemory" is false
Expand Down Expand Up @@ -279,6 +280,7 @@ Scenario: ANR detection
And the error payload field "events.0.app.duration" is an integer
And the error payload field "events.0.app.durationInForeground" is an integer
And the event "app.inForeground" is true
And the event "app.isLaunching" is true
And the event "metaData.app.name" equals "MazeRunner"

# Device data
Expand Down

0 comments on commit 697da0e

Please sign in to comment.