From f5604d6ede8deb4f8a60ecc2409c00d0ca0a4549 Mon Sep 17 00:00:00 2001 From: M66B Date: Thu, 23 Jun 2022 20:23:15 +0200 Subject: [PATCH] Updated Bugsnag to version 5.23.0 --- app/build.gradle | 2 +- .../bugsnag/android/BackgroundTaskService.kt | 9 +- .../java/com/bugsnag/android/Bugsnag.java | 59 +++--- .../com/bugsnag/android/BugsnagEventMapper.kt | 10 +- .../main/java/com/bugsnag/android/Client.java | 3 +- .../com/bugsnag/android/ConfigInternal.kt | 4 + .../com/bugsnag/android/Configuration.java | 46 +++++ .../com/bugsnag/android/ContextExtensions.kt | 5 + .../bugsnag/android/DataCollectionModule.kt | 2 + .../bugsnag/android/DeviceDataCollector.kt | 41 ++++- .../android/DeviceIdFilePersistence.kt | 163 +++++++++++++++++ .../bugsnag/android/DeviceIdPersistence.kt | 14 ++ .../java/com/bugsnag/android/DeviceIdStore.kt | 171 +++--------------- .../java/com/bugsnag/android/ErrorType.kt | 9 +- .../com/bugsnag/android/EventFilenameInfo.kt | 45 +++-- .../com/bugsnag/android/EventStorageModule.kt | 23 +-- .../java/com/bugsnag/android/EventStore.java | 44 ++++- .../java/com/bugsnag/android/FileStore.java | 2 +- .../bugsnag/android/ManifestConfigLoader.kt | 2 + .../com/bugsnag/android/NativeStackframe.kt | 8 +- .../main/java/com/bugsnag/android/Notifier.kt | 2 +- .../bugsnag/android/SessionFilenameInfo.kt | 55 ++++++ .../android/SessionLifecycleCallback.kt | 21 ++- .../com/bugsnag/android/SessionStore.java | 13 +- .../com/bugsnag/android/SessionTracker.java | 11 +- .../com/bugsnag/android/SharedPrefMigrator.kt | 7 +- .../java/com/bugsnag/android/Stackframe.kt | 8 + .../java/com/bugsnag/android/StorageModule.kt | 2 + .../java/com/bugsnag/android/Telemetry.kt | 16 ++ .../java/com/bugsnag/android/ThreadState.kt | 114 +++++++++--- .../java/com/bugsnag/android/ThreadType.kt | 5 + .../bugsnag/android/internal/BugsnagMapper.kt | 44 +++++ .../android/internal/ImmutableConfig.kt | 5 + app/src/main/java/eu/faircode/email/Log.java | 2 + patches/Bugsnag.patch | 46 +++-- 35 files changed, 737 insertions(+), 276 deletions(-) create mode 100644 app/src/main/java/com/bugsnag/android/DeviceIdFilePersistence.kt create mode 100644 app/src/main/java/com/bugsnag/android/DeviceIdPersistence.kt create mode 100644 app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt create mode 100644 app/src/main/java/com/bugsnag/android/Telemetry.kt create mode 100644 app/src/main/java/com/bugsnag/android/internal/BugsnagMapper.kt diff --git a/app/build.gradle b/app/build.gradle index fe45c85d24..5dfb8441db 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -359,7 +359,7 @@ dependencies { def dnsjava_version = "2.1.9" def openpgp_version = "12.0" def badge_version = "1.1.22" - def bugsnag_version = "5.19.2" + def bugsnag_version = "5.23.0" def biweekly_version = "0.6.6" def vcard_version = "0.11.3" def relinker_version = "1.4.3" diff --git a/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt b/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt index 4e763633f0..c171c23d50 100644 --- a/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt +++ b/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt @@ -158,18 +158,13 @@ internal class BackgroundTaskService( internalReportExecutor.shutdownNow() defaultExecutor.shutdownNow() - // shutdown the error/session executors first, waiting for existing tasks to complete. - // If a request fails it may perform IO to persist the payload for delivery next launch, - // which would submit tasks to the IO executor - therefore it's critical to - // shutdown the IO executor last. + // Wait a little while for these ones to shut down errorExecutor.shutdown() sessionExecutor.shutdown() + ioExecutor.shutdown() errorExecutor.awaitTerminationSafe() sessionExecutor.awaitTerminationSafe() - - // shutdown the IO executor last, waiting for any existing tasks to complete - ioExecutor.shutdown() ioExecutor.awaitTerminationSafe() } diff --git a/app/src/main/java/com/bugsnag/android/Bugsnag.java b/app/src/main/java/com/bugsnag/android/Bugsnag.java index f32b49d6ae..11538b580d 100644 --- a/app/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/app/src/main/java/com/bugsnag/android/Bugsnag.java @@ -69,6 +69,15 @@ public final class Bugsnag { return client; } + /** + * Returns true if one of the start methods have been has been called and + * so Bugsnag is initialized; false if start has not been called and the + * other methods will throw IllegalStateException. + */ + public static boolean isStarted() { + return client != null; + } + private static void logClientInitWarning() { getClient().logger.w("Multiple Bugsnag.start calls detected. Ignoring."); } @@ -76,18 +85,19 @@ public final class Bugsnag { /** * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts * represent what was happening in your application at the time an error occurs. - * + *

* In an android app the "context" is automatically set as the foreground Activity. * If you would like to set this value manually, you should alter this property. */ - @Nullable public static String getContext() { + @Nullable + public static String getContext() { return getClient().getContext(); } /** * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts * represent what was happening in your application at the time an error occurs. - * + *

* In an android app the "context" is automatically set as the foreground Activity. * If you would like to set this value manually, you should alter this property. */ @@ -115,15 +125,15 @@ public final class Bugsnag { /** * Add a "on error" callback, to execute code at the point where an error report is * captured in Bugsnag. - * + *

* You can use this to add or modify information attached to an Event * before it is sent to your dashboard. You can also return * false from any callback to prevent delivery. "on error" * callbacks do not run before reports generated in the event * of immediate app termination from crashes in C/C++ code. - * + *

* For example: - * + *

* Bugsnag.addOnError(new OnErrorCallback() { * public boolean run(Event event) { * event.setSeverity(Severity.INFO); @@ -140,6 +150,7 @@ public final class Bugsnag { /** * Removes a previously added "on error" callback + * * @param onError the callback to remove */ public static void removeOnError(@NonNull OnErrorCallback onError) { @@ -149,12 +160,12 @@ public final class Bugsnag { /** * Add an "on breadcrumb" callback, to execute code before every * breadcrumb captured by Bugsnag. - * + *

* You can use this to modify breadcrumbs before they are stored by Bugsnag. * You can also return false from any callback to ignore a breadcrumb. - * + *

* For example: - * + *

* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() { * public boolean run(Breadcrumb breadcrumb) { * return false; // ignore the breadcrumb @@ -170,6 +181,7 @@ public final class Bugsnag { /** * Removes a previously added "on breadcrumb" callback + * * @param onBreadcrumb the callback to remove */ public static void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) { @@ -179,12 +191,12 @@ public final class Bugsnag { /** * Add an "on session" callback, to execute code before every * session captured by Bugsnag. - * + *

* You can use this to modify sessions before they are stored by Bugsnag. * You can also return false from any callback to ignore a session. - * + *

* For example: - * + *

* Bugsnag.onSession(new OnSessionCallback() { * public boolean run(Session session) { * return false; // ignore the session @@ -200,6 +212,7 @@ public final class Bugsnag { /** * Removes a previously added "on session" callback + * * @param onSession the callback to remove */ public static void removeOnSession(@NonNull OnSessionCallback onSession) { @@ -219,7 +232,7 @@ public final class Bugsnag { * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag - * @param onError callback invoked on the generated error report for + * @param onError callback invoked on the generated error report for * additional modification */ public static void notify(@NonNull final Throwable exception, @@ -286,7 +299,8 @@ public final class Bugsnag { /** * Leave a "breadcrumb" log message representing an action or event which * occurred in your app, to aid with debugging - * @param message A short label + * + * @param message A short label * @param metadata Additional diagnostic information about the app environment * @param type A category for the breadcrumb */ @@ -332,11 +346,10 @@ public final class Bugsnag { * * stability score. * + * @return true if a previous session was resumed, false if a new session was started. * @see #startSession() * @see #pauseSession() * @see Configuration#setAutoTrackSessions(boolean) - * - * @return true if a previous session was resumed, false if a new session was started. */ public static boolean resumeSession() { return getClient().resumeSession(); @@ -365,7 +378,7 @@ public final class Bugsnag { * Returns the current buffer of breadcrumbs that will be sent with captured events. This * ordered list represents the most recent breadcrumbs to be captured up to the limit * set in {@link Configuration#getMaxBreadcrumbs()}. - * + *

* The returned collection is readonly and mutating the list will cause no effect on the * Client's state. If you wish to alter the breadcrumbs collected by the Client then you should * use {@link Configuration#setEnabledBreadcrumbTypes(Set)} and @@ -380,7 +393,7 @@ public final class Bugsnag { /** * 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. @@ -394,7 +407,7 @@ public final class Bugsnag { * 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. @@ -462,8 +475,12 @@ public final class Bugsnag { @NonNull public static Client getClient() { if (client == null) { - throw new IllegalStateException("You must call Bugsnag.start before any" - + " other Bugsnag methods"); + synchronized (lock) { + if (client == null) { + throw new IllegalStateException("You must call Bugsnag.start before any" + + " other Bugsnag methods"); + } + } } return client; diff --git a/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt b/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt index 8f17f4049d..d850562671 100644 --- a/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt +++ b/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt @@ -11,6 +11,10 @@ internal class BugsnagEventMapper( private val logger: Logger ) { + internal fun convertToEvent(map: Map, apiKey: String): Event { + return Event(convertToEventImpl(map, apiKey), logger) + } + @Suppress("UNCHECKED_CAST") internal fun convertToEventImpl(map: Map, apiKey: String): EventInternal { val event = EventInternal(apiKey) @@ -85,7 +89,11 @@ internal class BugsnagEventMapper( return event } - internal fun convertErrorInternal(error: Map): ErrorInternal { + internal fun convertError(error: Map): Error { + return Error(convertErrorInternal(error), logger) + } + + internal fun convertErrorInternal(error: Map): ErrorInternal { return ErrorInternal( error.readEntry("errorClass"), error["message"] as? String, diff --git a/app/src/main/java/com/bugsnag/android/Client.java b/app/src/main/java/com/bugsnag/android/Client.java index a6cdf8d932..c91becd799 100644 --- a/app/src/main/java/com/bugsnag/android/Client.java +++ b/app/src/main/java/com/bugsnag/android/Client.java @@ -170,7 +170,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule, configModule, systemServiceModule, trackerModule, - bgTaskService, connectivity, storageModule.getDeviceId(), memoryTrimState); + bgTaskService, connectivity, storageModule.getDeviceId(), + storageModule.getInternalDeviceId(), memoryTrimState); dataCollectionModule.resolveDependencies(bgTaskService, TaskType.IO); appDataCollector = dataCollectionModule.getAppDataCollector(); deviceDataCollector = dataCollectionModule.getDeviceDataCollector(); diff --git a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt index f3ec4939f9..9a967d1853 100644 --- a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt +++ b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt @@ -2,6 +2,7 @@ package com.bugsnag.android import android.content.Context import java.io.File +import java.util.EnumSet internal class ConfigInternal( var apiKey: String @@ -40,6 +41,7 @@ internal class ConfigInternal( var maxBreadcrumbs: Int = DEFAULT_MAX_BREADCRUMBS var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS + var maxReportedThreads: Int = DEFAULT_MAX_REPORTED_THREADS var context: String? = null var redactedKeys: Set @@ -51,6 +53,7 @@ internal class ConfigInternal( var discardClasses: Set = emptySet() var enabledReleaseStages: Set? = null var enabledBreadcrumbTypes: Set? = null + var telemetry: Set = EnumSet.of(Telemetry.INTERNAL_ERRORS) var projectPackages: Set = emptySet() var persistenceDirectory: File? = null @@ -99,6 +102,7 @@ internal class ConfigInternal( private const val DEFAULT_MAX_BREADCRUMBS = 50 private const val DEFAULT_MAX_PERSISTED_SESSIONS = 128 private const val DEFAULT_MAX_PERSISTED_EVENTS = 32 + private const val DEFAULT_MAX_REPORTED_THREADS = 200 private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000 @JvmStatic diff --git a/app/src/main/java/com/bugsnag/android/Configuration.java b/app/src/main/java/com/bugsnag/android/Configuration.java index 3b9e6dc2a2..0be3d0ff9a 100644 --- a/app/src/main/java/com/bugsnag/android/Configuration.java +++ b/app/src/main/java/com/bugsnag/android/Configuration.java @@ -561,6 +561,32 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F } } + /** + * Gets the maximum number of threads that will be reported with an event. Once the threshold is + * reached, all remaining threads will be omitted. + * + * By default, up to 200 threads are reported. + */ + public int getMaxReportedThreads() { + return impl.getMaxReportedThreads(); + } + + /** + * Sets the maximum number of threads that will be reported with an event. Once the threshold is + * reached, all remaining threads will be omitted. + * + * By default, up to 200 threads are reported. + */ + public void setMaxReportedThreads(int maxReportedThreads) { + if (maxReportedThreads >= 0) { + impl.setMaxReportedThreads(maxReportedThreads); + } else { + getLogger().e("Invalid configuration value detected. " + + "Option maxReportedThreads should be a positive integer." + + "Supplied value is " + maxReportedThreads); + } + } + /** * Sets the maximum number of persisted sessions which will be stored. Once the threshold is * reached, the oldest session will be deleted. @@ -720,6 +746,26 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F impl.setEnabledBreadcrumbTypes(enabledBreadcrumbTypes); } + @NonNull + public Set getTelemetry() { + return impl.getTelemetry(); + } + + /** + * Set which telemetry will be sent to Bugsnag. By default, all telemetry is enabled. + * + * The following telemetry can be enabled: + * + * - internal errors: Errors in the Bugsnag SDK itself. + */ + public void setTelemetry(@NonNull Set telemetry) { + if (telemetry != null) { + impl.setTelemetry(telemetry); + } else { + logNull("telemetry"); + } + } + /** * Sets which package names Bugsnag should consider as a part of the * running application. We mark stacktrace lines as in-project if they diff --git a/app/src/main/java/com/bugsnag/android/ContextExtensions.kt b/app/src/main/java/com/bugsnag/android/ContextExtensions.kt index 667d536e8a..da0c4c3c9e 100644 --- a/app/src/main/java/com/bugsnag/android/ContextExtensions.kt +++ b/app/src/main/java/com/bugsnag/android/ContextExtensions.kt @@ -5,6 +5,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.location.LocationManager import android.net.ConnectivityManager import android.os.RemoteException import android.os.storage.StorageManager @@ -69,3 +70,7 @@ internal fun Context.getConnectivityManager(): ConnectivityManager? = @JvmName("getStorageManagerFrom") internal fun Context.getStorageManager(): StorageManager? = safeGetSystemService(Context.STORAGE_SERVICE) + +@JvmName("getLocationManager") +internal fun Context.getLocationManager(): LocationManager? = + safeGetSystemService(Context.LOCATION_SERVICE) diff --git a/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt b/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt index 577b9bf7bc..20bf61229c 100644 --- a/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt +++ b/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt @@ -18,6 +18,7 @@ internal class DataCollectionModule( bgTaskService: BackgroundTaskService, connectivity: Connectivity, deviceId: String?, + internalDeviceId: String?, memoryTrimState: MemoryTrimState ) : DependencyModule() { @@ -49,6 +50,7 @@ internal class DataCollectionModule( ctx, ctx.resources, deviceId, + internalDeviceId, deviceBuildInfo, dataDir, rootDetector, diff --git a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index 69c4063e97..ed3573f3fe 100644 --- a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt @@ -13,7 +13,6 @@ import android.os.Build import android.provider.Settings import java.io.File import java.util.Date -import java.util.HashMap import java.util.Locale import java.util.concurrent.Callable import java.util.concurrent.Future @@ -28,6 +27,7 @@ internal class DeviceDataCollector( private val appContext: Context, resources: Resources, private val deviceId: String?, + private val internalDeviceId: String?, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, rootDetector: RootDetector, @@ -42,7 +42,7 @@ internal class DeviceDataCollector( private val screenResolution = getScreenResolution() private val locale = Locale.getDefault().toString() private val cpuAbi = getCpuAbi() - private val runtimeVersions: MutableMap + private var runtimeVersions: MutableMap private val rootedFuture: Future? private val totalMemoryFuture: Future? = retrieveTotalDeviceMemory() private var orientation = AtomicInteger(resources.configuration.orientation) @@ -89,6 +89,19 @@ internal class DeviceDataCollector( Date(now) ) + fun generateInternalDeviceWithState(now: Long) = DeviceWithState( + buildInfo, + checkIsRooted(), + internalDeviceId, + locale, + totalMemoryFuture.runCatching { this?.get() }.getOrNull(), + runtimeVersions.toMutableMap(), + calculateFreeDisk(), + calculateFreeMemory(), + getOrientationAsString(), + Date(now) + ) + fun getDeviceMetadata(): Map { val map = HashMap() populateBatteryInfo(into = map) @@ -163,19 +176,24 @@ internal class DeviceDataCollector( */ private fun getLocationStatus(): String? { try { - val cr = appContext.contentResolver - @Suppress("DEPRECATION") val providersAllowed = - Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED) - return when { - providersAllowed != null && providersAllowed.isNotEmpty() -> "allowed" - else -> "disallowed" - } + return if (isLocationEnabled()) "allowed" else "disallowed" } catch (exception: Exception) { logger.w("Could not get locationStatus") } return null } + private fun isLocationEnabled() = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> + appContext.getLocationManager()?.isLocationEnabled == true + else -> { + val cr = appContext.contentResolver + @Suppress("DEPRECATION") val providersAllowed = + Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED) + providersAllowed != null && providersAllowed.isNotEmpty() + } + } + /** * Get the current status of network access, eg "cellular" */ @@ -293,6 +311,9 @@ internal class DeviceDataCollector( } fun addRuntimeVersionInfo(key: String, value: String) { - runtimeVersions[key] = value + // Use copy-on-write to avoid a ConcurrentModificationException in generateDeviceWithState + val newRuntimeVersions = runtimeVersions.toMutableMap() + newRuntimeVersions[key] = value + runtimeVersions = newRuntimeVersions } } diff --git a/app/src/main/java/com/bugsnag/android/DeviceIdFilePersistence.kt b/app/src/main/java/com/bugsnag/android/DeviceIdFilePersistence.kt new file mode 100644 index 0000000000..60fa1f9364 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/DeviceIdFilePersistence.kt @@ -0,0 +1,163 @@ +package com.bugsnag.android + +import android.util.JsonReader +import java.io.File +import java.io.IOException +import java.lang.Thread +import java.nio.channels.FileChannel +import java.nio.channels.FileLock +import java.nio.channels.OverlappingFileLockException +import java.util.UUID + +/** + * This class is responsible for persisting and retrieving a device ID to a file. + * + * This class is made multi-process safe through the use of a [FileLock], and thread safe + * through the use of a [ReadWriteLock] in [SynchronizedStreamableStore]. + */ +class DeviceIdFilePersistence( + private val file: File, + private val deviceIdGenerator: () -> UUID, + private val logger: Logger +) : DeviceIdPersistence { + private val synchronizedStreamableStore: SynchronizedStreamableStore + + init { + try { + file.createNewFile() + } catch (exc: Throwable) { + logger.w("Failed to created device ID file", exc) + } + this.synchronizedStreamableStore = SynchronizedStreamableStore(file) + } + + /** + * Loads the device ID from its file system location. + * If no value is present then a UUID will be generated and persisted. + */ + override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean): String? { + return try { + // optimistically read device ID without a lock - the majority of the time + // the device ID will already be present so no synchronization is required. + val deviceId = loadDeviceIdInternal() + + if (deviceId?.id != null) { + deviceId.id + } else { + return if (requestCreateIfDoesNotExist) persistNewDeviceUuid(deviceIdGenerator()) else null + } + } catch (exc: Throwable) { + logger.w("Failed to load device ID", exc) + null + } + } + + /** + * Loads the device ID from the file. + * + * If the file has zero length it can't contain device ID, so reading will be skipped. + */ + private fun loadDeviceIdInternal(): DeviceId? { + if (file.length() > 0) { + try { + return synchronizedStreamableStore.load(DeviceId.Companion::fromReader) + } catch (exc: Throwable) { // catch AssertionError which can be thrown by JsonReader + // on Android 8.0/8.1. see https://issuetracker.google.com/issues/79920590 + logger.w("Failed to load device ID", exc) + } + } + return null + } + + /** + * Write a new Device ID to the file. + */ + private fun persistNewDeviceUuid(uuid: UUID): String? { + return try { + // acquire a FileLock to prevent Clients in different processes writing + // to the same file concurrently + file.outputStream().channel.use { channel -> + persistNewDeviceIdWithLock(channel, uuid) + } + } catch (exc: IOException) { + logger.w("Failed to persist device ID", exc) + null + } + } + + private fun persistNewDeviceIdWithLock( + channel: FileChannel, + uuid: UUID + ): String? { + val lock = waitForFileLock(channel) ?: return null + + return try { + // read the device ID again as it could have changed + // between the last read and when the lock was acquired + val deviceId = loadDeviceIdInternal() + + if (deviceId?.id != null) { + // the device ID changed between the last read + // and acquiring the lock, so return the generated value + deviceId.id + } else { + // generate a new device ID and persist it + val newId = DeviceId(uuid.toString()) + synchronizedStreamableStore.persist(newId) + newId.id + } + } finally { + lock.release() + } + } + + /** + * Attempt to acquire a file lock. If [OverlappingFileLockException] is thrown + * then the method will wait for 50ms then try again, for a maximum of 10 attempts. + */ + private fun waitForFileLock(channel: FileChannel): FileLock? { + repeat(MAX_FILE_LOCK_ATTEMPTS) { + try { + return channel.tryLock() + } catch (exc: OverlappingFileLockException) { + Thread.sleep(FILE_LOCK_WAIT_MS) + } + } + return null + } + + companion object { + private const val MAX_FILE_LOCK_ATTEMPTS = 20 + private const val FILE_LOCK_WAIT_MS = 25L + } +} + +/** + * Serializes and deserializes the device ID to/from JSON. + */ +private class DeviceId(val id: String?) : JsonStream.Streamable { + + override fun toStream(stream: JsonStream) { + with(stream) { + beginObject() + name(KEY_ID) + value(id) + endObject() + } + } + + companion object : JsonReadable { + private const val KEY_ID = "id" + + override fun fromReader(reader: JsonReader): DeviceId { + var id: String? = null + with(reader) { + beginObject() + if (hasNext() && KEY_ID == nextName()) { + id = nextString() + } + } + return DeviceId(id) + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/DeviceIdPersistence.kt b/app/src/main/java/com/bugsnag/android/DeviceIdPersistence.kt new file mode 100644 index 0000000000..0c9bec8ac5 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/DeviceIdPersistence.kt @@ -0,0 +1,14 @@ +package com.bugsnag.android + +interface DeviceIdPersistence { + /** + * Loads the device ID from storage. + * + * Device IDs are UUIDs which are persisted on a per-install basis. + * + * This method must be thread-safe and multi-process safe. + * + * Note: requestCreateIfDoesNotExist is only a request; an implementation may still refuse to create a new ID. + */ + fun loadDeviceId(requestCreateIfDoesNotExist: Boolean): String? +} diff --git a/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt b/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt index b9db53c296..31b7c7dc28 100644 --- a/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt +++ b/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt @@ -1,41 +1,33 @@ package com.bugsnag.android import android.content.Context -import android.util.JsonReader import java.io.File -import java.io.IOException -import java.lang.Thread -import java.nio.channels.FileChannel -import java.nio.channels.FileLock -import java.nio.channels.OverlappingFileLockException import java.util.UUID /** - * This class is responsible for persisting and retrieving the device ID which uniquely - * identifies this device. - * - * This class is made multi-process safe through the use of a [FileLock], and thread safe - * through the use of a [ReadWriteLock] in [SynchronizedStreamableStore]. + * This class is responsible for persisting and retrieving the device ID and internal device ID, + * which uniquely identify this device in various contexts. */ internal class DeviceIdStore @JvmOverloads constructor( context: Context, - private val file: File = File(context.filesDir, "device-id"), + deviceIdfile: File = File(context.filesDir, "device-id"), + deviceIdGenerator: () -> UUID = { UUID.randomUUID() }, + internalDeviceIdfile: File = File(context.filesDir, "internal-device-id"), + internalDeviceIdGenerator: () -> UUID = { UUID.randomUUID() }, private val sharedPrefMigrator: SharedPrefMigrator, - private val logger: Logger + logger: Logger ) { - private val synchronizedStreamableStore: SynchronizedStreamableStore + private val persistence: DeviceIdPersistence + private val internalPersistence: DeviceIdPersistence init { - try { - file.createNewFile() - } catch (exc: Throwable) { - logger.w("Failed to created device ID file", exc) - } - this.synchronizedStreamableStore = SynchronizedStreamableStore(file) + persistence = DeviceIdFilePersistence(deviceIdfile, deviceIdGenerator, logger) + internalPersistence = DeviceIdFilePersistence(internalDeviceIdfile, internalDeviceIdGenerator, logger) } /** + * Loads the device ID from * Loads the device ID from its file system location. Device IDs are UUIDs which are * persisted on a per-install basis. This method is thread-safe and multi-process safe. * @@ -43,137 +35,18 @@ internal class DeviceIdStore @JvmOverloads constructor( * be used. If no value is present then a random UUID will be generated and persisted. */ fun loadDeviceId(): String? { - return loadDeviceId { - when (val legacyDeviceId = sharedPrefMigrator.loadDeviceId()) { - null -> UUID.randomUUID() - else -> UUID.fromString(legacyDeviceId) - } + var result = persistence.loadDeviceId(false) + if (result != null) { + return result } + result = sharedPrefMigrator.loadDeviceId(false) + if (result != null) { + return result + } + return persistence.loadDeviceId(true) } - internal fun loadDeviceId(uuidProvider: () -> UUID): String? { - return try { - // optimistically read device ID without a lock - the majority of the time - // the device ID will already be present so no synchronization is required. - val deviceId = loadDeviceIdInternal() - - if (deviceId?.id != null) { - deviceId.id - } else { - return persistNewDeviceUuid(uuidProvider) - } - } catch (exc: Throwable) { - logger.w("Failed to load device ID", exc) - null - } - } - - /** - * Loads the device ID from the file. - * - * If the file has zero length it can't contain device ID, so reading will be skipped. - */ - private fun loadDeviceIdInternal(): DeviceId? { - if (file.length() > 0) { - try { - return synchronizedStreamableStore.load(DeviceId.Companion::fromReader) - } catch (exc: Throwable) { // catch AssertionError which can be thrown by JsonReader - // on Android 8.0/8.1. see https://issuetracker.google.com/issues/79920590 - logger.w("Failed to load device ID", exc) - } - } - return null - } - - /** - * Write a new Device ID to the file. - */ - private fun persistNewDeviceUuid(uuidProvider: () -> UUID): String? { - return try { - // acquire a FileLock to prevent Clients in different processes writing - // to the same file concurrently - file.outputStream().channel.use { channel -> - persistNewDeviceIdWithLock(channel, uuidProvider) - } - } catch (exc: IOException) { - logger.w("Failed to persist device ID", exc) - null - } - } - - private fun persistNewDeviceIdWithLock( - channel: FileChannel, - uuidProvider: () -> UUID - ): String? { - val lock = waitForFileLock(channel) ?: return null - - return try { - // read the device ID again as it could have changed - // between the last read and when the lock was acquired - val deviceId = loadDeviceIdInternal() - - if (deviceId?.id != null) { - // the device ID changed between the last read - // and acquiring the lock, so return the generated value - deviceId.id - } else { - // generate a new device ID and persist it - val newId = DeviceId(uuidProvider().toString()) - synchronizedStreamableStore.persist(newId) - newId.id - } - } finally { - lock.release() - } - } - - /** - * Attempt to acquire a file lock. If [OverlappingFileLockException] is thrown - * then the method will wait for 50ms then try again, for a maximum of 10 attempts. - */ - private fun waitForFileLock(channel: FileChannel): FileLock? { - repeat(MAX_FILE_LOCK_ATTEMPTS) { - try { - return channel.tryLock() - } catch (exc: OverlappingFileLockException) { - Thread.sleep(FILE_LOCK_WAIT_MS) - } - } - return null - } - - companion object { - private const val MAX_FILE_LOCK_ATTEMPTS = 20 - private const val FILE_LOCK_WAIT_MS = 25L - } -} - -/** - * Serializes and deserializes the device ID to/from JSON. - */ -private class DeviceId(val id: String?) : JsonStream.Streamable { - - override fun toStream(stream: JsonStream) { - with(stream) { - beginObject() - name(KEY_ID) - value(id) - endObject() - } - } - - companion object : JsonReadable { - private const val KEY_ID = "id" - - override fun fromReader(reader: JsonReader): DeviceId { - var id: String? = null - with(reader) { - beginObject() - if (hasNext() && KEY_ID == nextName()) { - id = nextString() - } - } - return DeviceId(id) - } + fun loadInternalDeviceId(): String? { + return internalPersistence.loadDeviceId(true) } } diff --git a/app/src/main/java/com/bugsnag/android/ErrorType.kt b/app/src/main/java/com/bugsnag/android/ErrorType.kt index 6ab5f98a56..299b1a0b45 100644 --- a/app/src/main/java/com/bugsnag/android/ErrorType.kt +++ b/app/src/main/java/com/bugsnag/android/ErrorType.kt @@ -18,9 +18,16 @@ enum class ErrorType(internal val desc: String) { /** * An error captured from Android's C layer */ - C("c"); + C("c"), + + /** + * An error captured from a Dart / Flutter application + */ + DART("dart"); internal companion object { + @JvmStatic + @JvmName("fromDescriptor") internal fun fromDescriptor(desc: String) = values().find { it.desc == desc } } } diff --git a/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt b/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt index 6d9ff766c9..f7cfd5f9af 100644 --- a/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt +++ b/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt @@ -22,12 +22,8 @@ internal data class EventFilenameInfo( val errorTypes: Set ) { - /** - * Generates a filename for the Event in the format - * "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json" - */ fun encode(): String { - return "${timestamp}_${apiKey}_${serializeErrorTypeHeader(errorTypes)}_${uuid}_$suffix.json" + return toFilename(apiKey, uuid, timestamp, suffix, errorTypes) } fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH @@ -36,7 +32,21 @@ internal data class EventFilenameInfo( private const val STARTUP_CRASH = "startupcrash" private const val NON_JVM_CRASH = "not-jvm" - @JvmOverloads + /** + * Generates a filename for the Event in the format + * "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json" + */ + fun toFilename( + apiKey: String, + uuid: String, + timestamp: Long, + suffix: String, + errorTypes: Set + ): String { + return "${timestamp}_${apiKey}_${serializeErrorTypeHeader(errorTypes)}_${uuid}_$suffix.json" + } + + @JvmOverloads @JvmStatic fun fromEvent( obj: Any, uuid: String = UUID.randomUUID().toString(), @@ -63,11 +73,12 @@ internal data class EventFilenameInfo( /** * Reads event information from a filename. */ + @JvmStatic fun fromFile(file: File, config: ImmutableConfig): EventFilenameInfo { return EventFilenameInfo( findApiKeyInFilename(file, config), "", // ignore UUID field when reading from file as unused - -1, // ignore timestamp when reading from file as unused + findTimestampInFilename(file), findSuffixInFilename(file), findErrorTypesInFilename(file) ) @@ -77,7 +88,7 @@ internal data class EventFilenameInfo( * Retrieves the api key encoded in the filename, or an empty string if this information * is not encoded for the given event */ - private fun findApiKeyInFilename(file: File, config: ImmutableConfig): String { + internal fun findApiKeyInFilename(file: File, config: ImmutableConfig): String { val name = file.name.removeSuffix("_$STARTUP_CRASH.json") val start = name.indexOf("_") + 1 val end = name.indexOf("_", start) @@ -93,7 +104,7 @@ internal data class EventFilenameInfo( * Retrieves the error types encoded in the filename, or an empty string if this * information is not encoded for the given event */ - private fun findErrorTypesInFilename(eventFile: File): Set { + internal fun findErrorTypesInFilename(eventFile: File): Set { val name = eventFile.name val end = name.lastIndexOf("_", name.lastIndexOf("_") - 1) val start = name.lastIndexOf("_", end - 1) + 1 @@ -111,7 +122,7 @@ internal data class EventFilenameInfo( * Retrieves the error types encoded in the filename, or an empty string if this * information is not encoded for the given event */ - private fun findSuffixInFilename(eventFile: File): String { + internal fun findSuffixInFilename(eventFile: File): String { val name = eventFile.nameWithoutExtension val suffix = name.substring(name.lastIndexOf("_") + 1) return when (suffix) { @@ -120,10 +131,20 @@ internal data class EventFilenameInfo( } } + /** + * Retrieves the error types encoded in the filename, or an empty string if this + * information is not encoded for the given event + */ + @JvmStatic + fun findTimestampInFilename(eventFile: File): Long { + val name = eventFile.nameWithoutExtension + return name.substringBefore("_", missingDelimiterValue = "-1").toLongOrNull() ?: -1 + } + /** * Retrieves the error types for the given event */ - private fun findErrorTypesForEvent(obj: Any): Set { + internal fun findErrorTypesForEvent(obj: Any): Set { return when (obj) { is Event -> obj.impl.getErrorTypesFromStackframes() else -> setOf(ErrorType.C) @@ -133,7 +154,7 @@ internal data class EventFilenameInfo( /** * Calculates the suffix for the given event */ - private fun findSuffixForEvent(obj: Any, launching: Boolean?): String { + internal fun findSuffixForEvent(obj: Any, launching: Boolean?): String { return when { obj is Event && obj.app.isLaunching == true -> STARTUP_CRASH launching == true -> STARTUP_CRASH diff --git a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt index 78980d1458..03f01f0a3e 100644 --- a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt +++ b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt @@ -22,17 +22,18 @@ internal class EventStorageModule( private val cfg = configModule.config private val delegate by future { - InternalReportDelegate( - contextModule.ctx, - cfg.logger, - cfg, - systemServiceModule.storageManager, - dataCollectionModule.appDataCollector, - dataCollectionModule.deviceDataCollector, - trackerModule.sessionTracker, - notifier, - bgTaskService - ) + if (cfg.telemetry.contains(Telemetry.INTERNAL_ERRORS) == true) + InternalReportDelegate( + contextModule.ctx, + cfg.logger, + cfg, + systemServiceModule.storageManager, + dataCollectionModule.appDataCollector, + dataCollectionModule.deviceDataCollector, + trackerModule.sessionTracker, + notifier, + bgTaskService + ) else null } val eventStore by future { EventStore(cfg, cfg.logger, notifier, bgTaskService, delegate, callbackState) } diff --git a/app/src/main/java/com/bugsnag/android/EventStore.java b/app/src/main/java/com/bugsnag/android/EventStore.java index e1365644ae..5ae0c04c50 100644 --- a/app/src/main/java/com/bugsnag/android/EventStore.java +++ b/app/src/main/java/com/bugsnag/android/EventStore.java @@ -7,9 +7,11 @@ import androidx.annotation.Nullable; import java.io.File; import java.util.ArrayList; +import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -119,7 +121,7 @@ class EventStore extends FileStore { List launchCrashes = new ArrayList<>(); for (File file : storedFiles) { - EventFilenameInfo filenameInfo = EventFilenameInfo.Companion.fromFile(file, config); + EventFilenameInfo filenameInfo = EventFilenameInfo.fromFile(file, config); if (filenameInfo.isLaunchCrashReport()) { launchCrashes.add(file); } @@ -163,7 +165,7 @@ class EventStore extends FileStore { private void flushEventFile(File eventFile) { try { - EventFilenameInfo eventInfo = EventFilenameInfo.Companion.fromFile(eventFile, config); + EventFilenameInfo eventInfo = EventFilenameInfo.fromFile(eventFile, config); String apiKey = eventInfo.getApiKey(); EventPayload payload = createEventPayload(eventFile, apiKey); @@ -188,9 +190,21 @@ class EventStore extends FileStore { logger.i("Deleting sent error file " + eventFile.getName()); break; case UNDELIVERED: - cancelQueuedFiles(Collections.singleton(eventFile)); - logger.w("Could not send previously saved error(s)" - + " to Bugsnag, will try again later"); + if (isTooBig(eventFile)) { + logger.w("Discarding over-sized event (" + + eventFile.length() + + ") after failed delivery"); + deleteStoredFiles(Collections.singleton(eventFile)); + } else if (isTooOld(eventFile)) { + logger.w("Discarding historical event (from " + + getCreationDate(eventFile) + + ") after failed delivery"); + deleteStoredFiles(Collections.singleton(eventFile)); + } else { + cancelQueuedFiles(Collections.singleton(eventFile)); + logger.w("Could not send previously saved error(s)" + + " to Bugsnag, will try again later"); + } break; case FAILURE: Exception exc = new RuntimeException("Failed to deliver event payload"); @@ -234,13 +248,29 @@ class EventStore extends FileStore { @Override String getFilename(Object object) { EventFilenameInfo eventInfo - = EventFilenameInfo.Companion.fromEvent(object, null, config); + = EventFilenameInfo.fromEvent(object, null, config); return eventInfo.encode(); } String getNdkFilename(Object object, String apiKey) { EventFilenameInfo eventInfo - = EventFilenameInfo.Companion.fromEvent(object, apiKey, config); + = EventFilenameInfo.fromEvent(object, apiKey, config); return eventInfo.encode(); } + + private static long oneMegabyte = 1024 * 1024; + + public boolean isTooBig(File file) { + return file.length() > oneMegabyte; + } + + public boolean isTooOld(File file) { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, -60); + return EventFilenameInfo.findTimestampInFilename(file) < cal.getTimeInMillis(); + } + + public Date getCreationDate(File file) { + return new Date(EventFilenameInfo.findTimestampInFilename(file)); + } } diff --git a/app/src/main/java/com/bugsnag/android/FileStore.java b/app/src/main/java/com/bugsnag/android/FileStore.java index cbafdac431..b613cea988 100644 --- a/app/src/main/java/com/bugsnag/android/FileStore.java +++ b/app/src/main/java/com/bugsnag/android/FileStore.java @@ -39,7 +39,7 @@ abstract class FileStore { private final Lock lock = new ReentrantLock(); private final Collection queuedFiles = new ConcurrentSkipListSet<>(); - private final Logger logger; + protected final Logger logger; private final EventStore.Delegate delegate; FileStore(@NonNull File storageDir, diff --git a/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt b/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt index 7f76a24271..ad166e35b8 100644 --- a/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt +++ b/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt @@ -37,6 +37,7 @@ internal class ManifestConfigLoader { private const val MAX_BREADCRUMBS = "$BUGSNAG_NS.MAX_BREADCRUMBS" private const val MAX_PERSISTED_EVENTS = "$BUGSNAG_NS.MAX_PERSISTED_EVENTS" private const val MAX_PERSISTED_SESSIONS = "$BUGSNAG_NS.MAX_PERSISTED_SESSIONS" + private const val MAX_REPORTED_THREADS = "$BUGSNAG_NS.MAX_REPORTED_THREADS" 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" @@ -77,6 +78,7 @@ internal class ManifestConfigLoader { maxBreadcrumbs = data.getInt(MAX_BREADCRUMBS, maxBreadcrumbs) maxPersistedEvents = data.getInt(MAX_PERSISTED_EVENTS, maxPersistedEvents) maxPersistedSessions = data.getInt(MAX_PERSISTED_SESSIONS, maxPersistedSessions) + maxReportedThreads = data.getInt(MAX_REPORTED_THREADS, maxReportedThreads) launchDurationMillis = data.getInt( LAUNCH_CRASH_THRESHOLD_MS, launchDurationMillis.toInt() diff --git a/app/src/main/java/com/bugsnag/android/NativeStackframe.kt b/app/src/main/java/com/bugsnag/android/NativeStackframe.kt index 5c47e1633c..3e82416a85 100644 --- a/app/src/main/java/com/bugsnag/android/NativeStackframe.kt +++ b/app/src/main/java/com/bugsnag/android/NativeStackframe.kt @@ -45,7 +45,12 @@ class NativeStackframe internal constructor( /** * The type of the error */ - var type: ErrorType? = null + var type: ErrorType? = null, + + /** + * Identifies the exact build this frame originates from. + */ + var codeIdentifier: String? = null, ) : JsonStream.Streamable { @Throws(IOException::class) @@ -57,6 +62,7 @@ class NativeStackframe internal constructor( writer.name("frameAddress").value(frameAddress) writer.name("symbolAddress").value(symbolAddress) writer.name("loadAddress").value(loadAddress) + writer.name("codeIdentifier").value(codeIdentifier) writer.name("isPC").value(isPC) type?.let { diff --git a/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt index b2f08dbab0..d131059fc1 100644 --- a/app/src/main/java/com/bugsnag/android/Notifier.kt +++ b/app/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "5.19.2", + var version: String = "5.23.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt b/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt new file mode 100644 index 0000000000..04efa4081b --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt @@ -0,0 +1,55 @@ +package com.bugsnag.android + +import java.io.File +import java.util.UUID + +/** + * Represents important information about a session filename. + * Currently the following information is encoded: + * + * uuid - to disambiguate stored error reports + * timestamp - to sort error reports by time of capture + */ +internal data class SessionFilenameInfo( + val timestamp: Long, + val uuid: String, +) { + + fun encode(): String { + return toFilename(timestamp, uuid) + } + + internal companion object { + + const val uuidLength = 36 + + /** + * Generates a filename for the session in the format + * "[UUID][timestamp]_v2.json" + */ + fun toFilename(timestamp: Long, uuid: String): String { + return "${uuid}${timestamp}_v2.json" + } + + @JvmStatic + fun defaultFilename(): String { + return toFilename(System.currentTimeMillis(), UUID.randomUUID().toString()) + } + + fun fromFile(file: File): SessionFilenameInfo { + return SessionFilenameInfo( + findTimestampInFilename(file), + findUuidInFilename(file) + ) + } + + private fun findUuidInFilename(file: File): String { + return file.name.substring(0, uuidLength - 1) + } + + @JvmStatic + fun findTimestampInFilename(file: File): Long { + return file.name.substring(uuidLength, file.name.indexOf("_")).toLongOrNull() ?: -1 + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt b/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt index b43f860813..2299fa87c9 100644 --- a/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt +++ b/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt @@ -2,17 +2,32 @@ package com.bugsnag.android import android.app.Activity import android.app.Application +import android.os.Build import android.os.Bundle internal class SessionLifecycleCallback( private val sessionTracker: SessionTracker ) : Application.ActivityLifecycleCallbacks { - override fun onActivityStarted(activity: Activity) = - sessionTracker.onActivityStarted(activity.javaClass.simpleName) + override fun onActivityStarted(activity: Activity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + sessionTracker.onActivityStarted(activity.javaClass.simpleName) + } + } - override fun onActivityStopped(activity: Activity) = + override fun onActivityPostStarted(activity: Activity) { + sessionTracker.onActivityStarted(activity.javaClass.simpleName) + } + + override fun onActivityStopped(activity: Activity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + sessionTracker.onActivityStopped(activity.javaClass.simpleName) + } + } + + override fun onActivityPostStopped(activity: Activity) { sessionTracker.onActivityStopped(activity.javaClass.simpleName) + } override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} override fun onActivityResumed(activity: Activity) {} diff --git a/app/src/main/java/com/bugsnag/android/SessionStore.java b/app/src/main/java/com/bugsnag/android/SessionStore.java index 0d84d8a677..a0238a5feb 100644 --- a/app/src/main/java/com/bugsnag/android/SessionStore.java +++ b/app/src/main/java/com/bugsnag/android/SessionStore.java @@ -6,7 +6,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.File; +import java.util.Calendar; import java.util.Comparator; +import java.util.Date; import java.util.UUID; /** @@ -46,7 +48,16 @@ class SessionStore extends FileStore { @NonNull @Override String getFilename(Object object) { - return UUID.randomUUID().toString() + System.currentTimeMillis() + "_v2.json"; + return SessionFilenameInfo.defaultFilename(); } + public boolean isTooOld(File file) { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, -60); + return SessionFilenameInfo.findTimestampInFilename(file) < cal.getTimeInMillis(); + } + + public Date getCreationDate(File file) { + return new Date(SessionFilenameInfo.findTimestampInFilename(file)); + } } diff --git a/app/src/main/java/com/bugsnag/android/SessionTracker.java b/app/src/main/java/com/bugsnag/android/SessionTracker.java index 2a5e95c7f1..ddb2f052d5 100644 --- a/app/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/app/src/main/java/com/bugsnag/android/SessionTracker.java @@ -270,8 +270,15 @@ class SessionTracker extends BaseObservable { logger.d("Sent 1 new session to Bugsnag"); break; case UNDELIVERED: - sessionStore.cancelQueuedFiles(Collections.singletonList(storedFile)); - logger.w("Leaving session payload for future delivery"); + if (sessionStore.isTooOld(storedFile)) { + logger.w("Discarding historical session (from {" + + sessionStore.getCreationDate(storedFile) + + "}) after failed delivery"); + sessionStore.deleteStoredFiles(Collections.singletonList(storedFile)); + } else { + sessionStore.cancelQueuedFiles(Collections.singletonList(storedFile)); + logger.w("Leaving session payload for future delivery"); + } break; case FAILURE: // drop bad data diff --git a/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt b/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt index e8af658a60..d664576791 100644 --- a/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt +++ b/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt @@ -6,12 +6,15 @@ import android.content.Context /** * Reads legacy information left in SharedPreferences and migrates it to the new location. */ -internal class SharedPrefMigrator(context: Context) { +internal class SharedPrefMigrator(context: Context) : DeviceIdPersistence { private val prefs = context .getSharedPreferences("com.bugsnag.android", Context.MODE_PRIVATE) - fun loadDeviceId() = prefs.getString(INSTALL_ID_KEY, null) + /** + * This implementation will never create an ID; it will only fetch one if present. + */ + override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean) = prefs.getString(INSTALL_ID_KEY, null) fun loadUser(deviceId: String?) = User( prefs.getString(USER_ID_KEY, deviceId), diff --git a/app/src/main/java/com/bugsnag/android/Stackframe.kt b/app/src/main/java/com/bugsnag/android/Stackframe.kt index 3e4e063a76..7ac08889bd 100644 --- a/app/src/main/java/com/bugsnag/android/Stackframe.kt +++ b/app/src/main/java/com/bugsnag/android/Stackframe.kt @@ -53,6 +53,11 @@ class Stackframe : JsonStream.Streamable { */ var loadAddress: Long? = null + /** + * Identifies the exact build this frame originates from. + */ + var codeIdentifier: String? = null + /** * Whether this frame identifies the program counter */ @@ -90,6 +95,7 @@ class Stackframe : JsonStream.Streamable { this.frameAddress = nativeFrame.frameAddress this.symbolAddress = nativeFrame.symbolAddress this.loadAddress = nativeFrame.loadAddress + this.codeIdentifier = nativeFrame.codeIdentifier this.isPC = nativeFrame.isPC this.type = nativeFrame.type } @@ -103,6 +109,7 @@ class Stackframe : JsonStream.Streamable { frameAddress = (json["frameAddress"] as? Number)?.toLong() symbolAddress = (json["symbolAddress"] as? Number)?.toLong() loadAddress = (json["loadAddress"] as? Number)?.toLong() + codeIdentifier = (json["codeIdentifier"] as? String) isPC = json["isPC"] as? Boolean @Suppress("UNCHECKED_CAST") @@ -124,6 +131,7 @@ class Stackframe : JsonStream.Streamable { frameAddress?.let { writer.name("frameAddress").value(it) } symbolAddress?.let { writer.name("symbolAddress").value(it) } loadAddress?.let { writer.name("loadAddress").value(it) } + codeIdentifier?.let { writer.name("codeIdentifier").value(it) } isPC?.let { writer.name("isPC").value(it) } type?.let { diff --git a/app/src/main/java/com/bugsnag/android/StorageModule.kt b/app/src/main/java/com/bugsnag/android/StorageModule.kt index 2d4bff14ee..caa419d7b9 100644 --- a/app/src/main/java/com/bugsnag/android/StorageModule.kt +++ b/app/src/main/java/com/bugsnag/android/StorageModule.kt @@ -25,6 +25,8 @@ internal class StorageModule( val deviceId by future { deviceIdStore.loadDeviceId() } + val internalDeviceId by future { deviceIdStore.loadInternalDeviceId() } + val userStore by future { UserStore( immutableConfig, diff --git a/app/src/main/java/com/bugsnag/android/Telemetry.kt b/app/src/main/java/com/bugsnag/android/Telemetry.kt new file mode 100644 index 0000000000..7d5a070a71 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/Telemetry.kt @@ -0,0 +1,16 @@ +package com.bugsnag.android + +/** + * Types of telemetry that may be sent to Bugsnag for product improvement purposes. + */ +enum class Telemetry { + + /** + * Errors within the Bugsnag SDK. + */ + INTERNAL_ERRORS; + + internal companion object { + fun fromString(str: String) = values().find { it.name == str } ?: INTERNAL_ERRORS + } +} diff --git a/app/src/main/java/com/bugsnag/android/ThreadState.kt b/app/src/main/java/com/bugsnag/android/ThreadState.kt index 06e00268ac..536ed89168 100644 --- a/app/src/main/java/com/bugsnag/android/ThreadState.kt +++ b/app/src/main/java/com/bugsnag/android/ThreadState.kt @@ -2,25 +2,27 @@ package com.bugsnag.android import com.bugsnag.android.internal.ImmutableConfig import java.io.IOException +import java.lang.Thread as JavaThread /** * Capture and serialize the state of all threads at the time of an exception. */ -internal class ThreadState @Suppress("LongParameterList") @JvmOverloads constructor( +internal class ThreadState @Suppress("LongParameterList") constructor( exc: Throwable?, isUnhandled: Boolean, + maxThreads: Int, sendThreads: ThreadSendPolicy, projectPackages: Collection, logger: Logger, - currentThread: java.lang.Thread? = null, - stackTraces: MutableMap>? = null + currentThread: JavaThread = JavaThread.currentThread(), + allThreads: List = allThreads() ) : JsonStream.Streamable { internal constructor( exc: Throwable?, isUnhandled: Boolean, config: ImmutableConfig - ) : this(exc, isUnhandled, config.sendThreads, config.projectPackages, config.logger) + ) : this(exc, isUnhandled, config.maxReportedThreads, config.sendThreads, config.projectPackages, config.logger) val threads: MutableList @@ -30,10 +32,11 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc threads = when { recordThreads -> captureThreadTrace( - stackTraces ?: java.lang.Thread.getAllStackTraces(), - currentThread ?: java.lang.Thread.currentThread(), + allThreads, + currentThread, exc, isUnhandled, + maxThreads, projectPackages, logger ) @@ -41,37 +44,88 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc } } + companion object { + private fun rootThreadGroup(): ThreadGroup { + var group = JavaThread.currentThread().threadGroup!! + + while (group.parent != null) { + group = group.parent + } + + return group + } + + internal fun allThreads(): List { + val rootGroup = rootThreadGroup() + val threadCount = rootGroup.activeCount() + val threads: Array = arrayOfNulls(threadCount) + rootGroup.enumerate(threads) + return threads.filterNotNull() + } + } + private fun captureThreadTrace( - stackTraces: MutableMap>, - currentThread: java.lang.Thread, + allThreads: List, + currentThread: JavaThread, exc: Throwable?, isUnhandled: Boolean, + maxThreadCount: Int, projectPackages: Collection, logger: Logger ): MutableList { - // API 24/25 don't record the currentThread, add it in manually - // https://issuetracker.google.com/issues/64122757 - if (!stackTraces.containsKey(currentThread)) { - stackTraces[currentThread] = currentThread.stackTrace - } - if (exc != null && isUnhandled) { // unhandled errors use the exception trace for thread traces - stackTraces[currentThread] = exc.stackTrace - } - - val currentThreadId = currentThread.id - return stackTraces.keys - .sortedBy { it.id } - .mapNotNull { thread -> - val trace = stackTraces[thread] - - if (trace != null) { - val stacktrace = Stacktrace(trace, projectPackages, logger) - val errorThread = thread.id == currentThreadId - Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, Thread.State.forThread(thread), stacktrace, logger) + fun toBugsnagThread(thread: JavaThread): Thread { + val isErrorThread = thread.id == currentThread.id + val stackTrace = Stacktrace( + if (isErrorThread) { + if (exc != null && isUnhandled) { // unhandled errors use the exception trace for thread traces + exc.stackTrace + } else { + currentThread.stackTrace + } } else { - null - } - }.toMutableList() + thread.stackTrace + }, + projectPackages, logger + ) + + return Thread( + thread.id, + thread.name, + ThreadType.ANDROID, + isErrorThread, + Thread.State.forThread(thread), + stackTrace, + logger + ) + } + + // Keep the lowest ID threads (ordered). Anything after maxThreadCount is lost. + // Note: We must ensure that currentThread is always present in the final list regardless. + val keepThreads = allThreads.sortedBy { it.id }.take(maxThreadCount) + + val reportThreads = if (keepThreads.contains(currentThread)) { + keepThreads + } else { + // API 24/25 don't record the currentThread, so add it in manually + // https://issuetracker.google.com/issues/64122757 + // currentThread may also have been removed if its ID occurred after maxThreadCount + keepThreads.take(Math.max(maxThreadCount - 1, 0)).plus(currentThread).sortedBy { it.id } + }.map { toBugsnagThread(it) }.toMutableList() + + if (allThreads.size > maxThreadCount) { + reportThreads.add( + Thread( + -1, + "[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]", + ThreadType.EMPTY, + false, + Thread.State.UNKNOWN, + Stacktrace(arrayOf(StackTraceElement("", "", "-", 0)), projectPackages, logger), + logger + ) + ) + } + return reportThreads } @Throws(IOException::class) diff --git a/app/src/main/java/com/bugsnag/android/ThreadType.kt b/app/src/main/java/com/bugsnag/android/ThreadType.kt index c1c3cbb5d7..60f834741c 100644 --- a/app/src/main/java/com/bugsnag/android/ThreadType.kt +++ b/app/src/main/java/com/bugsnag/android/ThreadType.kt @@ -5,6 +5,11 @@ package com.bugsnag.android */ enum class ThreadType(internal val desc: String) { + /** + * A thread captured from Android's JVM layer + */ + EMPTY(""), + /** * A thread captured from Android's JVM layer */ diff --git a/app/src/main/java/com/bugsnag/android/internal/BugsnagMapper.kt b/app/src/main/java/com/bugsnag/android/internal/BugsnagMapper.kt new file mode 100644 index 0000000000..c004241bc7 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/BugsnagMapper.kt @@ -0,0 +1,44 @@ +package com.bugsnag.android.internal + +import com.bugsnag.android.BugsnagEventMapper +import com.bugsnag.android.Event +import com.bugsnag.android.JsonStream +import com.bugsnag.android.Logger +import java.io.ByteArrayOutputStream +import com.bugsnag.android.Error as BugsnagError + +class BugsnagMapper(logger: Logger) { + private val eventMapper = BugsnagEventMapper(logger) + + /** + * Convert the given `Map` of data to an `Event` object + */ + fun convertToEvent(data: Map, fallbackApiKey: String): Event { + return eventMapper.convertToEvent(data, fallbackApiKey) + } + + /** + * Convert the given `Map` of data to an `Error` object + */ + fun convertToError(data: Map): BugsnagError { + return eventMapper.convertError(data) + } + + /** + * Convert a given `Event` object to a `Map` + */ + fun convertToMap(event: Event): Map { + val byteStream = ByteArrayOutputStream() + byteStream.writer().use { writer -> JsonStream(writer).value(event) } + return JsonHelper.deserialize(byteStream.toByteArray()) + } + + /** + * Convert a given `Error` object to a `Map` + */ + fun convertToMap(error: BugsnagError): Map { + val byteStream = ByteArrayOutputStream() + byteStream.writer().use { writer -> JsonStream(writer).value(error) } + return JsonHelper.deserialize(byteStream.toByteArray()) + } +} diff --git a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt index 9ace0f6686..06558cfca2 100644 --- a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt +++ b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt @@ -18,6 +18,7 @@ import com.bugsnag.android.EventPayload import com.bugsnag.android.Logger import com.bugsnag.android.ManifestConfigLoader.Companion.BUILD_UUID import com.bugsnag.android.NoopLogger +import com.bugsnag.android.Telemetry import com.bugsnag.android.ThreadSendPolicy import com.bugsnag.android.errorApiHeaders import com.bugsnag.android.safeUnrollCauses @@ -34,6 +35,7 @@ data class ImmutableConfig( val enabledReleaseStages: Collection?, val projectPackages: Collection, val enabledBreadcrumbTypes: Set?, + val telemetry: Set, val releaseStage: String?, val buildUuid: String?, val appVersion: String?, @@ -47,6 +49,7 @@ data class ImmutableConfig( val maxBreadcrumbs: Int, val maxPersistedEvents: Int, val maxPersistedSessions: Int, + val maxReportedThreads: Int, val persistenceDirectory: Lazy, val sendLaunchCrashesSynchronously: Boolean, @@ -159,7 +162,9 @@ internal fun convertToImmutableConfig( maxBreadcrumbs = config.maxBreadcrumbs, maxPersistedEvents = config.maxPersistedEvents, maxPersistedSessions = config.maxPersistedSessions, + maxReportedThreads = config.maxReportedThreads, enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), + telemetry = config.telemetry.toSet(), persistenceDirectory = persistenceDir, sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously, packageInfo = packageInfo, diff --git a/app/src/main/java/eu/faircode/email/Log.java b/app/src/main/java/eu/faircode/email/Log.java index f115c327f0..2eebf4762f 100644 --- a/app/src/main/java/eu/faircode/email/Log.java +++ b/app/src/main/java/eu/faircode/email/Log.java @@ -92,6 +92,7 @@ import com.bugsnag.android.OnErrorCallback; import com.bugsnag.android.OnSessionCallback; import com.bugsnag.android.Session; import com.bugsnag.android.Severity; +import com.bugsnag.android.Telemetry; import com.sun.mail.iap.BadCommandException; import com.sun.mail.iap.ConnectionException; import com.sun.mail.iap.ProtocolException; @@ -370,6 +371,7 @@ public class Log { // https://docs.bugsnag.com/platforms/android/sdk/ com.bugsnag.android.Configuration config = new com.bugsnag.android.Configuration("9d2d57476a0614974449a3ec33f2604a"); + config.setTelemetry(Collections.emptySet()); if (BuildConfig.DEBUG) config.setReleaseStage("debug"); diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch index c762d488a1..a45f6efb2a 100644 --- a/patches/Bugsnag.patch +++ b/patches/Bugsnag.patch @@ -1,5 +1,5 @@ diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt -index 0ce2eec8c..e1bac196e 100644 +index 0ce2eec8c4..e1bac196e2 100644 --- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt @@ -66,7 +66,7 @@ internal class DefaultDelivery( @@ -12,24 +12,15 @@ index 0ce2eec8c..e1bac196e 100644 logger.w("Unexpected error delivering payload", exception) return DeliveryStatus.FAILURE diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch -index 25c19fd4c..e69de29bb 100644 +index c762d488a1..e69de29bb2 100644 --- a/patches/Bugsnag.patch +++ b/patches/Bugsnag.patch -@@ -1,22 +0,0 @@ --From 3270faf44aea11754c940ba43ee6db72b7462f14 Mon Sep 17 00:00:00 2001 --From: M66B --Date: Sat, 15 May 2021 22:07:24 +0200 --Subject: [PATCH] Bugsnag failure on I/O error -- ----- -- app/src/main/java/com/bugsnag/android/DefaultDelivery.kt | 2 +- -- 1 file changed, 1 insertion(+), 1 deletion(-) -- +@@ -1,40 +0,0 @@ -diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt --index a7995164cb4e..5620f0bacd80 100644 +-index 0ce2eec8c..e1bac196e 100644 ---- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt -+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt --@@ -64,7 +64,7 @@ internal class DefaultDelivery( +-@@ -66,7 +66,7 @@ internal class DefaultDelivery( - return DeliveryStatus.UNDELIVERED - } catch (exception: IOException) { - logger.w("IOException encountered in request", exception) @@ -38,3 +29,30 @@ index 25c19fd4c..e69de29bb 100644 - } catch (exception: Exception) { - logger.w("Unexpected error delivering payload", exception) - return DeliveryStatus.FAILURE +-diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch +-index 25c19fd4c..e69de29bb 100644 +---- a/patches/Bugsnag.patch +-+++ b/patches/Bugsnag.patch +-@@ -1,22 +0,0 @@ +--From 3270faf44aea11754c940ba43ee6db72b7462f14 Mon Sep 17 00:00:00 2001 +--From: M66B +--Date: Sat, 15 May 2021 22:07:24 +0200 +--Subject: [PATCH] Bugsnag failure on I/O error +-- +----- +-- app/src/main/java/com/bugsnag/android/DefaultDelivery.kt | 2 +- +-- 1 file changed, 1 insertion(+), 1 deletion(-) +-- +--diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +--index a7995164cb4e..5620f0bacd80 100644 +----- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +--+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +--@@ -64,7 +64,7 @@ internal class DefaultDelivery( +-- return DeliveryStatus.UNDELIVERED +-- } catch (exception: IOException) { +-- logger.w("IOException encountered in request", exception) +--- return DeliveryStatus.UNDELIVERED +--+ return DeliveryStatus.FAILURE +-- } catch (exception: Exception) { +-- logger.w("Unexpected error delivering payload", exception) +-- return DeliveryStatus.FAILURE