mirror of
https://github.com/M66B/FairEmail.git
synced 2024-12-21 23:32:51 +00:00
Updated Bugsnag to version 5.23.0
This commit is contained in:
parent
dd8bd36712
commit
f5604d6ede
35 changed files with 737 additions and 276 deletions
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
163
app/src/main/java/com/bugsnag/android/DeviceIdFilePersistence.kt
Normal file
163
app/src/main/java/com/bugsnag/android/DeviceIdFilePersistence.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
14
app/src/main/java/com/bugsnag/android/DeviceIdPersistence.kt
Normal file
14
app/src/main/java/com/bugsnag/android/DeviceIdPersistence.kt
Normal 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?
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
55
app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt
Normal file
55
app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
16
app/src/main/java/com/bugsnag/android/Telemetry.kt
Normal file
16
app/src/main/java/com/bugsnag/android/Telemetry.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue