Updated Bugsnag to version 5.23.0

This commit is contained in:
M66B 2022-06-23 20:23:15 +02:00
parent dd8bd36712
commit f5604d6ede
35 changed files with 737 additions and 276 deletions

View File

@ -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"

View File

@ -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()
}

View File

@ -69,6 +69,15 @@ public final class Bugsnag {
return client;
}
/**
* Returns true if one of the <code>start</code> methods have been has been called and
* so Bugsnag is initialized; false if <code>start</code> 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.
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* You can use this to add or modify information attached to an Event
* before it is sent to your dashboard. You can also return
* <code>false</code> 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.
*
* <p>
* For example:
*
* <p>
* 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.
*
* <p>
* You can use this to modify breadcrumbs before they are stored by Bugsnag.
* You can also return <code>false</code> from any callback to ignore a breadcrumb.
*
* <p>
* For example:
*
* <p>
* 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.
*
* <p>
* You can use this to modify sessions before they are stored by Bugsnag.
* You can also return <code>false</code> from any callback to ignore a session.
*
* <p>
* For example:
*
* <p>
* 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 {
* <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score">
* stability score</a>.
*
* @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()}.
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* 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;

View File

@ -11,6 +11,10 @@ internal class BugsnagEventMapper(
private val logger: Logger
) {
internal fun convertToEvent(map: Map<in String, Any?>, apiKey: String): Event {
return Event(convertToEventImpl(map, apiKey), logger)
}
@Suppress("UNCHECKED_CAST")
internal fun convertToEventImpl(map: Map<in String, Any?>, apiKey: String): EventInternal {
val event = EventInternal(apiKey)
@ -85,7 +89,11 @@ internal class BugsnagEventMapper(
return event
}
internal fun convertErrorInternal(error: Map<String, Any?>): ErrorInternal {
internal fun convertError(error: Map<in String, Any?>): Error {
return Error(convertErrorInternal(error), logger)
}
internal fun convertErrorInternal(error: Map<in String, Any?>): ErrorInternal {
return ErrorInternal(
error.readEntry("errorClass"),
error["message"] as? String,

View File

@ -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();

View File

@ -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<String>
@ -51,6 +53,7 @@ internal class ConfigInternal(
var discardClasses: Set<String> = emptySet()
var enabledReleaseStages: Set<String>? = null
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null
var telemetry: Set<Telemetry> = EnumSet.of(Telemetry.INTERNAL_ERRORS)
var projectPackages: Set<String> = 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

View File

@ -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<Telemetry> 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> 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

View File

@ -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)

View File

@ -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,

View File

@ -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<String, Any>
private var runtimeVersions: MutableMap<String, Any>
private val rootedFuture: Future<Boolean>?
private val totalMemoryFuture: Future<Long?>? = 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<String, Any?> {
val map = HashMap<String, Any?>()
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
}
}

View File

@ -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<DeviceId>
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<DeviceId> {
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)
}
}
}

View File

@ -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?
}

View File

@ -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<DeviceId>
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<DeviceId> {
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)
}
}

View File

@ -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 }
}
}

View File

@ -22,12 +22,8 @@ internal data class EventFilenameInfo(
val errorTypes: Set<ErrorType>
) {
/**
* 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<ErrorType>
): 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<ErrorType> {
internal fun findErrorTypesInFilename(eventFile: File): Set<ErrorType> {
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<ErrorType> {
internal fun findErrorTypesForEvent(obj: Any): Set<ErrorType> {
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

View File

@ -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) }

View File

@ -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<File> 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));
}
}

View File

@ -39,7 +39,7 @@ abstract class FileStore {
private final Lock lock = new ReentrantLock();
private final Collection<File> queuedFiles = new ConcurrentSkipListSet<>();
private final Logger logger;
protected final Logger logger;
private final EventStore.Delegate delegate;
FileStore(@NonNull File storageDir,

View File

@ -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()

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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) {}

View File

@ -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));
}
}

View File

@ -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

View File

@ -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),

View File

@ -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 {

View File

@ -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,

View File

@ -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
}
}

View File

@ -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<String>,
logger: Logger,
currentThread: java.lang.Thread? = null,
stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>>? = null
currentThread: JavaThread = JavaThread.currentThread(),
allThreads: List<JavaThread> = 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<Thread>
@ -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<JavaThread> {
val rootGroup = rootThreadGroup()
val threadCount = rootGroup.activeCount()
val threads: Array<JavaThread?> = arrayOfNulls(threadCount)
rootGroup.enumerate(threads)
return threads.filterNotNull()
}
}
private fun captureThreadTrace(
stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>>,
currentThread: java.lang.Thread,
allThreads: List<JavaThread>,
currentThread: JavaThread,
exc: Throwable?,
isUnhandled: Boolean,
maxThreadCount: Int,
projectPackages: Collection<String>,
logger: Logger
): MutableList<Thread> {
// 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)

View File

@ -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
*/

View File

@ -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<in String, Any?>, fallbackApiKey: String): Event {
return eventMapper.convertToEvent(data, fallbackApiKey)
}
/**
* Convert the given `Map` of data to an `Error` object
*/
fun convertToError(data: Map<in String, Any?>): BugsnagError {
return eventMapper.convertError(data)
}
/**
* Convert a given `Event` object to a `Map<String, Any?>`
*/
fun convertToMap(event: Event): Map<in String, Any?> {
val byteStream = ByteArrayOutputStream()
byteStream.writer().use { writer -> JsonStream(writer).value(event) }
return JsonHelper.deserialize(byteStream.toByteArray())
}
/**
* Convert a given `Error` object to a `Map<String, Any?>`
*/
fun convertToMap(error: BugsnagError): Map<in String, Any?> {
val byteStream = ByteArrayOutputStream()
byteStream.writer().use { writer -> JsonStream(writer).value(error) }
return JsonHelper.deserialize(byteStream.toByteArray())
}
}

View File

@ -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<String>?,
val projectPackages: Collection<String>,
val enabledBreadcrumbTypes: Set<BreadcrumbType>?,
val telemetry: Set<Telemetry>,
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<File>,
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,

View File

@ -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");

View File

@ -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 <M66B@users.noreply.github.com>
-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 <M66B@users.noreply.github.com>
--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