Updated Bugsnag

This commit is contained in:
M66B 2022-11-12 21:57:15 +01:00
parent 1da91952f6
commit 084d0e1536
35 changed files with 1333 additions and 231 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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