diff --git a/CHANGELOG.md b/CHANGELOG.md index fa3bcb29fc..5d75f82c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index 1a0955bc37..9efca8fcc4 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -3,6 +3,7 @@ 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? ) + 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 ) 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? ) LongParameterList:AppWithState.kt$AppWithState$( config: ImmutableConfig, binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, duration: Number?, durationInForeground: Number?, inForeground: Boolean?, isLaunching: Boolean? ) LongParameterList:Device.kt$Device$( buildInfo: DeviceBuildInfo, /** * The Application Binary Interface used */ var cpuAbi: Array<String>?, /** * 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<String, Any>? ) diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt index c9519b0c5b..4733fc3867 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt @@ -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 @@ -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 { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 58aa5569cc..91ae5bba43 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -82,6 +82,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware { private PluginClient pluginClient; final Notifier notifier = new Notifier(); + private LastRunInfo lastRunInfo; + final LastRunInfoStore lastRunInfoStore; + final LaunchCrashTracker launchCrashTracker; /** * Initialize a Bugsnag client @@ -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); @@ -236,6 +240,9 @@ public void run() { eventStore.flushOnLaunch(); sessionTracker.flushAsync(); + lastRunInfoStore = new LastRunInfoStore(immutableConfig); + loadLastRunInfo(); + // leave auto breadcrumb Map data = Collections.emptyMap(); leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data); @@ -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; @@ -283,6 +292,24 @@ 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 ignored) { + } } private void loadPlugins(@NonNull Configuration configuration) { @@ -623,6 +650,12 @@ 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(); + new LastRunInfo(consecutiveLaunchCrashes, true, false); + lastRunInfoStore.persist(lastRunInfo); } void populateAndNotifyAndroidEvent(@NonNull Event event, @@ -856,7 +889,7 @@ void leaveAutoBreadcrumb(@NonNull String message, */ @Nullable public LastRunInfo getLastRunInfo() { - return null; + return lastRunInfo; } /** @@ -869,7 +902,7 @@ public LastRunInfo getLastRunInfo() { * has precedence over the value supplied via the launchDurationMillis configuration option. */ public void markLaunchCompleted() { - + launchCrashTracker.markLaunchCompleted(); } SessionTracker getSessionTracker() { diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt index 9c109325ed..2383330230 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt @@ -1,4 +1,37 @@ package com.bugsnag.android -class LaunchCrashTracker { -} \ No newline at end of file +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) + + init { + val delay = config.launchDurationMillis + + if (delay > 0) { + executor.executeExistingDelayedTasksAfterShutdownPolicy = false + try { + executor.schedule({ markLaunchCompleted() }, delay, TimeUnit.MILLISECONDS) + } catch (ignored: RejectedExecutionException) { + // ignore v. unlikely rejection failure on new executor + } + } + } + + fun markLaunchCompleted() { + executor.shutdown() + launching.set(false) + } + + fun isLaunching() = launching.get() +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorSerializationTest.kt index b3816f0bd2..d5620c39b7 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorSerializationTest.kt @@ -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 @@ -49,6 +50,7 @@ internal class AppDataCollectorSerializationTest { convert(config), sessionTracker, am, + launchCrashTracker, NoopLogger ) appData.codeBundleId = "foo-99" diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/AppMetadataSerializationTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/AppMetadataSerializationTest.kt index 8339a60f00..264fa20cbd 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/AppMetadataSerializationTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/AppMetadataSerializationTest.kt @@ -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 @@ -49,6 +50,7 @@ internal class AppMetadataSerializationTest { convert(config), sessionTracker, am, + launchCrashTracker, NoopLogger ) appData.codeBundleId = "foo-99" diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java index 01fab7556a..b4e54fc241 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java @@ -91,6 +91,12 @@ public class ClientFacadeTest { @Mock DeviceWithState device; + @Mock + LastRunInfoStore lastRunInfoStore; + + @Mock + LaunchCrashTracker launchCrashTracker; + private Client client; private InterceptingLogger logger; @@ -119,7 +125,9 @@ public void setUp() { connectivity, storageManager, logger, - deliveryDelegate + deliveryDelegate, + lastRunInfoStore, + launchCrashTracker ); // required fields for generating an event @@ -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(); + } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/LaunchCrashTrackerTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/LaunchCrashTrackerTest.kt index 2f2858a300..4ef1c6ec57 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/LaunchCrashTrackerTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/LaunchCrashTrackerTest.kt @@ -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 \ No newline at end of file +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()) + } +} diff --git a/features/smoke_tests/handled.feature b/features/smoke_tests/handled.feature index 8c521a2daf..c977823c35 100644 --- a/features/smoke_tests/handled.feature +++ b/features/smoke_tests/handled.feature @@ -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 @@ -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 diff --git a/features/smoke_tests/unhandled.feature b/features/smoke_tests/unhandled.feature index 07b6a4fcc2..206d06c3ec 100644 --- a/features/smoke_tests/unhandled.feature +++ b/features/smoke_tests/unhandled.feature @@ -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 @@ -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