mirror of https://github.com/M66B/FairEmail.git
Updated Bugsnag
This commit is contained in:
parent
1da91952f6
commit
084d0e1536
|
@ -380,7 +380,7 @@ dependencies {
|
||||||
def dnsjava_version = "2.1.9"
|
def dnsjava_version = "2.1.9"
|
||||||
def openpgp_version = "12.0"
|
def openpgp_version = "12.0"
|
||||||
def badge_version = "1.1.22"
|
def badge_version = "1.1.22"
|
||||||
def bugsnag_version = "5.23.0"
|
def bugsnag_version = "5.28.2"
|
||||||
def biweekly_version = "0.6.6"
|
def biweekly_version = "0.6.6"
|
||||||
def vcard_version = "0.11.3"
|
def vcard_version = "0.11.3"
|
||||||
def relinker_version = "1.4.5"
|
def relinker_version = "1.4.5"
|
||||||
|
|
|
@ -3,13 +3,16 @@ package com.bugsnag.android
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import java.util.concurrent.BlockingQueue
|
import java.util.concurrent.BlockingQueue
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
|
import java.util.concurrent.FutureTask
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
import java.util.concurrent.RejectedExecutionException
|
import java.util.concurrent.RejectedExecutionException
|
||||||
import java.util.concurrent.ThreadFactory
|
import java.util.concurrent.ThreadFactory
|
||||||
import java.util.concurrent.ThreadPoolExecutor
|
import java.util.concurrent.ThreadPoolExecutor
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.lang.Thread as JThread
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of task which is being submitted. This determines which execution queue
|
* The type of task which is being submitted. This determines which execution queue
|
||||||
|
@ -55,9 +58,14 @@ private const val THREAD_POOL_SIZE = 1
|
||||||
private const val KEEP_ALIVE_SECS = 30L
|
private const val KEEP_ALIVE_SECS = 30L
|
||||||
private const val TASK_QUEUE_SIZE = 128
|
private const val TASK_QUEUE_SIZE = 128
|
||||||
|
|
||||||
internal fun createExecutor(name: String, keepAlive: Boolean): ThreadPoolExecutor {
|
private class TaskTypeThread(runnable: Runnable, name: String, val taskType: TaskType) :
|
||||||
|
JThread(runnable, name)
|
||||||
|
|
||||||
|
internal val JThread.taskType get() = (this as? TaskTypeThread)?.taskType
|
||||||
|
|
||||||
|
internal fun createExecutor(name: String, type: TaskType, keepAlive: Boolean): ExecutorService {
|
||||||
val queue: BlockingQueue<Runnable> = LinkedBlockingQueue(TASK_QUEUE_SIZE)
|
val queue: BlockingQueue<Runnable> = LinkedBlockingQueue(TASK_QUEUE_SIZE)
|
||||||
val threadFactory = ThreadFactory { Thread(it, name) }
|
val threadFactory = ThreadFactory { TaskTypeThread(it, name, type) }
|
||||||
|
|
||||||
// certain executors (error/session/io) should always keep their threads alive, but others
|
// certain executors (error/session/io) should always keep their threads alive, but others
|
||||||
// are less important so are allowed a pool size of 0 that expands on demand.
|
// are less important so are allowed a pool size of 0 that expands on demand.
|
||||||
|
@ -86,33 +94,38 @@ internal fun createExecutor(name: String, keepAlive: Boolean): ThreadPoolExecuto
|
||||||
internal class BackgroundTaskService(
|
internal class BackgroundTaskService(
|
||||||
// these executors must remain single-threaded - the SDK makes assumptions
|
// these executors must remain single-threaded - the SDK makes assumptions
|
||||||
// about synchronization based on this.
|
// about synchronization based on this.
|
||||||
@VisibleForTesting
|
@get:VisibleForTesting
|
||||||
internal val errorExecutor: ThreadPoolExecutor = createExecutor(
|
internal val errorExecutor: ExecutorService = createExecutor(
|
||||||
"Bugsnag Error thread",
|
"Bugsnag Error thread",
|
||||||
|
TaskType.ERROR_REQUEST,
|
||||||
true
|
true
|
||||||
),
|
),
|
||||||
|
|
||||||
@VisibleForTesting
|
@get:VisibleForTesting
|
||||||
internal val sessionExecutor: ThreadPoolExecutor = createExecutor(
|
internal val sessionExecutor: ExecutorService = createExecutor(
|
||||||
"Bugsnag Session thread",
|
"Bugsnag Session thread",
|
||||||
|
TaskType.SESSION_REQUEST,
|
||||||
true
|
true
|
||||||
),
|
),
|
||||||
|
|
||||||
@VisibleForTesting
|
@get:VisibleForTesting
|
||||||
internal val ioExecutor: ThreadPoolExecutor = createExecutor(
|
internal val ioExecutor: ExecutorService = createExecutor(
|
||||||
"Bugsnag IO thread",
|
"Bugsnag IO thread",
|
||||||
|
TaskType.IO,
|
||||||
true
|
true
|
||||||
),
|
),
|
||||||
|
|
||||||
@VisibleForTesting
|
@get:VisibleForTesting
|
||||||
internal val internalReportExecutor: ThreadPoolExecutor = createExecutor(
|
internal val internalReportExecutor: ExecutorService = createExecutor(
|
||||||
"Bugsnag Internal Report thread",
|
"Bugsnag Internal Report thread",
|
||||||
|
TaskType.INTERNAL_REPORT,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
|
||||||
@VisibleForTesting
|
@get:VisibleForTesting
|
||||||
internal val defaultExecutor: ThreadPoolExecutor = createExecutor(
|
internal val defaultExecutor: ExecutorService = createExecutor(
|
||||||
"Bugsnag Default thread",
|
"Bugsnag Default thread",
|
||||||
|
TaskType.DEFAULT,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -138,13 +151,17 @@ internal class BackgroundTaskService(
|
||||||
*/
|
*/
|
||||||
@Throws(RejectedExecutionException::class)
|
@Throws(RejectedExecutionException::class)
|
||||||
fun <T> submitTask(taskType: TaskType, callable: Callable<T>): Future<T> {
|
fun <T> submitTask(taskType: TaskType, callable: Callable<T>): Future<T> {
|
||||||
return when (taskType) {
|
val task = FutureTask(callable)
|
||||||
TaskType.ERROR_REQUEST -> errorExecutor.submit(callable)
|
|
||||||
TaskType.SESSION_REQUEST -> sessionExecutor.submit(callable)
|
when (taskType) {
|
||||||
TaskType.IO -> ioExecutor.submit(callable)
|
TaskType.ERROR_REQUEST -> errorExecutor.execute(task)
|
||||||
TaskType.INTERNAL_REPORT -> internalReportExecutor.submit(callable)
|
TaskType.SESSION_REQUEST -> sessionExecutor.execute(task)
|
||||||
TaskType.DEFAULT -> defaultExecutor.submit(callable)
|
TaskType.IO -> ioExecutor.execute(task)
|
||||||
|
TaskType.INTERNAL_REPORT -> internalReportExecutor.execute(task)
|
||||||
|
TaskType.DEFAULT -> defaultExecutor.execute(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return SafeFuture(task, taskType)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,11 +185,34 @@ internal class BackgroundTaskService(
|
||||||
ioExecutor.awaitTerminationSafe()
|
ioExecutor.awaitTerminationSafe()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ThreadPoolExecutor.awaitTerminationSafe() {
|
private fun ExecutorService.awaitTerminationSafe() {
|
||||||
try {
|
try {
|
||||||
awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)
|
awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)
|
||||||
} catch (ignored: InterruptedException) {
|
} catch (ignored: InterruptedException) {
|
||||||
// ignore interrupted exception as the JVM is shutting down
|
// ignore interrupted exception as the JVM is shutting down
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SafeFuture<V>(
|
||||||
|
private val delegate: FutureTask<V>,
|
||||||
|
private val taskType: TaskType
|
||||||
|
) : Future<V> by delegate {
|
||||||
|
override fun get(): V {
|
||||||
|
ensureTaskGetSafe()
|
||||||
|
return delegate.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(timeout: Long, unit: TimeUnit?): V {
|
||||||
|
ensureTaskGetSafe()
|
||||||
|
return delegate.get(timeout, unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureTaskGetSafe() {
|
||||||
|
if (!delegate.isDone && JThread.currentThread().taskType == taskType) {
|
||||||
|
// if this is the execution queue for the wrapped FutureTask && it is not yet 'done'
|
||||||
|
// then it has not yet been started, so we run it immediately
|
||||||
|
delegate.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import com.bugsnag.android.internal.StringUtils
|
||||||
|
import com.bugsnag.android.internal.TrimMetrics
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -22,6 +24,11 @@ internal class BreadcrumbInternal internal constructor(
|
||||||
Date()
|
Date()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun trimMetadataStringsTo(maxStringLength: Int): TrimMetrics {
|
||||||
|
val metadata = this.metadata ?: return TrimMetrics(0, 0)
|
||||||
|
return StringUtils.trimStringValuesTo(maxStringLength, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun toStream(writer: JsonStream) {
|
override fun toStream(writer: JsonStream) {
|
||||||
writer.beginObject()
|
writer.beginObject()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
import com.bugsnag.android.internal.DateUtils
|
import com.bugsnag.android.internal.DateUtils
|
||||||
|
import com.bugsnag.android.internal.InternalMetricsImpl
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
@ -17,7 +18,7 @@ internal class BugsnagEventMapper(
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
internal fun convertToEventImpl(map: Map<in String, Any?>, apiKey: String): EventInternal {
|
internal fun convertToEventImpl(map: Map<in String, Any?>, apiKey: String): EventInternal {
|
||||||
val event = EventInternal(apiKey)
|
val event = EventInternal(apiKey, logger)
|
||||||
|
|
||||||
// populate exceptions. check this early to avoid unnecessary serialization if
|
// populate exceptions. check this early to avoid unnecessary serialization if
|
||||||
// no stacktrace was gathered.
|
// no stacktrace was gathered.
|
||||||
|
@ -86,6 +87,9 @@ internal class BugsnagEventMapper(
|
||||||
event.updateSeverityReasonInternal(reason)
|
event.updateSeverityReasonInternal(reason)
|
||||||
event.normalizeStackframeErrorTypes()
|
event.normalizeStackframeErrorTypes()
|
||||||
|
|
||||||
|
// populate internalMetrics
|
||||||
|
event.internalMetrics = InternalMetricsImpl(map["usage"] as MutableMap<String, Any>?)
|
||||||
|
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,31 +188,7 @@ internal class BugsnagEventMapper(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun convertStacktrace(trace: List<Map<String, Any?>>): Stacktrace {
|
internal fun convertStacktrace(trace: List<Map<String, Any?>>): Stacktrace {
|
||||||
return Stacktrace(trace.map { convertStackframe(it) })
|
return Stacktrace(trace.map { Stackframe(it) })
|
||||||
}
|
|
||||||
|
|
||||||
internal fun convertStackframe(frame: Map<String, Any?>): Stackframe {
|
|
||||||
val copy: MutableMap<String, Any?> = frame.toMutableMap()
|
|
||||||
val lineNumber = frame["lineNumber"] as? Number
|
|
||||||
copy["lineNumber"] = lineNumber?.toLong()
|
|
||||||
|
|
||||||
(frame["frameAddress"] as? String)?.let {
|
|
||||||
copy["frameAddress"] = java.lang.Long.decode(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
(frame["symbolAddress"] as? String)?.let {
|
|
||||||
copy["symbolAddress"] = java.lang.Long.decode(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
(frame["loadAddress"] as? String)?.let {
|
|
||||||
copy["loadAddress"] = java.lang.Long.decode(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
(frame["isPC"] as? Boolean)?.let {
|
|
||||||
copy["isPC"] = it
|
|
||||||
}
|
|
||||||
|
|
||||||
return Stackframe(copy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun deserializeSeverityReason(
|
internal fun deserializeSeverityReason(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
import com.bugsnag.android.internal.dag.ConfigModule
|
import com.bugsnag.android.internal.ImmutableConfig
|
||||||
import com.bugsnag.android.internal.dag.DependencyModule
|
import com.bugsnag.android.internal.dag.DependencyModule
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,15 +8,13 @@ import com.bugsnag.android.internal.dag.DependencyModule
|
||||||
* class is responsible for creating classes which track the current breadcrumb/metadata state.
|
* class is responsible for creating classes which track the current breadcrumb/metadata state.
|
||||||
*/
|
*/
|
||||||
internal class BugsnagStateModule(
|
internal class BugsnagStateModule(
|
||||||
configModule: ConfigModule,
|
cfg: ImmutableConfig,
|
||||||
configuration: Configuration
|
configuration: Configuration
|
||||||
) : DependencyModule() {
|
) : DependencyModule() {
|
||||||
|
|
||||||
private val cfg = configModule.config
|
|
||||||
|
|
||||||
val clientObservable = ClientObservable()
|
val clientObservable = ClientObservable()
|
||||||
|
|
||||||
val callbackState = configuration.impl.callbackState.copy()
|
val callbackState = configuration.impl.callbackState
|
||||||
|
|
||||||
val contextState = ContextState().apply {
|
val contextState = ContextState().apply {
|
||||||
if (configuration.context != null) {
|
if (configuration.context != null) {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import com.bugsnag.android.internal.InternalMetrics
|
||||||
|
import com.bugsnag.android.internal.InternalMetricsNoop
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
internal data class CallbackState(
|
internal data class CallbackState(
|
||||||
|
@ -9,36 +11,66 @@ internal data class CallbackState(
|
||||||
val onSendTasks: MutableCollection<OnSendCallback> = CopyOnWriteArrayList()
|
val onSendTasks: MutableCollection<OnSendCallback> = CopyOnWriteArrayList()
|
||||||
) : CallbackAware {
|
) : CallbackAware {
|
||||||
|
|
||||||
|
private var internalMetrics: InternalMetrics = InternalMetricsNoop()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val onBreadcrumbName = "onBreadcrumb"
|
||||||
|
private const val onErrorName = "onError"
|
||||||
|
private const val onSendName = "onSendError"
|
||||||
|
private const val onSessionName = "onSession"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInternalMetrics(metrics: InternalMetrics) {
|
||||||
|
internalMetrics = metrics
|
||||||
|
internalMetrics.setCallbackCounts(getCallbackCounts())
|
||||||
|
}
|
||||||
|
|
||||||
override fun addOnError(onError: OnErrorCallback) {
|
override fun addOnError(onError: OnErrorCallback) {
|
||||||
onErrorTasks.add(onError)
|
if (onErrorTasks.add(onError)) {
|
||||||
|
internalMetrics.notifyAddCallback(onErrorName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeOnError(onError: OnErrorCallback) {
|
override fun removeOnError(onError: OnErrorCallback) {
|
||||||
onErrorTasks.remove(onError)
|
if (onErrorTasks.remove(onError)) {
|
||||||
|
internalMetrics.notifyRemoveCallback(onErrorName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
|
override fun addOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
|
||||||
onBreadcrumbTasks.add(onBreadcrumb)
|
if (onBreadcrumbTasks.add(onBreadcrumb)) {
|
||||||
|
internalMetrics.notifyAddCallback(onBreadcrumbName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
|
override fun removeOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
|
||||||
onBreadcrumbTasks.remove(onBreadcrumb)
|
if (onBreadcrumbTasks.remove(onBreadcrumb)) {
|
||||||
|
internalMetrics.notifyRemoveCallback(onBreadcrumbName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addOnSession(onSession: OnSessionCallback) {
|
override fun addOnSession(onSession: OnSessionCallback) {
|
||||||
onSessionTasks.add(onSession)
|
if (onSessionTasks.add(onSession)) {
|
||||||
|
internalMetrics.notifyAddCallback(onSessionName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeOnSession(onSession: OnSessionCallback) {
|
override fun removeOnSession(onSession: OnSessionCallback) {
|
||||||
onSessionTasks.remove(onSession)
|
if (onSessionTasks.remove(onSession)) {
|
||||||
|
internalMetrics.notifyRemoveCallback(onSessionName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addOnSend(onSend: OnSendCallback) {
|
fun addOnSend(onSend: OnSendCallback) {
|
||||||
onSendTasks.add(onSend)
|
if (onSendTasks.add(onSend)) {
|
||||||
|
internalMetrics.notifyAddCallback(onSendName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeOnSend(onSend: OnSendCallback) {
|
fun removeOnSend(onSend: OnSendCallback) {
|
||||||
onSendTasks.remove(onSend)
|
if (onSendTasks.remove(onSend)) {
|
||||||
|
internalMetrics.notifyRemoveCallback(onSendName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun runOnErrorTasks(event: Event, logger: Logger): Boolean {
|
fun runOnErrorTasks(event: Event, logger: Logger): Boolean {
|
||||||
|
@ -120,4 +152,13 @@ internal data class CallbackState(
|
||||||
onSessionTasks = onSessionTasks,
|
onSessionTasks = onSessionTasks,
|
||||||
onSendTasks = onSendTasks
|
onSendTasks = onSendTasks
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun getCallbackCounts(): Map<String, Int> {
|
||||||
|
return hashMapOf<String, Int>().also { map ->
|
||||||
|
if (onBreadcrumbTasks.count() > 0) map[onBreadcrumbName] = onBreadcrumbTasks.count()
|
||||||
|
if (onErrorTasks.count() > 0) map[onErrorName] = onErrorTasks.count()
|
||||||
|
if (onSendTasks.count() > 0) map[onSendName] = onSendTasks.count()
|
||||||
|
if (onSessionTasks.count() > 0) map[onSessionName] = onSessionTasks.count()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@ package com.bugsnag.android;
|
||||||
import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION;
|
import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION;
|
||||||
|
|
||||||
import com.bugsnag.android.internal.ImmutableConfig;
|
import com.bugsnag.android.internal.ImmutableConfig;
|
||||||
|
import com.bugsnag.android.internal.InternalMetrics;
|
||||||
|
import com.bugsnag.android.internal.InternalMetricsImpl;
|
||||||
|
import com.bugsnag.android.internal.InternalMetricsNoop;
|
||||||
import com.bugsnag.android.internal.StateObserver;
|
import com.bugsnag.android.internal.StateObserver;
|
||||||
import com.bugsnag.android.internal.dag.ConfigModule;
|
import com.bugsnag.android.internal.dag.ConfigModule;
|
||||||
import com.bugsnag.android.internal.dag.ContextModule;
|
import com.bugsnag.android.internal.dag.ContextModule;
|
||||||
|
@ -16,7 +19,6 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import kotlin.jvm.functions.Function1;
|
|
||||||
import kotlin.jvm.functions.Function2;
|
import kotlin.jvm.functions.Function2;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -49,9 +51,11 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
final MetadataState metadataState;
|
final MetadataState metadataState;
|
||||||
final FeatureFlagState featureFlagState;
|
final FeatureFlagState featureFlagState;
|
||||||
|
|
||||||
|
private final InternalMetrics internalMetrics;
|
||||||
private final ContextState contextState;
|
private final ContextState contextState;
|
||||||
private final CallbackState callbackState;
|
private final CallbackState callbackState;
|
||||||
private final UserState userState;
|
private final UserState userState;
|
||||||
|
private final Map<String, Object> configDifferences;
|
||||||
|
|
||||||
final Context appContext;
|
final Context appContext;
|
||||||
|
|
||||||
|
@ -140,7 +144,18 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
ConfigModule configModule = new ConfigModule(contextModule, configuration, connectivity);
|
ConfigModule configModule = new ConfigModule(contextModule, configuration, connectivity);
|
||||||
immutableConfig = configModule.getConfig();
|
immutableConfig = configModule.getConfig();
|
||||||
logger = immutableConfig.getLogger();
|
logger = immutableConfig.getLogger();
|
||||||
warnIfNotAppContext(androidContext);
|
|
||||||
|
if (!(androidContext instanceof Application)) {
|
||||||
|
logger.w("You should initialize Bugsnag from the onCreate() callback of your "
|
||||||
|
+ "Application subclass, as this guarantees errors are captured as early "
|
||||||
|
+ "as possible. "
|
||||||
|
+ "If a custom Application subclass is not possible in your app then you "
|
||||||
|
+ "should suppress this warning by passing the Application context instead: "
|
||||||
|
+ "Bugsnag.start(context.getApplicationContext()). "
|
||||||
|
+ "For further info see: "
|
||||||
|
+ "https://docs.bugsnag.com/platforms/android/#basic-configuration");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// setup storage as soon as possible
|
// setup storage as soon as possible
|
||||||
final StorageModule storageModule = new StorageModule(appContext,
|
final StorageModule storageModule = new StorageModule(appContext,
|
||||||
|
@ -148,7 +163,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
|
|
||||||
// setup state trackers for bugsnag
|
// setup state trackers for bugsnag
|
||||||
BugsnagStateModule bugsnagStateModule = new BugsnagStateModule(
|
BugsnagStateModule bugsnagStateModule = new BugsnagStateModule(
|
||||||
configModule, configuration);
|
immutableConfig, configuration);
|
||||||
clientObservable = bugsnagStateModule.getClientObservable();
|
clientObservable = bugsnagStateModule.getClientObservable();
|
||||||
callbackState = bugsnagStateModule.getCallbackState();
|
callbackState = bugsnagStateModule.getCallbackState();
|
||||||
breadcrumbState = bugsnagStateModule.getBreadcrumbState();
|
breadcrumbState = bugsnagStateModule.getBreadcrumbState();
|
||||||
|
@ -180,8 +195,6 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
userState = storageModule.getUserStore().load(configuration.getUser());
|
userState = storageModule.getUserStore().load(configuration.getUser());
|
||||||
storageModule.getSharedPrefMigrator().deleteLegacyPrefs();
|
storageModule.getSharedPrefMigrator().deleteLegacyPrefs();
|
||||||
|
|
||||||
registerLifecycleCallbacks();
|
|
||||||
|
|
||||||
EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule,
|
EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule,
|
||||||
dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier,
|
dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier,
|
||||||
callbackState);
|
callbackState);
|
||||||
|
@ -191,33 +204,25 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
deliveryDelegate = new DeliveryDelegate(logger, eventStore,
|
deliveryDelegate = new DeliveryDelegate(logger, eventStore,
|
||||||
immutableConfig, callbackState, notifier, bgTaskService);
|
immutableConfig, callbackState, notifier, bgTaskService);
|
||||||
|
|
||||||
// Install a default exception handler with this client
|
|
||||||
exceptionHandler = new ExceptionHandler(this, logger);
|
exceptionHandler = new ExceptionHandler(this, logger);
|
||||||
if (immutableConfig.getEnabledErrorTypes().getUnhandledExceptions()) {
|
|
||||||
exceptionHandler.install();
|
|
||||||
}
|
|
||||||
|
|
||||||
// load last run info
|
// load last run info
|
||||||
lastRunInfoStore = storageModule.getLastRunInfoStore();
|
lastRunInfoStore = storageModule.getLastRunInfoStore();
|
||||||
lastRunInfo = storageModule.getLastRunInfo();
|
lastRunInfo = storageModule.getLastRunInfo();
|
||||||
|
|
||||||
// initialise plugins before attempting to flush any errors
|
Set<Plugin> userPlugins = configuration.getPlugins();
|
||||||
loadPlugins(configuration);
|
pluginClient = new PluginClient(userPlugins, immutableConfig, logger);
|
||||||
|
|
||||||
// Flush any on-disk errors and sessions
|
if (configuration.getTelemetry().contains(Telemetry.USAGE)) {
|
||||||
eventStore.flushOnLaunch();
|
internalMetrics = new InternalMetricsImpl();
|
||||||
eventStore.flushAsync();
|
} else {
|
||||||
sessionTracker.flushAsync();
|
internalMetrics = new InternalMetricsNoop();
|
||||||
|
}
|
||||||
|
|
||||||
// register listeners for system events in the background.
|
configDifferences = configuration.impl.getConfigDifferences();
|
||||||
systemBroadcastReceiver = new SystemBroadcastReceiver(this, logger);
|
systemBroadcastReceiver = new SystemBroadcastReceiver(this, logger);
|
||||||
registerComponentCallbacks();
|
|
||||||
registerListenersInBackground();
|
|
||||||
|
|
||||||
// leave auto breadcrumb
|
start();
|
||||||
Map<String, Object> data = Collections.emptyMap();
|
|
||||||
leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data);
|
|
||||||
logger.d("Bugsnag loaded");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@ -266,6 +271,42 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
this.lastRunInfo = null;
|
this.lastRunInfo = null;
|
||||||
this.exceptionHandler = exceptionHandler;
|
this.exceptionHandler = exceptionHandler;
|
||||||
this.notifier = notifier;
|
this.notifier = notifier;
|
||||||
|
internalMetrics = new InternalMetricsNoop();
|
||||||
|
configDifferences = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void start() {
|
||||||
|
if (immutableConfig.getEnabledErrorTypes().getUnhandledExceptions()) {
|
||||||
|
exceptionHandler.install();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise plugins before attempting anything else
|
||||||
|
NativeInterface.setClient(Client.this);
|
||||||
|
pluginClient.loadPlugins(Client.this);
|
||||||
|
NdkPluginCaller.INSTANCE.setNdkPlugin(pluginClient.getNdkPlugin());
|
||||||
|
if (immutableConfig.getTelemetry().contains(Telemetry.USAGE)) {
|
||||||
|
NdkPluginCaller.INSTANCE.setInternalMetricsEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any on-disk errors and sessions
|
||||||
|
eventStore.flushOnLaunch();
|
||||||
|
eventStore.flushAsync();
|
||||||
|
sessionTracker.flushAsync();
|
||||||
|
|
||||||
|
// These call into NdkPluginCaller to sync with the native side, so they must happen later
|
||||||
|
internalMetrics.setConfigDifferences(configDifferences);
|
||||||
|
callbackState.setInternalMetrics(internalMetrics);
|
||||||
|
|
||||||
|
// Register listeners for system events in the background
|
||||||
|
registerLifecycleCallbacks();
|
||||||
|
registerComponentCallbacks();
|
||||||
|
registerListenersInBackground();
|
||||||
|
|
||||||
|
// Leave auto breadcrumb
|
||||||
|
Map<String, Object> data = Collections.emptyMap();
|
||||||
|
leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data);
|
||||||
|
|
||||||
|
logger.d("Bugsnag loaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerLifecycleCallbacks() {
|
void registerLifecycleCallbacks() {
|
||||||
|
@ -327,13 +368,6 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadPlugins(@NonNull final Configuration configuration) {
|
|
||||||
NativeInterface.setClient(Client.this);
|
|
||||||
Set<Plugin> userPlugins = configuration.getPlugins();
|
|
||||||
pluginClient = new PluginClient(userPlugins, immutableConfig, logger);
|
|
||||||
pluginClient.loadPlugins(Client.this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void logNull(String property) {
|
private void logNull(String property) {
|
||||||
logger.e("Invalid null value supplied to client." + property + ", ignoring");
|
logger.e("Invalid null value supplied to client." + property + ", ignoring");
|
||||||
}
|
}
|
||||||
|
@ -390,7 +424,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
return bgTaskService.submitTask(TaskType.IO, new Callable<Boolean>() {
|
return bgTaskService.submitTask(TaskType.IO, new Callable<Boolean>() {
|
||||||
@Override
|
@Override
|
||||||
public Boolean call() {
|
public Boolean call() {
|
||||||
File outFile = new File(NativeInterface.getNativeReportPath());
|
File outFile = NativeInterface.getNativeReportPath();
|
||||||
return outFile.exists() || outFile.mkdirs();
|
return outFile.exists() || outFile.mkdirs();
|
||||||
}
|
}
|
||||||
}).get();
|
}).get();
|
||||||
|
@ -746,6 +780,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
|
|
||||||
// Attach context to the event
|
// Attach context to the event
|
||||||
event.setContext(contextState.getContext());
|
event.setContext(contextState.getContext());
|
||||||
|
|
||||||
|
event.setInternalMetrics(internalMetrics);
|
||||||
|
|
||||||
notifyInternal(event, onError);
|
notifyInternal(event, onError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1064,20 +1101,6 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
|
||||||
super.finalize();
|
super.finalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void warnIfNotAppContext(Context androidContext) {
|
|
||||||
if (!(androidContext instanceof Application)) {
|
|
||||||
logger.w("You should initialize Bugsnag from the onCreate() callback of your "
|
|
||||||
+ "Application subclass, as this guarantees errors are captured as early "
|
|
||||||
+ "as possible. "
|
|
||||||
+ "If a custom Application subclass is not possible in your app then you "
|
|
||||||
+ "should suppress this warning by passing the Application context instead: "
|
|
||||||
+ "Bugsnag.start(context.getApplicationContext()). "
|
|
||||||
+ "For further info see: "
|
|
||||||
+ "https://docs.bugsnag.com/platforms/android/#basic-configuration");
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImmutableConfig getConfig() {
|
ImmutableConfig getConfig() {
|
||||||
return immutableConfig;
|
return immutableConfig;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ internal class ConfigInternal(
|
||||||
var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS
|
var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS
|
||||||
var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS
|
var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS
|
||||||
var maxReportedThreads: Int = DEFAULT_MAX_REPORTED_THREADS
|
var maxReportedThreads: Int = DEFAULT_MAX_REPORTED_THREADS
|
||||||
|
var maxStringValueLength: Int = DEFAULT_MAX_STRING_VALUE_LENGTH
|
||||||
var context: String? = null
|
var context: String? = null
|
||||||
|
|
||||||
var redactedKeys: Set<String>
|
var redactedKeys: Set<String>
|
||||||
|
@ -53,10 +54,12 @@ internal class ConfigInternal(
|
||||||
var discardClasses: Set<String> = emptySet()
|
var discardClasses: Set<String> = emptySet()
|
||||||
var enabledReleaseStages: Set<String>? = null
|
var enabledReleaseStages: Set<String>? = null
|
||||||
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null
|
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null
|
||||||
var telemetry: Set<Telemetry> = EnumSet.of(Telemetry.INTERNAL_ERRORS)
|
var telemetry: Set<Telemetry> = EnumSet.of(Telemetry.INTERNAL_ERRORS, Telemetry.USAGE)
|
||||||
var projectPackages: Set<String> = emptySet()
|
var projectPackages: Set<String> = emptySet()
|
||||||
var persistenceDirectory: File? = null
|
var persistenceDirectory: File? = null
|
||||||
|
|
||||||
|
var attemptDeliveryOnCrash: Boolean = false
|
||||||
|
|
||||||
val notifier: Notifier = Notifier()
|
val notifier: Notifier = Notifier()
|
||||||
|
|
||||||
protected val plugins = HashSet<Plugin>()
|
protected val plugins = HashSet<Plugin>()
|
||||||
|
@ -98,12 +101,59 @@ internal class ConfigInternal(
|
||||||
plugins.add(plugin)
|
plugins.add(plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun toCommaSeparated(coll: Collection<Any>?): String {
|
||||||
|
return coll?.map { it.toString() }?.sorted()?.joinToString(",") ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConfigDifferences(): Map<String, Any> {
|
||||||
|
// allocate a local ConfigInternal with all-defaults to compare against
|
||||||
|
val defaultConfig = ConfigInternal("")
|
||||||
|
|
||||||
|
return listOfNotNull(
|
||||||
|
if (plugins.count() > 0) "pluginCount" to plugins.count() else null,
|
||||||
|
if (autoDetectErrors != defaultConfig.autoDetectErrors)
|
||||||
|
"autoDetectErrors" to autoDetectErrors else null,
|
||||||
|
if (autoTrackSessions != defaultConfig.autoTrackSessions)
|
||||||
|
"autoTrackSessions" to autoTrackSessions else null,
|
||||||
|
if (discardClasses.count() > 0)
|
||||||
|
"discardClassesCount" to discardClasses.count() else null,
|
||||||
|
if (enabledBreadcrumbTypes != defaultConfig.enabledBreadcrumbTypes)
|
||||||
|
"enabledBreadcrumbTypes" to toCommaSeparated(enabledBreadcrumbTypes) else null,
|
||||||
|
if (enabledErrorTypes != defaultConfig.enabledErrorTypes)
|
||||||
|
"enabledErrorTypes" to toCommaSeparated(
|
||||||
|
listOfNotNull(
|
||||||
|
if (enabledErrorTypes.anrs) "anrs" else null,
|
||||||
|
if (enabledErrorTypes.ndkCrashes) "ndkCrashes" else null,
|
||||||
|
if (enabledErrorTypes.unhandledExceptions) "unhandledExceptions" else null,
|
||||||
|
if (enabledErrorTypes.unhandledRejections) "unhandledRejections" else null,
|
||||||
|
)
|
||||||
|
) else null,
|
||||||
|
if (launchDurationMillis != 0L) "launchDurationMillis" to launchDurationMillis else null,
|
||||||
|
if (logger != NoopLogger) "logger" to true else null,
|
||||||
|
if (maxBreadcrumbs != defaultConfig.maxBreadcrumbs)
|
||||||
|
"maxBreadcrumbs" to maxBreadcrumbs else null,
|
||||||
|
if (maxPersistedEvents != defaultConfig.maxPersistedEvents)
|
||||||
|
"maxPersistedEvents" to maxPersistedEvents else null,
|
||||||
|
if (maxPersistedSessions != defaultConfig.maxPersistedSessions)
|
||||||
|
"maxPersistedSessions" to maxPersistedSessions else null,
|
||||||
|
if (maxReportedThreads != defaultConfig.maxReportedThreads)
|
||||||
|
"maxReportedThreads" to maxReportedThreads else null,
|
||||||
|
if (persistenceDirectory != null)
|
||||||
|
"persistenceDirectorySet" to true else null,
|
||||||
|
if (sendThreads != defaultConfig.sendThreads)
|
||||||
|
"sendThreads" to sendThreads else null,
|
||||||
|
if (attemptDeliveryOnCrash != defaultConfig.attemptDeliveryOnCrash)
|
||||||
|
"attemptDeliveryOnCrash" to attemptDeliveryOnCrash else null
|
||||||
|
).toMap()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEFAULT_MAX_BREADCRUMBS = 50
|
private const val DEFAULT_MAX_BREADCRUMBS = 100
|
||||||
private const val DEFAULT_MAX_PERSISTED_SESSIONS = 128
|
private const val DEFAULT_MAX_PERSISTED_SESSIONS = 128
|
||||||
private const val DEFAULT_MAX_PERSISTED_EVENTS = 32
|
private const val DEFAULT_MAX_PERSISTED_EVENTS = 32
|
||||||
private const val DEFAULT_MAX_REPORTED_THREADS = 200
|
private const val DEFAULT_MAX_REPORTED_THREADS = 200
|
||||||
private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000
|
private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000
|
||||||
|
private const val DEFAULT_MAX_STRING_VALUE_LENGTH = 10000
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun load(context: Context): Configuration = load(context, null)
|
fun load(context: Context): Configuration = load(context, null)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -19,7 +18,7 @@ import java.util.Set;
|
||||||
public class Configuration implements CallbackAware, MetadataAware, UserAware, FeatureFlagAware {
|
public class Configuration implements CallbackAware, MetadataAware, UserAware, FeatureFlagAware {
|
||||||
|
|
||||||
private static final int MIN_BREADCRUMBS = 0;
|
private static final int MIN_BREADCRUMBS = 0;
|
||||||
private static final int MAX_BREADCRUMBS = 100;
|
private static final int MAX_BREADCRUMBS = 500;
|
||||||
private static final int VALID_API_KEY_LEN = 32;
|
private static final int VALID_API_KEY_LEN = 32;
|
||||||
private static final long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0;
|
private static final long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0;
|
||||||
|
|
||||||
|
@ -513,7 +512,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
|
||||||
* Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached,
|
* Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached,
|
||||||
* the oldest breadcrumbs will be deleted.
|
* the oldest breadcrumbs will be deleted.
|
||||||
*
|
*
|
||||||
* By default, 50 breadcrumbs are stored: this can be amended up to a maximum of 100.
|
* By default, 100 breadcrumbs are stored: this can be amended up to a maximum of 500.
|
||||||
*/
|
*/
|
||||||
public int getMaxBreadcrumbs() {
|
public int getMaxBreadcrumbs() {
|
||||||
return impl.getMaxBreadcrumbs();
|
return impl.getMaxBreadcrumbs();
|
||||||
|
@ -523,7 +522,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
|
||||||
* Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached,
|
* Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached,
|
||||||
* the oldest breadcrumbs will be deleted.
|
* the oldest breadcrumbs will be deleted.
|
||||||
*
|
*
|
||||||
* By default, 50 breadcrumbs are stored: this can be amended up to a maximum of 100.
|
* By default, 100 breadcrumbs are stored: this can be amended up to a maximum of 500.
|
||||||
*/
|
*/
|
||||||
public void setMaxBreadcrumbs(int maxBreadcrumbs) {
|
public void setMaxBreadcrumbs(int maxBreadcrumbs) {
|
||||||
if (maxBreadcrumbs >= MIN_BREADCRUMBS && maxBreadcrumbs <= MAX_BREADCRUMBS) {
|
if (maxBreadcrumbs >= MIN_BREADCRUMBS && maxBreadcrumbs <= MAX_BREADCRUMBS) {
|
||||||
|
@ -613,6 +612,32 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the maximum string length in any metadata field. Once the threshold is
|
||||||
|
* reached in a particular string, all excess characters will be deleted.
|
||||||
|
*
|
||||||
|
* By default, the limit is 10,000.
|
||||||
|
*/
|
||||||
|
public int getMaxStringValueLength() {
|
||||||
|
return impl.getMaxStringValueLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum string length in any metadata field. Once the threshold is
|
||||||
|
* reached in a particular string, all excess characters will be deleted.
|
||||||
|
*
|
||||||
|
* By default, the limit is 10,000.
|
||||||
|
*/
|
||||||
|
public void setMaxStringValueLength(int maxStringValueLength) {
|
||||||
|
if (maxStringValueLength >= 0) {
|
||||||
|
impl.setMaxStringValueLength(maxStringValueLength);
|
||||||
|
} else {
|
||||||
|
getLogger().e("Invalid configuration value detected. "
|
||||||
|
+ "Option maxStringValueLength should be a positive integer."
|
||||||
|
+ "Supplied value is " + maxStringValueLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
|
* 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.
|
* represent what was happening in your application at the time an error occurs.
|
||||||
|
@ -1112,6 +1137,39 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Bugsnag should try to send crashing errors prior to app termination.
|
||||||
|
*
|
||||||
|
* Delivery will only be attempted for uncaught Java / Kotlin exceptions or errors, and
|
||||||
|
* while in progress will block the crashing thread for up to 3 seconds.
|
||||||
|
*
|
||||||
|
* Delivery on crash should be considered unreliable due to the necessary short timeout and
|
||||||
|
* potential for generating "errors on errors".
|
||||||
|
*
|
||||||
|
* Use of this feature is discouraged because it:
|
||||||
|
* - may cause Application Not Responding (ANR) errors on-top of existing crashes
|
||||||
|
* - will result in duplicate errors in your Dashboard when errors are not detected as sent
|
||||||
|
* before termination
|
||||||
|
* - may prevent other error handlers from detecting or reporting a crash
|
||||||
|
*
|
||||||
|
* By default this value is {@code false}.
|
||||||
|
*
|
||||||
|
* @param attemptDeliveryOnCrash {@code true} if Bugsnag should try to send crashing errors
|
||||||
|
* prior to app termination
|
||||||
|
*/
|
||||||
|
public void setAttemptDeliveryOnCrash(boolean attemptDeliveryOnCrash) {
|
||||||
|
impl.setAttemptDeliveryOnCrash(attemptDeliveryOnCrash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether Bugsnag should try to send crashing errors prior to app termination.
|
||||||
|
*
|
||||||
|
* @see #setAttemptDeliveryOnCrash(boolean)
|
||||||
|
*/
|
||||||
|
public boolean isAttemptDeliveryOnCrash() {
|
||||||
|
return impl.getAttemptDeliveryOnCrash();
|
||||||
|
}
|
||||||
|
|
||||||
Set<Plugin> getPlugins() {
|
Set<Plugin> getPlugins() {
|
||||||
return impl.getPlugins();
|
return impl.getPlugins();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,79 @@
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
import android.net.TrafficStats
|
import android.net.TrafficStats
|
||||||
import java.io.ByteArrayOutputStream
|
import com.bugsnag.android.internal.JsonHelper
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
|
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
|
||||||
import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT
|
import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT
|
||||||
import java.net.HttpURLConnection.HTTP_OK
|
import java.net.HttpURLConnection.HTTP_OK
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a [JsonStream.Streamable] into JSON, placing it in a [ByteArray]
|
|
||||||
*/
|
|
||||||
internal fun serializeJsonPayload(streamable: JsonStream.Streamable): ByteArray {
|
|
||||||
return ByteArrayOutputStream().use { baos ->
|
|
||||||
JsonStream(PrintWriter(baos).buffered()).use(streamable::toStream)
|
|
||||||
baos.toByteArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DefaultDelivery(
|
internal class DefaultDelivery(
|
||||||
private val connectivity: Connectivity?,
|
private val connectivity: Connectivity?,
|
||||||
val logger: Logger
|
private val apiKey: String,
|
||||||
|
private val maxStringValueLength: Int,
|
||||||
|
private val logger: Logger
|
||||||
) : Delivery {
|
) : Delivery {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// 1MB with some fiddle room in case of encoding overhead
|
||||||
|
const val maxPayloadSize = 999700
|
||||||
|
}
|
||||||
|
|
||||||
override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
|
override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||||
val status = deliver(deliveryParams.endpoint, payload, deliveryParams.headers)
|
val status = deliver(
|
||||||
|
deliveryParams.endpoint,
|
||||||
|
JsonHelper.serialize(payload),
|
||||||
|
deliveryParams.headers
|
||||||
|
)
|
||||||
logger.i("Session API request finished with status $status")
|
logger.i("Session API request finished with status $status")
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun serializePayload(payload: EventPayload): ByteArray {
|
||||||
|
var json = JsonHelper.serialize(payload)
|
||||||
|
if (json.size <= maxPayloadSize) {
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
var event = payload.event
|
||||||
|
if (event == null) {
|
||||||
|
event = MarshalledEventSource(payload.eventFile!!, apiKey, logger).invoke()
|
||||||
|
payload.event = event
|
||||||
|
payload.apiKey = apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
val (itemsTrimmed, dataTrimmed) = event.impl.trimMetadataStringsTo(maxStringValueLength)
|
||||||
|
event.impl.internalMetrics.setMetadataTrimMetrics(
|
||||||
|
itemsTrimmed,
|
||||||
|
dataTrimmed
|
||||||
|
)
|
||||||
|
|
||||||
|
json = JsonHelper.serialize(payload)
|
||||||
|
if (json.size <= maxPayloadSize) {
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
val breadcrumbAndBytesRemovedCounts =
|
||||||
|
event.impl.trimBreadcrumbsBy(json.size - maxPayloadSize)
|
||||||
|
event.impl.internalMetrics.setBreadcrumbTrimMetrics(
|
||||||
|
breadcrumbAndBytesRemovedCounts.itemsTrimmed,
|
||||||
|
breadcrumbAndBytesRemovedCounts.dataTrimmed
|
||||||
|
)
|
||||||
|
return JsonHelper.serialize(payload)
|
||||||
|
}
|
||||||
|
|
||||||
override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus {
|
override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||||
val status = deliver(deliveryParams.endpoint, payload, deliveryParams.headers)
|
val json = serializePayload(payload)
|
||||||
|
val status = deliver(deliveryParams.endpoint, json, deliveryParams.headers)
|
||||||
logger.i("Error API request finished with status $status")
|
logger.i("Error API request finished with status $status")
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deliver(
|
fun deliver(
|
||||||
urlString: String,
|
urlString: String,
|
||||||
streamable: JsonStream.Streamable,
|
json: ByteArray,
|
||||||
headers: Map<String, String?>
|
headers: Map<String, String?>
|
||||||
): DeliveryStatus {
|
): DeliveryStatus {
|
||||||
|
|
||||||
|
@ -50,7 +84,6 @@ internal class DefaultDelivery(
|
||||||
var conn: HttpURLConnection? = null
|
var conn: HttpURLConnection? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val json = serializeJsonPayload(streamable)
|
|
||||||
conn = makeRequest(URL(urlString), json, headers)
|
conn = makeRequest(URL(urlString), json, headers)
|
||||||
|
|
||||||
// End the request, get the response code
|
// End the request, get the response code
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import android.net.TrafficStats
|
||||||
|
import com.bugsnag.android.internal.JsonHelper
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
|
||||||
|
import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT
|
||||||
|
import java.net.HttpURLConnection.HTTP_OK
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
internal class DefaultDelivery(
|
||||||
|
private val connectivity: Connectivity?,
|
||||||
|
private val apiKey: String,
|
||||||
|
private val maxStringValueLength: Int,
|
||||||
|
private val logger: Logger
|
||||||
|
) : Delivery {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// 1MB with some fiddle room in case of encoding overhead
|
||||||
|
const val maxPayloadSize = 999700
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||||
|
val status = deliver(
|
||||||
|
deliveryParams.endpoint,
|
||||||
|
JsonHelper.serialize(payload),
|
||||||
|
deliveryParams.headers
|
||||||
|
)
|
||||||
|
logger.i("Session API request finished with status $status")
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serializePayload(payload: EventPayload): ByteArray {
|
||||||
|
var json = JsonHelper.serialize(payload)
|
||||||
|
if (json.size <= maxPayloadSize) {
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
var event = payload.event
|
||||||
|
if (event == null) {
|
||||||
|
event = MarshalledEventSource(payload.eventFile!!, apiKey, logger).invoke()
|
||||||
|
payload.event = event
|
||||||
|
payload.apiKey = apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
val (itemsTrimmed, dataTrimmed) = event.impl.trimMetadataStringsTo(maxStringValueLength)
|
||||||
|
event.impl.internalMetrics.setMetadataTrimMetrics(
|
||||||
|
itemsTrimmed,
|
||||||
|
dataTrimmed
|
||||||
|
)
|
||||||
|
|
||||||
|
json = JsonHelper.serialize(payload)
|
||||||
|
if (json.size <= maxPayloadSize) {
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
val breadcrumbAndBytesRemovedCounts =
|
||||||
|
event.impl.trimBreadcrumbsBy(json.size - maxPayloadSize)
|
||||||
|
event.impl.internalMetrics.setBreadcrumbTrimMetrics(
|
||||||
|
breadcrumbAndBytesRemovedCounts.itemsTrimmed,
|
||||||
|
breadcrumbAndBytesRemovedCounts.dataTrimmed
|
||||||
|
)
|
||||||
|
return JsonHelper.serialize(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||||
|
val json = serializePayload(payload)
|
||||||
|
val status = deliver(deliveryParams.endpoint, json, deliveryParams.headers)
|
||||||
|
logger.i("Error API request finished with status $status")
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deliver(
|
||||||
|
urlString: String,
|
||||||
|
json: ByteArray,
|
||||||
|
headers: Map<String, String?>
|
||||||
|
): DeliveryStatus {
|
||||||
|
|
||||||
|
TrafficStats.setThreadStatsTag(1)
|
||||||
|
if (connectivity != null && !connectivity.hasNetworkConnection()) {
|
||||||
|
return DeliveryStatus.UNDELIVERED
|
||||||
|
}
|
||||||
|
var conn: HttpURLConnection? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
conn = makeRequest(URL(urlString), json, headers)
|
||||||
|
|
||||||
|
// End the request, get the response code
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
val status = getDeliveryStatus(responseCode)
|
||||||
|
logRequestInfo(responseCode, conn, status)
|
||||||
|
return status
|
||||||
|
} catch (oom: OutOfMemoryError) {
|
||||||
|
// attempt to persist the payload on disk. This approach uses streams to write to a
|
||||||
|
// file, which takes less memory than serializing the payload into a ByteArray, and
|
||||||
|
// therefore has a reasonable chance of retaining the payload for future delivery.
|
||||||
|
logger.w("Encountered OOM delivering payload, falling back to persist on disk", oom)
|
||||||
|
return DeliveryStatus.UNDELIVERED
|
||||||
|
} catch (exception: IOException) {
|
||||||
|
logger.w("IOException encountered in request", exception)
|
||||||
|
return DeliveryStatus.UNDELIVERED
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
logger.w("Unexpected error delivering payload", exception)
|
||||||
|
return DeliveryStatus.FAILURE
|
||||||
|
} finally {
|
||||||
|
conn?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeRequest(
|
||||||
|
url: URL,
|
||||||
|
json: ByteArray,
|
||||||
|
headers: Map<String, String?>
|
||||||
|
): HttpURLConnection {
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
conn.doOutput = true
|
||||||
|
|
||||||
|
// avoids creating a buffer within HttpUrlConnection, see
|
||||||
|
// https://developer.android.com/reference/java/net/HttpURLConnection
|
||||||
|
conn.setFixedLengthStreamingMode(json.size)
|
||||||
|
|
||||||
|
// calculate the SHA-1 digest and add all other headers
|
||||||
|
computeSha1Digest(json)?.let { digest ->
|
||||||
|
conn.addRequestProperty(HEADER_BUGSNAG_INTEGRITY, digest)
|
||||||
|
}
|
||||||
|
headers.forEach { (key, value) ->
|
||||||
|
if (value != null) {
|
||||||
|
conn.addRequestProperty(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the JSON payload
|
||||||
|
conn.outputStream.use {
|
||||||
|
it.write(json)
|
||||||
|
}
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logRequestInfo(code: Int, conn: HttpURLConnection, status: DeliveryStatus) {
|
||||||
|
runCatching {
|
||||||
|
logger.i(
|
||||||
|
"Request completed with code $code, " +
|
||||||
|
"message: ${conn.responseMessage}, " +
|
||||||
|
"headers: ${conn.headerFields}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
|
conn.inputStream.bufferedReader().use {
|
||||||
|
logger.d("Received request response: ${it.readText()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
if (status != DeliveryStatus.DELIVERED) {
|
||||||
|
conn.errorStream.bufferedReader().use {
|
||||||
|
logger.w("Request error details: ${it.readText()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun getDeliveryStatus(responseCode: Int): DeliveryStatus {
|
||||||
|
return when {
|
||||||
|
responseCode in HTTP_OK..299 -> DeliveryStatus.DELIVERED
|
||||||
|
isUnrecoverableStatusCode(responseCode) -> DeliveryStatus.FAILURE
|
||||||
|
else -> DeliveryStatus.UNDELIVERED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUnrecoverableStatusCode(responseCode: Int) =
|
||||||
|
responseCode in HTTP_BAD_REQUEST..499 && // 400-499 are considered unrecoverable
|
||||||
|
responseCode != HTTP_CLIENT_TIMEOUT && // except for 408
|
||||||
|
responseCode != 429 // and 429
|
||||||
|
}
|
|
@ -7,14 +7,15 @@ import com.bugsnag.android.internal.ImmutableConfig;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.concurrent.Future;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
class DeliveryDelegate extends BaseObservable {
|
class DeliveryDelegate extends BaseObservable {
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static long DELIVERY_TIMEOUT = 3000L;
|
||||||
|
|
||||||
final Logger logger;
|
final Logger logger;
|
||||||
private final EventStore eventStore;
|
private final EventStore eventStore;
|
||||||
private final ImmutableConfig immutableConfig;
|
private final ImmutableConfig immutableConfig;
|
||||||
|
@ -55,7 +56,13 @@ class DeliveryDelegate extends BaseObservable {
|
||||||
String severityReasonType = event.getImpl().getSeverityReasonType();
|
String severityReasonType = event.getImpl().getSeverityReasonType();
|
||||||
boolean promiseRejection = REASON_PROMISE_REJECTION.equals(severityReasonType);
|
boolean promiseRejection = REASON_PROMISE_REJECTION.equals(severityReasonType);
|
||||||
boolean anr = event.getImpl().isAnr(event);
|
boolean anr = event.getImpl().isAnr(event);
|
||||||
cacheEvent(event, anr || promiseRejection);
|
if (anr || promiseRejection) {
|
||||||
|
cacheEvent(event, true);
|
||||||
|
} else if (immutableConfig.getAttemptDeliveryOnCrash()) {
|
||||||
|
cacheAndSendSynchronously(event);
|
||||||
|
} else {
|
||||||
|
cacheEvent(event, false);
|
||||||
|
}
|
||||||
} else if (callbackState.runOnSendTasks(event, logger)) {
|
} else if (callbackState.runOnSendTasks(event, logger)) {
|
||||||
// Build the eventPayload
|
// Build the eventPayload
|
||||||
String apiKey = event.getApiKey();
|
String apiKey = event.getApiKey();
|
||||||
|
@ -107,6 +114,24 @@ class DeliveryDelegate extends BaseObservable {
|
||||||
return deliveryStatus;
|
return deliveryStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cacheAndSendSynchronously(@NonNull Event event) {
|
||||||
|
long cutoffTime = System.currentTimeMillis() + DELIVERY_TIMEOUT;
|
||||||
|
Future<String> task = eventStore.writeAndDeliver(event);
|
||||||
|
|
||||||
|
long timeout = cutoffTime - System.currentTimeMillis();
|
||||||
|
if (task != null && timeout > 0) {
|
||||||
|
try {
|
||||||
|
task.get(timeout, TimeUnit.MILLISECONDS);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.w("failed to immediately deliver event", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!task.isDone()) {
|
||||||
|
task.cancel(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void cacheEvent(@NonNull Event event, boolean attemptSend) {
|
private void cacheEvent(@NonNull Event event, boolean attemptSend) {
|
||||||
eventStore.write(event);
|
eventStore.write(event);
|
||||||
if (attemptSend) {
|
if (attemptSend) {
|
||||||
|
|
|
@ -33,4 +33,20 @@ class ErrorTypes(
|
||||||
internal constructor(detectErrors: Boolean) : this(detectErrors, detectErrors, detectErrors, detectErrors)
|
internal constructor(detectErrors: Boolean) : this(detectErrors, detectErrors, detectErrors, detectErrors)
|
||||||
|
|
||||||
internal fun copy() = ErrorTypes(anrs, ndkCrashes, unhandledExceptions, unhandledRejections)
|
internal fun copy() = ErrorTypes(anrs, ndkCrashes, unhandledExceptions, unhandledRejections)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return other is ErrorTypes &&
|
||||||
|
anrs == other.anrs &&
|
||||||
|
ndkCrashes == other.ndkCrashes &&
|
||||||
|
unhandledExceptions == other.unhandledExceptions &&
|
||||||
|
unhandledRejections == other.unhandledRejections
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = anrs.hashCode()
|
||||||
|
result = 31 * result + ndkCrashes.hashCode()
|
||||||
|
result = 31 * result + unhandledExceptions.hashCode()
|
||||||
|
result = 31 * result + unhandledRejections.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.bugsnag.android;
|
package com.bugsnag.android;
|
||||||
|
|
||||||
import com.bugsnag.android.internal.ImmutableConfig;
|
import com.bugsnag.android.internal.ImmutableConfig;
|
||||||
|
import com.bugsnag.android.internal.InternalMetrics;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -9,7 +10,6 @@ import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An Event object represents a Throwable captured by Bugsnag and is available as a parameter on
|
* An Event object represents a Throwable captured by Bugsnag and is available as a parameter on
|
||||||
|
@ -91,6 +91,15 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F
|
||||||
return impl.getBreadcrumbs();
|
return impl.getBreadcrumbs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of feature flags active at the time of the event.
|
||||||
|
* See {@link FeatureFlag} for details of the data available.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public List<FeatureFlag> getFeatureFlags() {
|
||||||
|
return impl.getFeatureFlags().toList();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information set by the notifier about your app can be found in this field. These values
|
* Information set by the notifier about your app can be found in this field. These values
|
||||||
* can be accessed and amended if necessary.
|
* can be accessed and amended if necessary.
|
||||||
|
@ -412,4 +421,8 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F
|
||||||
void setRedactedKeys(Collection<String> redactedKeys) {
|
void setRedactedKeys(Collection<String> redactedKeys) {
|
||||||
impl.setRedactedKeys(redactedKeys);
|
impl.setRedactedKeys(redactedKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setInternalMetrics(InternalMetrics metrics) {
|
||||||
|
impl.setInternalMetrics(metrics);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
import com.bugsnag.android.internal.ImmutableConfig
|
import com.bugsnag.android.internal.ImmutableConfig
|
||||||
|
import com.bugsnag.android.internal.InternalMetrics
|
||||||
|
import com.bugsnag.android.internal.InternalMetricsNoop
|
||||||
|
import com.bugsnag.android.internal.JsonHelper
|
||||||
|
import com.bugsnag.android.internal.TrimMetrics
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware {
|
internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware {
|
||||||
|
@ -14,6 +18,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
|
||||||
featureFlags: FeatureFlags = FeatureFlags()
|
featureFlags: FeatureFlags = FeatureFlags()
|
||||||
) : this(
|
) : this(
|
||||||
config.apiKey,
|
config.apiKey,
|
||||||
|
config.logger,
|
||||||
mutableListOf(),
|
mutableListOf(),
|
||||||
config.discardClasses.toSet(),
|
config.discardClasses.toSet(),
|
||||||
when (originalError) {
|
when (originalError) {
|
||||||
|
@ -32,6 +37,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
|
||||||
|
|
||||||
internal constructor(
|
internal constructor(
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
|
logger: Logger,
|
||||||
breadcrumbs: MutableList<Breadcrumb> = mutableListOf(),
|
breadcrumbs: MutableList<Breadcrumb> = mutableListOf(),
|
||||||
discardClasses: Set<String> = setOf(),
|
discardClasses: Set<String> = setOf(),
|
||||||
errors: MutableList<Error> = mutableListOf(),
|
errors: MutableList<Error> = mutableListOf(),
|
||||||
|
@ -44,6 +50,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
|
||||||
user: User = User(),
|
user: User = User(),
|
||||||
redactionKeys: Set<String>? = null
|
redactionKeys: Set<String>? = null
|
||||||
) {
|
) {
|
||||||
|
this.logger = logger
|
||||||
this.apiKey = apiKey
|
this.apiKey = apiKey
|
||||||
this.breadcrumbs = breadcrumbs
|
this.breadcrumbs = breadcrumbs
|
||||||
this.discardClasses = discardClasses
|
this.discardClasses = discardClasses
|
||||||
|
@ -64,6 +71,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
|
||||||
val originalError: Throwable?
|
val originalError: Throwable?
|
||||||
internal var severityReason: SeverityReason
|
internal var severityReason: SeverityReason
|
||||||
|
|
||||||
|
val logger: Logger
|
||||||
val metadata: Metadata
|
val metadata: Metadata
|
||||||
val featureFlags: FeatureFlags
|
val featureFlags: FeatureFlags
|
||||||
private val discardClasses: Set<String>
|
private val discardClasses: Set<String>
|
||||||
|
@ -103,6 +111,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
|
||||||
jsonStreamer.redactedKeys = value.toSet()
|
jsonStreamer.redactedKeys = value.toSet()
|
||||||
metadata.redactedKeys = value.toSet()
|
metadata.redactedKeys = value.toSet()
|
||||||
}
|
}
|
||||||
|
var internalMetrics: InternalMetrics = InternalMetricsNoop()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return user information associated with this Event
|
* @return user information associated with this Event
|
||||||
|
@ -162,6 +171,15 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
|
||||||
writer.name("device").value(device)
|
writer.name("device").value(device)
|
||||||
writer.name("breadcrumbs").value(breadcrumbs)
|
writer.name("breadcrumbs").value(breadcrumbs)
|
||||||
writer.name("groupingHash").value(groupingHash)
|
writer.name("groupingHash").value(groupingHash)
|
||||||
|
val usage = internalMetrics.toJsonableMap()
|
||||||
|
if (usage.isNotEmpty()) {
|
||||||
|
writer.name("usage")
|
||||||
|
writer.beginObject()
|
||||||
|
usage.forEach { entry ->
|
||||||
|
writer.name(entry.key).value(entry.value)
|
||||||
|
}
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
writer.name("threads")
|
writer.name("threads")
|
||||||
writer.beginArray()
|
writer.beginArray()
|
||||||
|
@ -229,6 +247,41 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
|
||||||
|
|
||||||
fun getSeverityReasonType(): String = severityReason.severityReasonType
|
fun getSeverityReasonType(): String = severityReason.severityReasonType
|
||||||
|
|
||||||
|
fun trimMetadataStringsTo(maxLength: Int): TrimMetrics {
|
||||||
|
var stringCount = 0
|
||||||
|
var charCount = 0
|
||||||
|
|
||||||
|
var stringAndCharCounts = metadata.trimMetadataStringsTo(maxLength)
|
||||||
|
stringCount += stringAndCharCounts.itemsTrimmed
|
||||||
|
charCount += stringAndCharCounts.dataTrimmed
|
||||||
|
for (breadcrumb in breadcrumbs) {
|
||||||
|
stringAndCharCounts = breadcrumb.impl.trimMetadataStringsTo(maxLength)
|
||||||
|
stringCount += stringAndCharCounts.itemsTrimmed
|
||||||
|
charCount += stringAndCharCounts.dataTrimmed
|
||||||
|
}
|
||||||
|
return TrimMetrics(stringCount, charCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trimBreadcrumbsBy(byteCount: Int): TrimMetrics {
|
||||||
|
var removedBreadcrumbCount = 0
|
||||||
|
var removedByteCount = 0
|
||||||
|
while (removedByteCount < byteCount && breadcrumbs.isNotEmpty()) {
|
||||||
|
val breadcrumb = breadcrumbs.removeAt(0)
|
||||||
|
removedByteCount += JsonHelper.serialize(breadcrumb).size
|
||||||
|
removedBreadcrumbCount++
|
||||||
|
}
|
||||||
|
when (removedBreadcrumbCount) {
|
||||||
|
1 -> breadcrumbs.add(Breadcrumb("Removed to reduce payload size", logger))
|
||||||
|
else -> breadcrumbs.add(
|
||||||
|
Breadcrumb(
|
||||||
|
"Removed, along with ${removedBreadcrumbCount - 1} older breadcrumbs, to reduce payload size",
|
||||||
|
logger
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return TrimMetrics(removedBreadcrumbCount, removedByteCount)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setUser(id: String?, email: String?, name: String?) {
|
override fun setUser(id: String?, email: String?, name: String?) {
|
||||||
userImpl = User(id, email, name)
|
userImpl = User(id, email, name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,17 +12,21 @@ import java.io.IOException
|
||||||
*/
|
*/
|
||||||
class EventPayload @JvmOverloads internal constructor(
|
class EventPayload @JvmOverloads internal constructor(
|
||||||
var apiKey: String?,
|
var apiKey: String?,
|
||||||
val event: Event? = null,
|
event: Event? = null,
|
||||||
internal val eventFile: File? = null,
|
internal val eventFile: File? = null,
|
||||||
notifier: Notifier,
|
notifier: Notifier,
|
||||||
private val config: ImmutableConfig
|
private val config: ImmutableConfig
|
||||||
) : JsonStream.Streamable {
|
) : JsonStream.Streamable {
|
||||||
|
|
||||||
|
var event = event
|
||||||
|
internal set(value) { field = value }
|
||||||
|
|
||||||
internal val notifier = Notifier(notifier.name, notifier.version, notifier.url).apply {
|
internal val notifier = Notifier(notifier.name, notifier.version, notifier.url).apply {
|
||||||
dependencies = notifier.dependencies.toMutableList()
|
dependencies = notifier.dependencies.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun getErrorTypes(): Set<ErrorType> {
|
internal fun getErrorTypes(): Set<ErrorType> {
|
||||||
|
val event = this.event
|
||||||
return when {
|
return when {
|
||||||
event != null -> event.impl.getErrorTypesFromStackframes()
|
event != null -> event.impl.getErrorTypesFromStackframes()
|
||||||
eventFile != null -> EventFilenameInfo.fromFile(eventFile, config).errorTypes
|
eventFile != null -> EventFilenameInfo.fromFile(eventFile, config).errorTypes
|
||||||
|
|
|
@ -13,6 +13,7 @@ import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
@ -132,6 +133,26 @@ class EventStore extends FileStore {
|
||||||
return launchCrashes.isEmpty() ? null : launchCrashes.get(launchCrashes.size() - 1);
|
return launchCrashes.isEmpty() ? null : launchCrashes.get(launchCrashes.size() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
Future<String> writeAndDeliver(@NonNull final JsonStream.Streamable streamable) {
|
||||||
|
final String filename = write(streamable);
|
||||||
|
|
||||||
|
if (filename != null) {
|
||||||
|
try {
|
||||||
|
return bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Callable<String>() {
|
||||||
|
public String call() {
|
||||||
|
flushEventFile(new File(filename));
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (RejectedExecutionException exception) {
|
||||||
|
logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flush any on-disk errors to Bugsnag
|
* Flush any on-disk errors to Bugsnag
|
||||||
*/
|
*/
|
||||||
|
@ -163,7 +184,7 @@ class EventStore extends FileStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void flushEventFile(File eventFile) {
|
void flushEventFile(File eventFile) {
|
||||||
try {
|
try {
|
||||||
EventFilenameInfo eventInfo = EventFilenameInfo.fromFile(eventFile, config);
|
EventFilenameInfo eventInfo = EventFilenameInfo.fromFile(eventFile, config);
|
||||||
String apiKey = eventInfo.getApiKey();
|
String apiKey = eventInfo.getApiKey();
|
||||||
|
|
|
@ -1,39 +1,40 @@
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
internal class FeatureFlags(
|
internal class FeatureFlags(
|
||||||
internal val store: MutableMap<String, String?> = ConcurrentHashMap()
|
internal val store: MutableMap<String, String?> = mutableMapOf()
|
||||||
) : JsonStream.Streamable, FeatureFlagAware {
|
) : JsonStream.Streamable, FeatureFlagAware {
|
||||||
private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__"
|
private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__"
|
||||||
|
|
||||||
override fun addFeatureFlag(name: String) {
|
@Synchronized override fun addFeatureFlag(name: String) {
|
||||||
store[name] = emptyVariant
|
addFeatureFlag(name, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addFeatureFlag(name: String, variant: String?) {
|
@Synchronized override fun addFeatureFlag(name: String, variant: String?) {
|
||||||
|
store.remove(name)
|
||||||
store[name] = variant ?: emptyVariant
|
store[name] = variant ?: emptyVariant
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
|
@Synchronized override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
|
||||||
featureFlags.forEach { (name, variant) ->
|
featureFlags.forEach { (name, variant) ->
|
||||||
addFeatureFlag(name, variant)
|
addFeatureFlag(name, variant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearFeatureFlag(name: String) {
|
@Synchronized override fun clearFeatureFlag(name: String) {
|
||||||
store.remove(name)
|
store.remove(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearFeatureFlags() {
|
@Synchronized override fun clearFeatureFlags() {
|
||||||
store.clear()
|
store.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun toStream(stream: JsonStream) {
|
override fun toStream(stream: JsonStream) {
|
||||||
|
val storeCopy = synchronized(this) { store.toMap() }
|
||||||
stream.beginArray()
|
stream.beginArray()
|
||||||
store.forEach { (name, variant) ->
|
storeCopy.forEach { (name, variant) ->
|
||||||
stream.beginObject()
|
stream.beginObject()
|
||||||
stream.name("featureFlag").value(name)
|
stream.name("featureFlag").value(name)
|
||||||
if (variant != emptyVariant) {
|
if (variant != emptyVariant) {
|
||||||
|
@ -44,9 +45,9 @@ internal class FeatureFlags(
|
||||||
stream.endArray()
|
stream.endArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toList(): List<FeatureFlag> = store.entries.map { (name, variant) ->
|
@Synchronized fun toList(): List<FeatureFlag> = store.entries.map { (name, variant) ->
|
||||||
FeatureFlag(name, variant.takeUnless { it == emptyVariant })
|
FeatureFlag(name, variant.takeUnless { it == emptyVariant })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copy() = FeatureFlags(store.toMutableMap())
|
@Synchronized fun copy() = FeatureFlags(store.toMutableMap())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import static com.bugsnag.android.DeliveryHeadersKt.HEADER_INTERNAL_ERROR;
|
||||||
import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION;
|
import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION;
|
||||||
|
|
||||||
import com.bugsnag.android.internal.ImmutableConfig;
|
import com.bugsnag.android.internal.ImmutableConfig;
|
||||||
|
import com.bugsnag.android.internal.JsonHelper;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
@ -121,7 +122,11 @@ class InternalReportDelegate implements EventStore.Delegate {
|
||||||
headers.put(HEADER_INTERNAL_ERROR, "bugsnag-android");
|
headers.put(HEADER_INTERNAL_ERROR, "bugsnag-android");
|
||||||
headers.remove(DeliveryHeadersKt.HEADER_API_KEY);
|
headers.remove(DeliveryHeadersKt.HEADER_API_KEY);
|
||||||
DefaultDelivery defaultDelivery = (DefaultDelivery) delivery;
|
DefaultDelivery defaultDelivery = (DefaultDelivery) delivery;
|
||||||
defaultDelivery.deliver(params.getEndpoint(), payload, headers);
|
defaultDelivery.deliver(
|
||||||
|
params.getEndpoint(),
|
||||||
|
JsonHelper.INSTANCE.serialize(payload),
|
||||||
|
headers
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
|
|
|
@ -42,6 +42,7 @@ internal class ManifestConfigLoader {
|
||||||
private const val LAUNCH_DURATION_MILLIS = "$BUGSNAG_NS.LAUNCH_DURATION_MILLIS"
|
private const val LAUNCH_DURATION_MILLIS = "$BUGSNAG_NS.LAUNCH_DURATION_MILLIS"
|
||||||
private const val SEND_LAUNCH_CRASHES_SYNCHRONOUSLY = "$BUGSNAG_NS.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY"
|
private const val SEND_LAUNCH_CRASHES_SYNCHRONOUSLY = "$BUGSNAG_NS.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY"
|
||||||
private const val APP_TYPE = "$BUGSNAG_NS.APP_TYPE"
|
private const val APP_TYPE = "$BUGSNAG_NS.APP_TYPE"
|
||||||
|
private const val ATTEMPT_DELIVERY_ON_CRASH = "$BUGSNAG_NS.ATTEMPT_DELIVERY_ON_CRASH"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load(ctx: Context, userSuppliedApiKey: String?): Configuration {
|
fun load(ctx: Context, userSuppliedApiKey: String?): Configuration {
|
||||||
|
@ -91,6 +92,10 @@ internal class ManifestConfigLoader {
|
||||||
SEND_LAUNCH_CRASHES_SYNCHRONOUSLY,
|
SEND_LAUNCH_CRASHES_SYNCHRONOUSLY,
|
||||||
sendLaunchCrashesSynchronously
|
sendLaunchCrashesSynchronously
|
||||||
)
|
)
|
||||||
|
isAttemptDeliveryOnCrash = data.getBoolean(
|
||||||
|
ATTEMPT_DELIVERY_ON_CRASH,
|
||||||
|
isAttemptDeliveryOnCrash
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import com.bugsnag.android.internal.StringUtils
|
||||||
|
import com.bugsnag.android.internal.TrimMetrics
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
@ -137,4 +139,19 @@ internal data class Metadata @JvmOverloads constructor(
|
||||||
return this.copy(store = toMap())
|
return this.copy(store = toMap())
|
||||||
.also { it.redactedKeys = redactedKeys.toSet() }
|
.also { it.redactedKeys = redactedKeys.toSet() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun trimMetadataStringsTo(maxStringLength: Int): TrimMetrics {
|
||||||
|
var stringCount = 0
|
||||||
|
var charCount = 0
|
||||||
|
store.forEach { entry ->
|
||||||
|
val stringAndCharCounts = StringUtils.trimStringValuesTo(
|
||||||
|
maxStringLength,
|
||||||
|
entry.value as MutableMap<String, Any?>
|
||||||
|
)
|
||||||
|
|
||||||
|
stringCount += stringAndCharCounts.itemsTrimmed
|
||||||
|
charCount += stringAndCharCounts.dataTrimmed
|
||||||
|
}
|
||||||
|
return TrimMetrics(stringCount, charCount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
package com.bugsnag.android;
|
package com.bugsnag.android;
|
||||||
|
|
||||||
import com.bugsnag.android.internal.ImmutableConfig;
|
import com.bugsnag.android.internal.ImmutableConfig;
|
||||||
|
import com.bugsnag.android.internal.JsonHelper;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -38,6 +43,26 @@ public class NativeInterface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an empty Event for a "handled exception" report. The returned Event will have
|
||||||
|
* no Error objects, metadata, breadcrumbs, or feature flags. It's indented that the caller
|
||||||
|
* will populate the Error and then pass the Event object to
|
||||||
|
* {@link Client#populateAndNotifyAndroidEvent(Event, OnErrorCallback)}.
|
||||||
|
*/
|
||||||
|
private static Event createEmptyEvent() {
|
||||||
|
Client client = getClient();
|
||||||
|
|
||||||
|
return new Event(
|
||||||
|
new EventInternal(
|
||||||
|
(Throwable) null,
|
||||||
|
client.getConfig(),
|
||||||
|
SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION),
|
||||||
|
client.getMetadataState().getMetadata().copy()
|
||||||
|
),
|
||||||
|
client.getLogger()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Caches a client instance for responding to future events
|
* Caches a client instance for responding to future events
|
||||||
*/
|
*/
|
||||||
|
@ -54,10 +79,16 @@ public class NativeInterface {
|
||||||
* Retrieves the directory used to store native crash reports
|
* Retrieves the directory used to store native crash reports
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static String getNativeReportPath() {
|
public static File getNativeReportPath() {
|
||||||
ImmutableConfig config = getClient().getConfig();
|
return getNativeReportPath(getPersistenceDirectory());
|
||||||
File persistenceDirectory = config.getPersistenceDirectory().getValue();
|
}
|
||||||
return new File(persistenceDirectory, "bugsnag-native").getAbsolutePath();
|
|
||||||
|
private static @NonNull File getNativeReportPath(@NonNull File persistenceDirectory) {
|
||||||
|
return new File(persistenceDirectory, "bugsnag-native");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NonNull File getPersistenceDirectory() {
|
||||||
|
return getClient().getConfig().getPersistenceDirectory().getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,7 +96,7 @@ public class NativeInterface {
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static Map<String,String> getUser() {
|
public static Map<String, String> getUser() {
|
||||||
HashMap<String, String> userData = new HashMap<>();
|
HashMap<String, String> userData = new HashMap<>();
|
||||||
User user = getClient().getUser();
|
User user = getClient().getUser();
|
||||||
userData.put("id", user.getId());
|
userData.put("id", user.getId());
|
||||||
|
@ -79,8 +110,8 @@ public class NativeInterface {
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static Map<String,Object> getApp() {
|
public static Map<String, Object> getApp() {
|
||||||
HashMap<String,Object> data = new HashMap<>();
|
HashMap<String, Object> data = new HashMap<>();
|
||||||
AppDataCollector source = getClient().getAppDataCollector();
|
AppDataCollector source = getClient().getAppDataCollector();
|
||||||
AppWithState app = source.generateAppWithState();
|
AppWithState app = source.generateAppWithState();
|
||||||
data.put("version", app.getVersion());
|
data.put("version", app.getVersion());
|
||||||
|
@ -103,7 +134,7 @@ public class NativeInterface {
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static Map<String,Object> getDevice() {
|
public static Map<String, Object> getDevice() {
|
||||||
DeviceDataCollector source = getClient().getDeviceDataCollector();
|
DeviceDataCollector source = getClient().getDeviceDataCollector();
|
||||||
HashMap<String, Object> deviceData = new HashMap<>(source.getDeviceMetadata());
|
HashMap<String, Object> deviceData = new HashMap<>(source.getDeviceMetadata());
|
||||||
|
|
||||||
|
@ -152,9 +183,9 @@ public class NativeInterface {
|
||||||
/**
|
/**
|
||||||
* Sets the user
|
* Sets the user
|
||||||
*
|
*
|
||||||
* @param id id
|
* @param id id
|
||||||
* @param email email
|
* @param email email
|
||||||
* @param name name
|
* @param name name
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static void setUser(@Nullable final String id,
|
public static void setUser(@Nullable final String id,
|
||||||
|
@ -167,9 +198,9 @@ public class NativeInterface {
|
||||||
/**
|
/**
|
||||||
* Sets the user
|
* Sets the user
|
||||||
*
|
*
|
||||||
* @param idBytes id
|
* @param idBytes id
|
||||||
* @param emailBytes email
|
* @param emailBytes email
|
||||||
* @param nameBytes name
|
* @param nameBytes name
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static void setUser(@Nullable final byte[] idBytes,
|
public static void setUser(@Nullable final byte[] idBytes,
|
||||||
|
@ -300,6 +331,36 @@ public class NativeInterface {
|
||||||
unhandledCount, handledCount);
|
unhandledCount, handledCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask if an error class is on the configurable discard list.
|
||||||
|
* This is used by the native layer to decide whether to pass an event to
|
||||||
|
* deliverReport() or not.
|
||||||
|
*
|
||||||
|
* @param name The error class to ask about.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static boolean isDiscardErrorClass(@NonNull String name) {
|
||||||
|
return getClient().getConfig().getDiscardClasses().contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void deepMerge(Map<String, Object> src, Map<String, Object> dst) {
|
||||||
|
for (Map.Entry<String, Object> entry: src.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
Object srcValue = entry.getValue();
|
||||||
|
Object dstValue = dst.get(key);
|
||||||
|
if (srcValue instanceof Map && (dstValue instanceof Map)) {
|
||||||
|
deepMerge((Map<String, Object>)srcValue, (Map<String, Object>)dstValue);
|
||||||
|
} else if (srcValue instanceof Collection && dstValue instanceof Collection) {
|
||||||
|
// Just append everything because we don't know enough about the context or
|
||||||
|
// provenance of the data to make an intelligent decision about this.
|
||||||
|
((Collection<Object>)dstValue).addAll((Collection<Object>)srcValue);
|
||||||
|
} else {
|
||||||
|
dst.put(key, srcValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deliver a report, serialized as an event JSON payload.
|
* Deliver a report, serialized as an event JSON payload.
|
||||||
*
|
*
|
||||||
|
@ -307,18 +368,31 @@ public class NativeInterface {
|
||||||
* captured. Used to determine whether the report
|
* captured. Used to determine whether the report
|
||||||
* should be discarded, based on configured release
|
* should be discarded, based on configured release
|
||||||
* stages
|
* stages
|
||||||
* @param payloadBytes The raw JSON payload of the event
|
* @param payloadBytes The raw JSON payload of the event
|
||||||
* @param apiKey The apiKey for the event
|
* @param apiKey The apiKey for the event
|
||||||
* @param isLaunching whether the crash occurred when the app was launching
|
* @param isLaunching whether the crash occurred when the app was launching
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static void deliverReport(@Nullable byte[] releaseStageBytes,
|
public static void deliverReport(@Nullable byte[] releaseStageBytes,
|
||||||
@NonNull byte[] payloadBytes,
|
@NonNull byte[] payloadBytes,
|
||||||
|
@Nullable byte[] staticDataBytes,
|
||||||
@NonNull String apiKey,
|
@NonNull String apiKey,
|
||||||
boolean isLaunching) {
|
boolean isLaunching) {
|
||||||
if (payloadBytes == null) {
|
// If there's saved static data, merge it directly into the payload map.
|
||||||
return;
|
if (staticDataBytes != null) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> payloadMap = (Map<String, Object>) JsonHelper.INSTANCE.deserialize(
|
||||||
|
new ByteArrayInputStream(payloadBytes));
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> staticDataMap =
|
||||||
|
(Map<String, Object>) JsonHelper.INSTANCE.deserialize(
|
||||||
|
new ByteArrayInputStream(staticDataBytes));
|
||||||
|
deepMerge(staticDataMap, payloadMap);
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
JsonHelper.INSTANCE.serialize(payloadMap, os);
|
||||||
|
payloadBytes = os.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
String payload = new String(payloadBytes, UTF8Charset);
|
String payload = new String(payloadBytes, UTF8Charset);
|
||||||
String releaseStage = releaseStageBytes == null
|
String releaseStage = releaseStageBytes == null
|
||||||
? null
|
? null
|
||||||
|
@ -341,10 +415,10 @@ public class NativeInterface {
|
||||||
/**
|
/**
|
||||||
* Notifies using the Android SDK
|
* Notifies using the Android SDK
|
||||||
*
|
*
|
||||||
* @param nameBytes the error name
|
* @param nameBytes the error name
|
||||||
* @param messageBytes the error message
|
* @param messageBytes the error message
|
||||||
* @param severity the error severity
|
* @param severity the error severity
|
||||||
* @param stacktrace a stacktrace
|
* @param stacktrace a stacktrace
|
||||||
*/
|
*/
|
||||||
public static void notify(@NonNull final byte[] nameBytes,
|
public static void notify(@NonNull final byte[] nameBytes,
|
||||||
@NonNull final byte[] messageBytes,
|
@NonNull final byte[] messageBytes,
|
||||||
|
@ -361,9 +435,9 @@ public class NativeInterface {
|
||||||
/**
|
/**
|
||||||
* Notifies using the Android SDK
|
* Notifies using the Android SDK
|
||||||
*
|
*
|
||||||
* @param name the error name
|
* @param name the error name
|
||||||
* @param message the error message
|
* @param message the error message
|
||||||
* @param severity the error severity
|
* @param severity the error severity
|
||||||
* @param stacktrace a stacktrace
|
* @param stacktrace a stacktrace
|
||||||
*/
|
*/
|
||||||
public static void notify(@NonNull final String name,
|
public static void notify(@NonNull final String name,
|
||||||
|
@ -397,11 +471,65 @@ public class NativeInterface {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies using the Android SDK
|
||||||
|
*
|
||||||
|
* @param nameBytes the error name
|
||||||
|
* @param messageBytes the error message
|
||||||
|
* @param severity the error severity
|
||||||
|
* @param stacktrace a stacktrace
|
||||||
|
*/
|
||||||
|
public static void notify(@NonNull final byte[] nameBytes,
|
||||||
|
@NonNull final byte[] messageBytes,
|
||||||
|
@NonNull final Severity severity,
|
||||||
|
@NonNull final NativeStackframe[] stacktrace) {
|
||||||
|
|
||||||
|
if (nameBytes == null || messageBytes == null || stacktrace == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String name = new String(nameBytes, UTF8Charset);
|
||||||
|
String message = new String(messageBytes, UTF8Charset);
|
||||||
|
notify(name, message, severity, stacktrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies using the Android SDK
|
||||||
|
*
|
||||||
|
* @param name the error name
|
||||||
|
* @param message the error message
|
||||||
|
* @param severity the error severity
|
||||||
|
* @param stacktrace a stacktrace
|
||||||
|
*/
|
||||||
|
public static void notify(@NonNull final String name,
|
||||||
|
@NonNull final String message,
|
||||||
|
@NonNull final Severity severity,
|
||||||
|
@NonNull final NativeStackframe[] stacktrace) {
|
||||||
|
Client client = getClient();
|
||||||
|
|
||||||
|
if (client.getConfig().shouldDiscardError(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Event event = createEmptyEvent();
|
||||||
|
event.updateSeverityInternal(severity);
|
||||||
|
|
||||||
|
List<Stackframe> stackframes = new ArrayList<>(stacktrace.length);
|
||||||
|
for (NativeStackframe nativeStackframe : stacktrace) {
|
||||||
|
stackframes.add(new Stackframe(nativeStackframe));
|
||||||
|
}
|
||||||
|
event.getErrors().add(new Error(
|
||||||
|
new ErrorInternal(name, message, new Stacktrace(stackframes), ErrorType.C),
|
||||||
|
client.getLogger()
|
||||||
|
));
|
||||||
|
|
||||||
|
getClient().populateAndNotifyAndroidEvent(event, null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an {@code Event} object
|
* Create an {@code Event} object
|
||||||
*
|
*
|
||||||
* @param exc the Throwable object that caused the event
|
* @param exc the Throwable object that caused the event
|
||||||
* @param client the Client object that the event is associated with
|
* @param client the Client object that the event is associated with
|
||||||
* @param severityReason the severity of the Event
|
* @param severityReason the severity of the Event
|
||||||
* @return a new {@code Event} object
|
* @return a new {@code Event} object
|
||||||
*/
|
*/
|
||||||
|
@ -471,5 +599,4 @@ public class NativeInterface {
|
||||||
public static LastRunInfo getLastRunInfo() {
|
public static LastRunInfo getLastRunInfo() {
|
||||||
return getClient().getLastRunInfo();
|
return getClient().getLastRunInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import com.bugsnag.android.internal.JsonHelper
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,9 +60,9 @@ class NativeStackframe internal constructor(
|
||||||
writer.name("method").value(method)
|
writer.name("method").value(method)
|
||||||
writer.name("file").value(file)
|
writer.name("file").value(file)
|
||||||
writer.name("lineNumber").value(lineNumber)
|
writer.name("lineNumber").value(lineNumber)
|
||||||
writer.name("frameAddress").value(frameAddress)
|
frameAddress?.let { writer.name("frameAddress").value(JsonHelper.ulongToHex(frameAddress)) }
|
||||||
writer.name("symbolAddress").value(symbolAddress)
|
symbolAddress?.let { writer.name("symbolAddress").value(JsonHelper.ulongToHex(symbolAddress)) }
|
||||||
writer.name("loadAddress").value(loadAddress)
|
loadAddress?.let { writer.name("loadAddress").value(JsonHelper.ulongToHex(loadAddress)) }
|
||||||
writer.name("codeIdentifier").value(codeIdentifier)
|
writer.name("codeIdentifier").value(codeIdentifier)
|
||||||
writer.name("isPC").value(isPC)
|
writer.name("isPC").value(isPC)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the NDK plugin if it is loaded, otherwise does nothing / returns the default.
|
||||||
|
*/
|
||||||
|
internal object NdkPluginCaller {
|
||||||
|
private var ndkPlugin: Plugin? = null
|
||||||
|
private var setInternalMetricsEnabled: Method? = null
|
||||||
|
private var setStaticData: Method? = null
|
||||||
|
private var getSignalUnwindStackFunction: Method? = null
|
||||||
|
private var getCurrentCallbackSetCounts: Method? = null
|
||||||
|
private var getCurrentNativeApiCallUsage: Method? = null
|
||||||
|
private var initCallbackCounts: Method? = null
|
||||||
|
private var notifyAddCallback: Method? = null
|
||||||
|
private var notifyRemoveCallback: Method? = null
|
||||||
|
|
||||||
|
private fun getMethod(name: String, vararg parameterTypes: Class<*>): Method? {
|
||||||
|
val plugin = ndkPlugin
|
||||||
|
if (plugin == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return plugin.javaClass.getMethod(name, *parameterTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNdkPlugin(plugin: Plugin?) {
|
||||||
|
if (plugin != null) {
|
||||||
|
ndkPlugin = plugin
|
||||||
|
setInternalMetricsEnabled = getMethod("setInternalMetricsEnabled", Boolean::class.java)
|
||||||
|
setStaticData = getMethod("setStaticData", Map::class.java)
|
||||||
|
getSignalUnwindStackFunction = getMethod("getSignalUnwindStackFunction")
|
||||||
|
getCurrentCallbackSetCounts = getMethod("getCurrentCallbackSetCounts")
|
||||||
|
getCurrentNativeApiCallUsage = getMethod("getCurrentNativeApiCallUsage")
|
||||||
|
initCallbackCounts = getMethod("initCallbackCounts", Map::class.java)
|
||||||
|
notifyAddCallback = getMethod("notifyAddCallback", String::class.java)
|
||||||
|
notifyRemoveCallback = getMethod("notifyRemoveCallback", String::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSignalUnwindStackFunction(): Long {
|
||||||
|
val method = getSignalUnwindStackFunction
|
||||||
|
if (method != null) {
|
||||||
|
return method.invoke(ndkPlugin) as Long
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInternalMetricsEnabled(enabled: Boolean) {
|
||||||
|
val method = setInternalMetricsEnabled
|
||||||
|
if (method != null) {
|
||||||
|
method.invoke(ndkPlugin, enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentCallbackSetCounts(): Map<String, Int>? {
|
||||||
|
val method = getCurrentCallbackSetCounts
|
||||||
|
if (method != null) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return method.invoke(ndkPlugin) as Map<String, Int>
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentNativeApiCallUsage(): Map<String, Boolean>? {
|
||||||
|
val method = getCurrentNativeApiCallUsage
|
||||||
|
if (method != null) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return method.invoke(ndkPlugin) as Map<String, Boolean>
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initCallbackCounts(counts: Map<String, Int>) {
|
||||||
|
val method = initCallbackCounts
|
||||||
|
if (method != null) {
|
||||||
|
method.invoke(ndkPlugin, counts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyAddCallback(callback: String) {
|
||||||
|
val method = notifyAddCallback
|
||||||
|
if (method != null) {
|
||||||
|
method.invoke(ndkPlugin, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyRemoveCallback(callback: String) {
|
||||||
|
val method = notifyRemoveCallback
|
||||||
|
if (method != null) {
|
||||||
|
method.invoke(ndkPlugin, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStaticData(data: Map<String, Any>) {
|
||||||
|
val method = setStaticData
|
||||||
|
if (method != null) {
|
||||||
|
method.invoke(ndkPlugin, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import java.io.IOException
|
||||||
*/
|
*/
|
||||||
class Notifier @JvmOverloads constructor(
|
class Notifier @JvmOverloads constructor(
|
||||||
var name: String = "Android Bugsnag Notifier",
|
var name: String = "Android Bugsnag Notifier",
|
||||||
var version: String = "5.23.0",
|
var version: String = "5.28.2",
|
||||||
var url: String = "https://bugsnag.com"
|
var url: String = "https://bugsnag.com"
|
||||||
) : JsonStream.Streamable {
|
) : JsonStream.Streamable {
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,8 @@ internal class PluginClient(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getNdkPlugin(): Plugin? = ndkPlugin
|
||||||
|
|
||||||
fun loadPlugins(client: Client) {
|
fun loadPlugins(client: Client) {
|
||||||
plugins.forEach { plugin ->
|
plugins.forEach { plugin ->
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.bugsnag.android
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import com.bugsnag.android.internal.JsonHelper
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -103,13 +104,13 @@ class Stackframe : JsonStream.Streamable {
|
||||||
internal constructor(json: Map<String, Any?>) {
|
internal constructor(json: Map<String, Any?>) {
|
||||||
method = json["method"] as? String
|
method = json["method"] as? String
|
||||||
file = json["file"] as? String
|
file = json["file"] as? String
|
||||||
lineNumber = json["lineNumber"] as? Number
|
lineNumber = JsonHelper.jsonToLong(json["lineNumber"])
|
||||||
inProject = json["inProject"] as? Boolean
|
inProject = json["inProject"] as? Boolean
|
||||||
columnNumber = json["columnNumber"] as? Number
|
columnNumber = json["columnNumber"] as? Number
|
||||||
frameAddress = (json["frameAddress"] as? Number)?.toLong()
|
frameAddress = JsonHelper.jsonToLong(json["frameAddress"])
|
||||||
symbolAddress = (json["symbolAddress"] as? Number)?.toLong()
|
symbolAddress = JsonHelper.jsonToLong(json["symbolAddress"])
|
||||||
loadAddress = (json["loadAddress"] as? Number)?.toLong()
|
loadAddress = JsonHelper.jsonToLong(json["loadAddress"])
|
||||||
codeIdentifier = (json["codeIdentifier"] as? String)
|
codeIdentifier = json["codeIdentifier"] as? String
|
||||||
isPC = json["isPC"] as? Boolean
|
isPC = json["isPC"] as? Boolean
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@ -128,9 +129,9 @@ class Stackframe : JsonStream.Streamable {
|
||||||
|
|
||||||
writer.name("columnNumber").value(columnNumber)
|
writer.name("columnNumber").value(columnNumber)
|
||||||
|
|
||||||
frameAddress?.let { writer.name("frameAddress").value(it) }
|
frameAddress?.let { writer.name("frameAddress").value(JsonHelper.ulongToHex(frameAddress)) }
|
||||||
symbolAddress?.let { writer.name("symbolAddress").value(it) }
|
symbolAddress?.let { writer.name("symbolAddress").value(JsonHelper.ulongToHex(symbolAddress)) }
|
||||||
loadAddress?.let { writer.name("loadAddress").value(it) }
|
loadAddress?.let { writer.name("loadAddress").value(JsonHelper.ulongToHex(loadAddress)) }
|
||||||
codeIdentifier?.let { writer.name("codeIdentifier").value(it) }
|
codeIdentifier?.let { writer.name("codeIdentifier").value(it) }
|
||||||
isPC?.let { writer.name("isPC").value(it) }
|
isPC?.let { writer.name("isPC").value(it) }
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,12 @@ enum class Telemetry {
|
||||||
/**
|
/**
|
||||||
* Errors within the Bugsnag SDK.
|
* Errors within the Bugsnag SDK.
|
||||||
*/
|
*/
|
||||||
INTERNAL_ERRORS;
|
INTERNAL_ERRORS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Differences from the default configuration.
|
||||||
|
*/
|
||||||
|
USAGE;
|
||||||
|
|
||||||
internal companion object {
|
internal companion object {
|
||||||
fun fromString(str: String) = values().find { it.name == str } ?: INTERNAL_ERRORS
|
fun fromString(str: String) = values().find { it.name == str } ?: INTERNAL_ERRORS
|
||||||
|
|
|
@ -52,6 +52,7 @@ data class ImmutableConfig(
|
||||||
val maxReportedThreads: Int,
|
val maxReportedThreads: Int,
|
||||||
val persistenceDirectory: Lazy<File>,
|
val persistenceDirectory: Lazy<File>,
|
||||||
val sendLaunchCrashesSynchronously: Boolean,
|
val sendLaunchCrashesSynchronously: Boolean,
|
||||||
|
val attemptDeliveryOnCrash: Boolean,
|
||||||
|
|
||||||
// results cached here to avoid unnecessary lookups in Client.
|
// results cached here to avoid unnecessary lookups in Client.
|
||||||
val packageInfo: PackageInfo?,
|
val packageInfo: PackageInfo?,
|
||||||
|
@ -167,6 +168,7 @@ internal fun convertToImmutableConfig(
|
||||||
telemetry = config.telemetry.toSet(),
|
telemetry = config.telemetry.toSet(),
|
||||||
persistenceDirectory = persistenceDir,
|
persistenceDirectory = persistenceDir,
|
||||||
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously,
|
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously,
|
||||||
|
attemptDeliveryOnCrash = config.isAttemptDeliveryOnCrash,
|
||||||
packageInfo = packageInfo,
|
packageInfo = packageInfo,
|
||||||
appInfo = appInfo,
|
appInfo = appInfo,
|
||||||
redactedKeys = config.redactedKeys.toSet()
|
redactedKeys = config.redactedKeys.toSet()
|
||||||
|
@ -220,7 +222,12 @@ internal fun sanitiseConfiguration(
|
||||||
|
|
||||||
@Suppress("SENSELESS_COMPARISON")
|
@Suppress("SENSELESS_COMPARISON")
|
||||||
if (configuration.delivery == null) {
|
if (configuration.delivery == null) {
|
||||||
configuration.delivery = DefaultDelivery(connectivity, configuration.logger!!)
|
configuration.delivery = DefaultDelivery(
|
||||||
|
connectivity,
|
||||||
|
configuration.apiKey,
|
||||||
|
configuration.maxStringValueLength,
|
||||||
|
configuration.logger!!
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return convertToImmutableConfig(
|
return convertToImmutableConfig(
|
||||||
configuration,
|
configuration,
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores internal metrics for Bugsnag use.
|
||||||
|
*/
|
||||||
|
interface InternalMetrics {
|
||||||
|
/**
|
||||||
|
* Returns a map that can be merged with the top-level JSON report.
|
||||||
|
*/
|
||||||
|
fun toJsonableMap(): Map<String, Any>
|
||||||
|
|
||||||
|
fun setConfigDifferences(differences: Map<String, Any>)
|
||||||
|
|
||||||
|
fun setCallbackCounts(newCallbackCounts: Map<String, Int>)
|
||||||
|
|
||||||
|
fun notifyAddCallback(callback: String)
|
||||||
|
|
||||||
|
fun notifyRemoveCallback(callback: String)
|
||||||
|
|
||||||
|
fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int)
|
||||||
|
|
||||||
|
fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class TrimMetrics(
|
||||||
|
val itemsTrimmed: Int, // breadcrumbs, strings, whatever
|
||||||
|
val dataTrimmed: Int // chars, bytes, whatever
|
||||||
|
)
|
|
@ -0,0 +1,111 @@
|
||||||
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
import com.bugsnag.android.NdkPluginCaller
|
||||||
|
|
||||||
|
class InternalMetricsImpl(source: Map<String, Any>? = null) : InternalMetrics {
|
||||||
|
private val configDifferences: MutableMap<String, Any>
|
||||||
|
private val callbackCounts: MutableMap<String, Int>
|
||||||
|
private var metadataStringsTrimmedCount = 0
|
||||||
|
private var metadataCharsTruncatedCount = 0
|
||||||
|
private var breadcrumbsRemovedCount = 0
|
||||||
|
private var breadcrumbBytesRemovedCount = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (source != null) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
configDifferences = (source["config"] as MutableMap<String, Any>?) ?: hashMapOf()
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
callbackCounts = (source["callbacks"] as MutableMap<String, Int>?) ?: hashMapOf()
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val system = source["system"] as MutableMap<String, Any>?
|
||||||
|
if (system != null) {
|
||||||
|
metadataStringsTrimmedCount = (system["stringsTruncated"] as Number?)?.toInt() ?: 0
|
||||||
|
metadataCharsTruncatedCount = (system["stringCharsTruncated"] as Number?)?.toInt() ?: 0
|
||||||
|
breadcrumbsRemovedCount = (system["breadcrumbsRemovedCount"] as Number?)?.toInt() ?: 0
|
||||||
|
breadcrumbBytesRemovedCount = (system["breadcrumbBytesRemoved"] as Number?)?.toInt() ?: 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
configDifferences = hashMapOf()
|
||||||
|
callbackCounts = hashMapOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJsonableMap(): Map<String, Any> {
|
||||||
|
val callbacks = allCallbacks()
|
||||||
|
|
||||||
|
val system = listOfNotNull(
|
||||||
|
if (metadataStringsTrimmedCount > 0) "stringsTruncated" to metadataStringsTrimmedCount else null,
|
||||||
|
if (metadataCharsTruncatedCount > 0) "stringCharsTruncated" to metadataCharsTruncatedCount else null,
|
||||||
|
if (breadcrumbsRemovedCount > 0) "breadcrumbsRemoved" to breadcrumbsRemovedCount else null,
|
||||||
|
if (breadcrumbBytesRemovedCount > 0) "breadcrumbBytesRemoved" to breadcrumbBytesRemovedCount else null,
|
||||||
|
).toMap()
|
||||||
|
|
||||||
|
return listOfNotNull(
|
||||||
|
if (configDifferences.isNotEmpty()) "config" to configDifferences else null,
|
||||||
|
if (callbacks.isNotEmpty()) "callbacks" to callbacks else null,
|
||||||
|
if (system.isNotEmpty()) "system" to system else null,
|
||||||
|
).toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setConfigDifferences(differences: Map<String, Any>) {
|
||||||
|
configDifferences.clear()
|
||||||
|
configDifferences.putAll(differences)
|
||||||
|
// This is currently the only place where we set static data.
|
||||||
|
// When that changes in future, we'll need a StaticData object to properly merge data
|
||||||
|
// coming from multiple sources.
|
||||||
|
NdkPluginCaller.setStaticData(mapOf("usage" to mapOf("config" to configDifferences)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCallbackCounts(newCallbackCounts: Map<String, Int>) {
|
||||||
|
callbackCounts.clear()
|
||||||
|
callbackCounts.putAll(newCallbackCounts)
|
||||||
|
NdkPluginCaller.initCallbackCounts(newCallbackCounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notifyAddCallback(callback: String) {
|
||||||
|
modifyCallback(callback, 1)
|
||||||
|
NdkPluginCaller.notifyAddCallback(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notifyRemoveCallback(callback: String) {
|
||||||
|
modifyCallback(callback, -1)
|
||||||
|
NdkPluginCaller.notifyRemoveCallback(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun modifyCallback(callback: String, delta: Int) {
|
||||||
|
var currentValue = callbackCounts[callback] ?: 0
|
||||||
|
currentValue += delta
|
||||||
|
callbackCounts[callback] = currentValue.coerceAtLeast(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun allCallbacks(): Map<String, Any> {
|
||||||
|
val result = hashMapOf<String, Any>()
|
||||||
|
result.putAll(callbackCounts)
|
||||||
|
|
||||||
|
val counts = NdkPluginCaller.getCurrentCallbackSetCounts()
|
||||||
|
if (counts != null) {
|
||||||
|
// ndkOnError comes from the native side. The rest we already have.
|
||||||
|
val ndkOnError = counts["ndkOnError"]
|
||||||
|
if (ndkOnError != null) {
|
||||||
|
result["ndkOnError"] = ndkOnError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val usage = NdkPluginCaller.getCurrentNativeApiCallUsage()
|
||||||
|
if (usage != null) {
|
||||||
|
result.putAll(usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int) {
|
||||||
|
metadataStringsTrimmedCount = stringsTrimmed
|
||||||
|
metadataCharsTruncatedCount = charsRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int) {
|
||||||
|
breadcrumbsRemovedCount = breadcrumbsRemoved
|
||||||
|
breadcrumbBytesRemovedCount = bytesRemoved
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
class InternalMetricsNoop : InternalMetrics {
|
||||||
|
override fun toJsonableMap(): Map<String, Any> = emptyMap()
|
||||||
|
override fun setConfigDifferences(differences: Map<String, Any>) = Unit
|
||||||
|
override fun setCallbackCounts(newCallbackCounts: Map<String, Int>) = Unit
|
||||||
|
override fun notifyAddCallback(callback: String) = Unit
|
||||||
|
override fun notifyRemoveCallback(callback: String) = Unit
|
||||||
|
override fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int) = Unit
|
||||||
|
override fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int) = Unit
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
package com.bugsnag.android.internal
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
import com.bugsnag.android.JsonStream
|
||||||
import com.bugsnag.android.repackaged.dslplatform.json.DslJson
|
import com.bugsnag.android.repackaged.dslplatform.json.DslJson
|
||||||
import com.bugsnag.android.repackaged.dslplatform.json.JsonWriter
|
import com.bugsnag.android.repackaged.dslplatform.json.JsonWriter
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
@ -9,6 +11,7 @@ import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.io.PrintWriter
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
internal object JsonHelper {
|
internal object JsonHelper {
|
||||||
|
@ -31,6 +34,20 @@ internal object JsonHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun serialize(streamable: JsonStream.Streamable): ByteArray {
|
||||||
|
return ByteArrayOutputStream().use { baos ->
|
||||||
|
JsonStream(PrintWriter(baos)).use(streamable::toStream)
|
||||||
|
baos.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun serialize(value: Any): ByteArray {
|
||||||
|
return ByteArrayOutputStream().use { baos ->
|
||||||
|
serialize(value, baos)
|
||||||
|
baos.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun serialize(value: Any, stream: OutputStream) {
|
fun serialize(value: Any, stream: OutputStream) {
|
||||||
dslJson.serialize(value, stream)
|
dslJson.serialize(value, stream)
|
||||||
}
|
}
|
||||||
|
@ -76,4 +93,68 @@ internal object JsonHelper {
|
||||||
throw IOException("Could not deserialize from $file", ex)
|
throw IOException("Could not deserialize from $file", ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a long that technically contains an unsigned long value into its (unsigned) hex string equivalent.
|
||||||
|
* Negative values are interpreted as if the sign bit is the high bit of an unsigned integer.
|
||||||
|
*
|
||||||
|
* Returns null if null is passed in.
|
||||||
|
*/
|
||||||
|
fun ulongToHex(value: Long?): String? {
|
||||||
|
return if (value == null) {
|
||||||
|
null
|
||||||
|
} else if (value >= 0) {
|
||||||
|
"0x%x".format(value)
|
||||||
|
} else {
|
||||||
|
return "0x%x%02x".format(value.ushr(8), value.and(0xff))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a JSON-decoded value into a long. Accepts numeric types, or numeric encoded strings
|
||||||
|
* (e.g. "1234", "0xb1ff").
|
||||||
|
*
|
||||||
|
* Returns null if null or an empty string is passed in.
|
||||||
|
*/
|
||||||
|
fun jsonToLong(value: Any?): Long? {
|
||||||
|
return when (value) {
|
||||||
|
null -> null
|
||||||
|
is Number -> value.toLong()
|
||||||
|
is String -> {
|
||||||
|
if (value.length == 0) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
java.lang.Long.decode(value)
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
// Check if the value overflows a long, and correct for it.
|
||||||
|
if (value.startsWith("0x")) {
|
||||||
|
// All problematic hex values (e.g. 0x8000000000000000) have 18 characters
|
||||||
|
if (value.length != 18) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
// Decode all but the last byte, then shift and add it.
|
||||||
|
// This overflows and gives the "correct" signed result.
|
||||||
|
val headLength = value.length - 2
|
||||||
|
java.lang.Long.decode(value.substring(0, headLength))
|
||||||
|
.shl(8)
|
||||||
|
.or(value.substring(headLength, value.length).toLong(16))
|
||||||
|
} else {
|
||||||
|
// The first problematic decimal value (9223372036854775808) has 19 digits
|
||||||
|
if (value.length < 19) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
// Decode all but the last 3 chars, then multiply and add them.
|
||||||
|
// This overflows and gives the "correct" signed result.
|
||||||
|
val headLength = value.length - 3
|
||||||
|
java.lang.Long.decode(value.substring(0, headLength)) *
|
||||||
|
1000 +
|
||||||
|
java.lang.Long.decode(value.substring(headLength, value.length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("Cannot convert " + value + " to long")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
import java.util.EnumMap
|
||||||
|
import java.util.Hashtable
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.TreeMap
|
||||||
|
import java.util.Vector
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
import java.util.concurrent.ConcurrentMap
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
internal object StringUtils {
|
||||||
|
private const val trimMessageLength = "***<9> CHARS TRUNCATED***".length
|
||||||
|
|
||||||
|
fun stringTrimmedTo(maxLength: Int, str: String): String {
|
||||||
|
val excessCharCount = str.length - maxLength
|
||||||
|
return when {
|
||||||
|
excessCharCount < trimMessageLength -> str
|
||||||
|
else -> "${str.substring(0, maxLength)}***<$excessCharCount> CHARS TRUNCATED***"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
fun trimStringValuesTo(maxStringLength: Int, list: MutableList<Any?>): TrimMetrics {
|
||||||
|
var stringCount = 0
|
||||||
|
var charCount = 0
|
||||||
|
|
||||||
|
repeat(list.size) { index ->
|
||||||
|
trimValue(maxStringLength, list[index]) { newValue, stringTrimmed, charsTrimmed ->
|
||||||
|
list[index] = newValue
|
||||||
|
stringCount += stringTrimmed
|
||||||
|
charCount += charsTrimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrimMetrics(stringCount, charCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
fun trimStringValuesTo(maxStringLength: Int, map: MutableMap<String, Any?>): TrimMetrics {
|
||||||
|
var stringCount = 0
|
||||||
|
var charCount = 0
|
||||||
|
map.entries.forEach { entry ->
|
||||||
|
trimValue(maxStringLength, entry.value) { newValue, stringTrimmed, charsTrimmed ->
|
||||||
|
entry.setValue(newValue)
|
||||||
|
stringCount += stringTrimmed
|
||||||
|
charCount += charsTrimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrimMetrics(stringCount, charCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
private inline fun trimValue(
|
||||||
|
maxStringLength: Int,
|
||||||
|
value: Any?,
|
||||||
|
update: (newValue: Any, stringTrimmed: Int, charsTrimmed: Int) -> Unit
|
||||||
|
) {
|
||||||
|
if (value is String && value.length > maxStringLength) {
|
||||||
|
update(stringTrimmedTo(maxStringLength, value), 1, value.length - maxStringLength)
|
||||||
|
} else if (value.isDefinitelyMutableMap()) {
|
||||||
|
val (innerStringCount, innerCharCount) = trimStringValuesTo(
|
||||||
|
maxStringLength,
|
||||||
|
value as MutableMap<String, Any?>
|
||||||
|
)
|
||||||
|
|
||||||
|
update(value, innerStringCount, innerCharCount)
|
||||||
|
} else if (value.isDefinitelyMutableList()) {
|
||||||
|
val (innerStringCount, innerCharCount) = trimStringValuesTo(
|
||||||
|
maxStringLength,
|
||||||
|
value as MutableList<Any?>
|
||||||
|
)
|
||||||
|
|
||||||
|
update(value, innerStringCount, innerCharCount)
|
||||||
|
} else if (value is Map<*, *>) {
|
||||||
|
val newValue = value.toMutableMap() as MutableMap<String, Any?>
|
||||||
|
val (innerStringCount, innerCharCount) = trimStringValuesTo(maxStringLength, newValue)
|
||||||
|
update(newValue, innerStringCount, innerCharCount)
|
||||||
|
} else if (value is Collection<*>) {
|
||||||
|
val newValue = value.toMutableList()
|
||||||
|
val (innerStringCount, innerCharCount) = trimStringValuesTo(maxStringLength, newValue)
|
||||||
|
update(newValue, innerStringCount, innerCharCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In order to avoid surprises we have a small list of commonly used Map types that are known
|
||||||
|
* to be mutable (avoiding issues around Kotlin trying to determine whether
|
||||||
|
* `Collections.singletonMap` (and such) is mutable or not).
|
||||||
|
*
|
||||||
|
* It is technically possible that a HashMap was extended to be immutable, but it's unlikely.
|
||||||
|
*/
|
||||||
|
private fun Any?.isDefinitelyMutableMap() =
|
||||||
|
this is HashMap<*, *> ||
|
||||||
|
this is TreeMap<*, *> ||
|
||||||
|
this is ConcurrentMap<*, *> || // concurrent automatically implies mutability
|
||||||
|
this is EnumMap<*, *> ||
|
||||||
|
this is Hashtable<*, *> ||
|
||||||
|
this is WeakHashMap<*, *>
|
||||||
|
|
||||||
|
private fun Any?.isDefinitelyMutableList() =
|
||||||
|
this is ArrayList<*> ||
|
||||||
|
this is LinkedList<*> ||
|
||||||
|
this is CopyOnWriteArrayList<*> ||
|
||||||
|
this is Vector<*>
|
||||||
|
}
|
|
@ -11,48 +11,3 @@ index 0ce2eec8c4..e1bac196e2 100644
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
logger.w("Unexpected error delivering payload", exception)
|
logger.w("Unexpected error delivering payload", exception)
|
||||||
return DeliveryStatus.FAILURE
|
return DeliveryStatus.FAILURE
|
||||||
diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch
|
|
||||||
index c762d488a1..e69de29bb2 100644
|
|
||||||
--- a/patches/Bugsnag.patch
|
|
||||||
+++ b/patches/Bugsnag.patch
|
|
||||||
@@ -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 0ce2eec8c..e1bac196e 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(
|
|
||||||
- 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
|
|
||||||
-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 New Issue