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 openpgp_version = "12.0"
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 vcard_version = "0.11.3"
def relinker_version = "1.4.5"

View File

@ -3,13 +3,16 @@ package com.bugsnag.android
import androidx.annotation.VisibleForTesting
import java.util.concurrent.BlockingQueue
import java.util.concurrent.Callable
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.FutureTask
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.lang.Thread as JThread
/**
* 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 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 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
// 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(
// these executors must remain single-threaded - the SDK makes assumptions
// about synchronization based on this.
@VisibleForTesting
internal val errorExecutor: ThreadPoolExecutor = createExecutor(
@get:VisibleForTesting
internal val errorExecutor: ExecutorService = createExecutor(
"Bugsnag Error thread",
TaskType.ERROR_REQUEST,
true
),
@VisibleForTesting
internal val sessionExecutor: ThreadPoolExecutor = createExecutor(
@get:VisibleForTesting
internal val sessionExecutor: ExecutorService = createExecutor(
"Bugsnag Session thread",
TaskType.SESSION_REQUEST,
true
),
@VisibleForTesting
internal val ioExecutor: ThreadPoolExecutor = createExecutor(
@get:VisibleForTesting
internal val ioExecutor: ExecutorService = createExecutor(
"Bugsnag IO thread",
TaskType.IO,
true
),
@VisibleForTesting
internal val internalReportExecutor: ThreadPoolExecutor = createExecutor(
@get:VisibleForTesting
internal val internalReportExecutor: ExecutorService = createExecutor(
"Bugsnag Internal Report thread",
TaskType.INTERNAL_REPORT,
false
),
@VisibleForTesting
internal val defaultExecutor: ThreadPoolExecutor = createExecutor(
@get:VisibleForTesting
internal val defaultExecutor: ExecutorService = createExecutor(
"Bugsnag Default thread",
TaskType.DEFAULT,
false
)
) {
@ -138,13 +151,17 @@ internal class BackgroundTaskService(
*/
@Throws(RejectedExecutionException::class)
fun <T> submitTask(taskType: TaskType, callable: Callable<T>): Future<T> {
return when (taskType) {
TaskType.ERROR_REQUEST -> errorExecutor.submit(callable)
TaskType.SESSION_REQUEST -> sessionExecutor.submit(callable)
TaskType.IO -> ioExecutor.submit(callable)
TaskType.INTERNAL_REPORT -> internalReportExecutor.submit(callable)
TaskType.DEFAULT -> defaultExecutor.submit(callable)
val task = FutureTask(callable)
when (taskType) {
TaskType.ERROR_REQUEST -> errorExecutor.execute(task)
TaskType.SESSION_REQUEST -> sessionExecutor.execute(task)
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()
}
private fun ThreadPoolExecutor.awaitTerminationSafe() {
private fun ExecutorService.awaitTerminationSafe() {
try {
awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)
} catch (ignored: InterruptedException) {
// 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
import com.bugsnag.android.internal.StringUtils
import com.bugsnag.android.internal.TrimMetrics
import java.io.IOException
import java.util.Date
@ -22,6 +24,11 @@ internal class BreadcrumbInternal internal constructor(
Date()
)
internal fun trimMetadataStringsTo(maxStringLength: Int): TrimMetrics {
val metadata = this.metadata ?: return TrimMetrics(0, 0)
return StringUtils.trimStringValuesTo(maxStringLength, metadata)
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginObject()

View File

@ -1,6 +1,7 @@
package com.bugsnag.android
import com.bugsnag.android.internal.DateUtils
import com.bugsnag.android.internal.InternalMetricsImpl
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
@ -17,7 +18,7 @@ internal class BugsnagEventMapper(
@Suppress("UNCHECKED_CAST")
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
// no stacktrace was gathered.
@ -86,6 +87,9 @@ internal class BugsnagEventMapper(
event.updateSeverityReasonInternal(reason)
event.normalizeStackframeErrorTypes()
// populate internalMetrics
event.internalMetrics = InternalMetricsImpl(map["usage"] as MutableMap<String, Any>?)
return event
}
@ -184,31 +188,7 @@ internal class BugsnagEventMapper(
}
internal fun convertStacktrace(trace: List<Map<String, Any?>>): Stacktrace {
return Stacktrace(trace.map { convertStackframe(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)
return Stacktrace(trace.map { Stackframe(it) })
}
internal fun deserializeSeverityReason(

View File

@ -1,6 +1,6 @@
package com.bugsnag.android
import com.bugsnag.android.internal.dag.ConfigModule
import com.bugsnag.android.internal.ImmutableConfig
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.
*/
internal class BugsnagStateModule(
configModule: ConfigModule,
cfg: ImmutableConfig,
configuration: Configuration
) : DependencyModule() {
private val cfg = configModule.config
val clientObservable = ClientObservable()
val callbackState = configuration.impl.callbackState.copy()
val callbackState = configuration.impl.callbackState
val contextState = ContextState().apply {
if (configuration.context != null) {

View File

@ -1,5 +1,7 @@
package com.bugsnag.android
import com.bugsnag.android.internal.InternalMetrics
import com.bugsnag.android.internal.InternalMetricsNoop
import java.util.concurrent.CopyOnWriteArrayList
internal data class CallbackState(
@ -9,36 +11,66 @@ internal data class CallbackState(
val onSendTasks: MutableCollection<OnSendCallback> = CopyOnWriteArrayList()
) : 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) {
onErrorTasks.add(onError)
if (onErrorTasks.add(onError)) {
internalMetrics.notifyAddCallback(onErrorName)
}
}
override fun removeOnError(onError: OnErrorCallback) {
onErrorTasks.remove(onError)
if (onErrorTasks.remove(onError)) {
internalMetrics.notifyRemoveCallback(onErrorName)
}
}
override fun addOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
onBreadcrumbTasks.add(onBreadcrumb)
if (onBreadcrumbTasks.add(onBreadcrumb)) {
internalMetrics.notifyAddCallback(onBreadcrumbName)
}
}
override fun removeOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
onBreadcrumbTasks.remove(onBreadcrumb)
if (onBreadcrumbTasks.remove(onBreadcrumb)) {
internalMetrics.notifyRemoveCallback(onBreadcrumbName)
}
}
override fun addOnSession(onSession: OnSessionCallback) {
onSessionTasks.add(onSession)
if (onSessionTasks.add(onSession)) {
internalMetrics.notifyAddCallback(onSessionName)
}
}
override fun removeOnSession(onSession: OnSessionCallback) {
onSessionTasks.remove(onSession)
if (onSessionTasks.remove(onSession)) {
internalMetrics.notifyRemoveCallback(onSessionName)
}
}
fun addOnSend(onSend: OnSendCallback) {
onSendTasks.add(onSend)
if (onSendTasks.add(onSend)) {
internalMetrics.notifyAddCallback(onSendName)
}
}
fun removeOnSend(onSend: OnSendCallback) {
onSendTasks.remove(onSend)
if (onSendTasks.remove(onSend)) {
internalMetrics.notifyRemoveCallback(onSendName)
}
}
fun runOnErrorTasks(event: Event, logger: Logger): Boolean {
@ -120,4 +152,13 @@ internal data class CallbackState(
onSessionTasks = onSessionTasks,
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 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.dag.ConfigModule;
import com.bugsnag.android.internal.dag.ContextModule;
@ -16,7 +19,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.functions.Function2;
import java.io.File;
@ -49,9 +51,11 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
final MetadataState metadataState;
final FeatureFlagState featureFlagState;
private final InternalMetrics internalMetrics;
private final ContextState contextState;
private final CallbackState callbackState;
private final UserState userState;
private final Map<String, Object> configDifferences;
final Context appContext;
@ -140,7 +144,18 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
ConfigModule configModule = new ConfigModule(contextModule, configuration, connectivity);
immutableConfig = configModule.getConfig();
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
final StorageModule storageModule = new StorageModule(appContext,
@ -148,7 +163,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
// setup state trackers for bugsnag
BugsnagStateModule bugsnagStateModule = new BugsnagStateModule(
configModule, configuration);
immutableConfig, configuration);
clientObservable = bugsnagStateModule.getClientObservable();
callbackState = bugsnagStateModule.getCallbackState();
breadcrumbState = bugsnagStateModule.getBreadcrumbState();
@ -180,8 +195,6 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
userState = storageModule.getUserStore().load(configuration.getUser());
storageModule.getSharedPrefMigrator().deleteLegacyPrefs();
registerLifecycleCallbacks();
EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule,
dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier,
callbackState);
@ -191,33 +204,25 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
deliveryDelegate = new DeliveryDelegate(logger, eventStore,
immutableConfig, callbackState, notifier, bgTaskService);
// Install a default exception handler with this client
exceptionHandler = new ExceptionHandler(this, logger);
if (immutableConfig.getEnabledErrorTypes().getUnhandledExceptions()) {
exceptionHandler.install();
}
// load last run info
lastRunInfoStore = storageModule.getLastRunInfoStore();
lastRunInfo = storageModule.getLastRunInfo();
// initialise plugins before attempting to flush any errors
loadPlugins(configuration);
Set<Plugin> userPlugins = configuration.getPlugins();
pluginClient = new PluginClient(userPlugins, immutableConfig, logger);
// Flush any on-disk errors and sessions
eventStore.flushOnLaunch();
eventStore.flushAsync();
sessionTracker.flushAsync();
if (configuration.getTelemetry().contains(Telemetry.USAGE)) {
internalMetrics = new InternalMetricsImpl();
} else {
internalMetrics = new InternalMetricsNoop();
}
// register listeners for system events in the background.
configDifferences = configuration.impl.getConfigDifferences();
systemBroadcastReceiver = new SystemBroadcastReceiver(this, logger);
registerComponentCallbacks();
registerListenersInBackground();
// leave auto breadcrumb
Map<String, Object> data = Collections.emptyMap();
leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data);
logger.d("Bugsnag loaded");
start();
}
@VisibleForTesting
@ -266,6 +271,42 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
this.lastRunInfo = null;
this.exceptionHandler = exceptionHandler;
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() {
@ -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) {
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>() {
@Override
public Boolean call() {
File outFile = new File(NativeInterface.getNativeReportPath());
File outFile = NativeInterface.getNativeReportPath();
return outFile.exists() || outFile.mkdirs();
}
}).get();
@ -746,6 +780,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
// Attach context to the event
event.setContext(contextState.getContext());
event.setInternalMetrics(internalMetrics);
notifyInternal(event, onError);
}
@ -1064,20 +1101,6 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
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() {
return immutableConfig;
}

View File

@ -42,6 +42,7 @@ internal class ConfigInternal(
var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS
var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS
var maxReportedThreads: Int = DEFAULT_MAX_REPORTED_THREADS
var maxStringValueLength: Int = DEFAULT_MAX_STRING_VALUE_LENGTH
var context: String? = null
var redactedKeys: Set<String>
@ -53,10 +54,12 @@ internal class ConfigInternal(
var discardClasses: Set<String> = emptySet()
var enabledReleaseStages: Set<String>? = null
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null
var telemetry: Set<Telemetry> = EnumSet.of(Telemetry.INTERNAL_ERRORS)
var telemetry: Set<Telemetry> = EnumSet.of(Telemetry.INTERNAL_ERRORS, Telemetry.USAGE)
var projectPackages: Set<String> = emptySet()
var persistenceDirectory: File? = null
var attemptDeliveryOnCrash: Boolean = false
val notifier: Notifier = Notifier()
protected val plugins = HashSet<Plugin>()
@ -98,12 +101,59 @@ internal class ConfigInternal(
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 {
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_EVENTS = 32
private const val DEFAULT_MAX_REPORTED_THREADS = 200
private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000
private const val DEFAULT_MAX_STRING_VALUE_LENGTH = 10000
@JvmStatic
fun load(context: Context): Configuration = load(context, null)

View File

@ -7,7 +7,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.io.File;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@ -19,7 +18,7 @@ import java.util.Set;
public class Configuration implements CallbackAware, MetadataAware, UserAware, FeatureFlagAware {
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 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,
* 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() {
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,
* 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) {
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
* 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() {
return impl.getPlugins();
}

View File

@ -1,45 +1,79 @@
package com.bugsnag.android
import android.net.TrafficStats
import java.io.ByteArrayOutputStream
import com.bugsnag.android.internal.JsonHelper
import java.io.IOException
import java.io.PrintWriter
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
/**
* 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(
private val connectivity: Connectivity?,
val logger: Logger
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, payload, deliveryParams.headers)
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 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")
return status
}
fun deliver(
urlString: String,
streamable: JsonStream.Streamable,
json: ByteArray,
headers: Map<String, String?>
): DeliveryStatus {
@ -50,7 +84,6 @@ internal class DefaultDelivery(
var conn: HttpURLConnection? = null
try {
val json = serializeJsonPayload(streamable)
conn = makeRequest(URL(urlString), json, headers)
// 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.VisibleForTesting;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
class DeliveryDelegate extends BaseObservable {
@VisibleForTesting
static long DELIVERY_TIMEOUT = 3000L;
final Logger logger;
private final EventStore eventStore;
private final ImmutableConfig immutableConfig;
@ -55,7 +56,13 @@ class DeliveryDelegate extends BaseObservable {
String severityReasonType = event.getImpl().getSeverityReasonType();
boolean promiseRejection = REASON_PROMISE_REJECTION.equals(severityReasonType);
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)) {
// Build the eventPayload
String apiKey = event.getApiKey();
@ -107,6 +114,24 @@ class DeliveryDelegate extends BaseObservable {
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) {
eventStore.write(event);
if (attemptSend) {

View File

@ -33,4 +33,20 @@ class ErrorTypes(
internal constructor(detectErrors: Boolean) : this(detectErrors, detectErrors, detectErrors, detectErrors)
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;
import com.bugsnag.android.internal.ImmutableConfig;
import com.bugsnag.android.internal.InternalMetrics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -9,7 +10,6 @@ import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 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();
}
/**
* 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
* 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) {
impl.setRedactedKeys(redactedKeys);
}
void setInternalMetrics(InternalMetrics metrics) {
impl.setInternalMetrics(metrics);
}
}

View File

@ -1,6 +1,10 @@
package com.bugsnag.android
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
internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware {
@ -14,6 +18,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
featureFlags: FeatureFlags = FeatureFlags()
) : this(
config.apiKey,
config.logger,
mutableListOf(),
config.discardClasses.toSet(),
when (originalError) {
@ -32,6 +37,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
internal constructor(
apiKey: String,
logger: Logger,
breadcrumbs: MutableList<Breadcrumb> = mutableListOf(),
discardClasses: Set<String> = setOf(),
errors: MutableList<Error> = mutableListOf(),
@ -44,6 +50,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
user: User = User(),
redactionKeys: Set<String>? = null
) {
this.logger = logger
this.apiKey = apiKey
this.breadcrumbs = breadcrumbs
this.discardClasses = discardClasses
@ -64,6 +71,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
val originalError: Throwable?
internal var severityReason: SeverityReason
val logger: Logger
val metadata: Metadata
val featureFlags: FeatureFlags
private val discardClasses: Set<String>
@ -103,6 +111,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
jsonStreamer.redactedKeys = value.toSet()
metadata.redactedKeys = value.toSet()
}
var internalMetrics: InternalMetrics = InternalMetricsNoop()
/**
* @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("breadcrumbs").value(breadcrumbs)
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.beginArray()
@ -229,6 +247,41 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
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?) {
userImpl = User(id, email, name)
}

View File

@ -12,17 +12,21 @@ import java.io.IOException
*/
class EventPayload @JvmOverloads internal constructor(
var apiKey: String?,
val event: Event? = null,
event: Event? = null,
internal val eventFile: File? = null,
notifier: Notifier,
private val config: ImmutableConfig
) : JsonStream.Streamable {
var event = event
internal set(value) { field = value }
internal val notifier = Notifier(notifier.name, notifier.version, notifier.url).apply {
dependencies = notifier.dependencies.toMutableList()
}
internal fun getErrorTypes(): Set<ErrorType> {
val event = this.event
return when {
event != null -> event.impl.getErrorTypesFromStackframes()
eventFile != null -> EventFilenameInfo.fromFile(eventFile, config).errorTypes

View File

@ -13,6 +13,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
@ -132,6 +133,26 @@ class EventStore extends FileStore {
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
*/
@ -163,7 +184,7 @@ class EventStore extends FileStore {
}
}
private void flushEventFile(File eventFile) {
void flushEventFile(File eventFile) {
try {
EventFilenameInfo eventInfo = EventFilenameInfo.fromFile(eventFile, config);
String apiKey = eventInfo.getApiKey();

View File

@ -1,39 +1,40 @@
package com.bugsnag.android
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
internal class FeatureFlags(
internal val store: MutableMap<String, String?> = ConcurrentHashMap()
internal val store: MutableMap<String, String?> = mutableMapOf()
) : JsonStream.Streamable, FeatureFlagAware {
private val emptyVariant = "__EMPTY_VARIANT_SENTINEL__"
override fun addFeatureFlag(name: String) {
store[name] = emptyVariant
@Synchronized override fun addFeatureFlag(name: String) {
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
}
override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
@Synchronized override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
featureFlags.forEach { (name, variant) ->
addFeatureFlag(name, variant)
}
}
override fun clearFeatureFlag(name: String) {
@Synchronized override fun clearFeatureFlag(name: String) {
store.remove(name)
}
override fun clearFeatureFlags() {
@Synchronized override fun clearFeatureFlags() {
store.clear()
}
@Throws(IOException::class)
override fun toStream(stream: JsonStream) {
val storeCopy = synchronized(this) { store.toMap() }
stream.beginArray()
store.forEach { (name, variant) ->
storeCopy.forEach { (name, variant) ->
stream.beginObject()
stream.name("featureFlag").value(name)
if (variant != emptyVariant) {
@ -44,9 +45,9 @@ internal class FeatureFlags(
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 })
}
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 com.bugsnag.android.internal.ImmutableConfig;
import com.bugsnag.android.internal.JsonHelper;
import android.annotation.SuppressLint;
import android.content.Context;
@ -121,7 +122,11 @@ class InternalReportDelegate implements EventStore.Delegate {
headers.put(HEADER_INTERNAL_ERROR, "bugsnag-android");
headers.remove(DeliveryHeadersKt.HEADER_API_KEY);
DefaultDelivery defaultDelivery = (DefaultDelivery) delivery;
defaultDelivery.deliver(params.getEndpoint(), payload, headers);
defaultDelivery.deliver(
params.getEndpoint(),
JsonHelper.INSTANCE.serialize(payload),
headers
);
}
} 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 SEND_LAUNCH_CRASHES_SYNCHRONOUSLY = "$BUGSNAG_NS.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY"
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 {
@ -91,6 +92,10 @@ internal class ManifestConfigLoader {
SEND_LAUNCH_CRASHES_SYNCHRONOUSLY,
sendLaunchCrashesSynchronously
)
isAttemptDeliveryOnCrash = data.getBoolean(
ATTEMPT_DELIVERY_ON_CRASH,
isAttemptDeliveryOnCrash
)
}
}
return config

View File

@ -2,6 +2,8 @@
package com.bugsnag.android
import com.bugsnag.android.internal.StringUtils
import com.bugsnag.android.internal.TrimMetrics
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
@ -137,4 +139,19 @@ internal data class Metadata @JvmOverloads constructor(
return this.copy(store = toMap())
.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;
import com.bugsnag.android.internal.ImmutableConfig;
import com.bugsnag.android.internal.JsonHelper;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
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
*/
@ -54,10 +79,16 @@ public class NativeInterface {
* Retrieves the directory used to store native crash reports
*/
@NonNull
public static String getNativeReportPath() {
ImmutableConfig config = getClient().getConfig();
File persistenceDirectory = config.getPersistenceDirectory().getValue();
return new File(persistenceDirectory, "bugsnag-native").getAbsolutePath();
public static File getNativeReportPath() {
return getNativeReportPath(getPersistenceDirectory());
}
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
@SuppressWarnings("unused")
public static Map<String,String> getUser() {
public static Map<String, String> getUser() {
HashMap<String, String> userData = new HashMap<>();
User user = getClient().getUser();
userData.put("id", user.getId());
@ -79,8 +110,8 @@ public class NativeInterface {
*/
@NonNull
@SuppressWarnings("unused")
public static Map<String,Object> getApp() {
HashMap<String,Object> data = new HashMap<>();
public static Map<String, Object> getApp() {
HashMap<String, Object> data = new HashMap<>();
AppDataCollector source = getClient().getAppDataCollector();
AppWithState app = source.generateAppWithState();
data.put("version", app.getVersion());
@ -103,7 +134,7 @@ public class NativeInterface {
*/
@NonNull
@SuppressWarnings("unused")
public static Map<String,Object> getDevice() {
public static Map<String, Object> getDevice() {
DeviceDataCollector source = getClient().getDeviceDataCollector();
HashMap<String, Object> deviceData = new HashMap<>(source.getDeviceMetadata());
@ -152,9 +183,9 @@ public class NativeInterface {
/**
* Sets the user
*
* @param id id
* @param id id
* @param email email
* @param name name
* @param name name
*/
@SuppressWarnings("unused")
public static void setUser(@Nullable final String id,
@ -167,9 +198,9 @@ public class NativeInterface {
/**
* Sets the user
*
* @param idBytes id
* @param idBytes id
* @param emailBytes email
* @param nameBytes name
* @param nameBytes name
*/
@SuppressWarnings("unused")
public static void setUser(@Nullable final byte[] idBytes,
@ -300,6 +331,36 @@ public class NativeInterface {
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.
*
@ -307,18 +368,31 @@ public class NativeInterface {
* captured. Used to determine whether the report
* should be discarded, based on configured release
* stages
* @param payloadBytes The raw JSON payload of the event
* @param apiKey The apiKey for the event
* @param isLaunching whether the crash occurred when the app was launching
* @param payloadBytes The raw JSON payload of the event
* @param apiKey The apiKey for the event
* @param isLaunching whether the crash occurred when the app was launching
*/
@SuppressWarnings("unused")
public static void deliverReport(@Nullable byte[] releaseStageBytes,
@NonNull byte[] payloadBytes,
@Nullable byte[] staticDataBytes,
@NonNull String apiKey,
boolean isLaunching) {
if (payloadBytes == null) {
return;
// If there's saved static data, merge it directly into the payload map.
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 releaseStage = releaseStageBytes == null
? null
@ -341,10 +415,10 @@ public class NativeInterface {
/**
* Notifies using the Android SDK
*
* @param nameBytes the error name
* @param nameBytes the error name
* @param messageBytes the error message
* @param severity the error severity
* @param stacktrace a stacktrace
* @param severity the error severity
* @param stacktrace a stacktrace
*/
public static void notify(@NonNull final byte[] nameBytes,
@NonNull final byte[] messageBytes,
@ -361,9 +435,9 @@ public class NativeInterface {
/**
* Notifies using the Android SDK
*
* @param name the error name
* @param message the error message
* @param severity the error severity
* @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,
@ -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
*
* @param exc the Throwable object that caused the event
* @param client the Client object that the event is associated with
* @param exc the Throwable object that caused the event
* @param client the Client object that the event is associated with
* @param severityReason the severity of the Event
* @return a new {@code Event} object
*/
@ -471,5 +599,4 @@ public class NativeInterface {
public static LastRunInfo getLastRunInfo() {
return getClient().getLastRunInfo();
}
}

View File

@ -1,5 +1,6 @@
package com.bugsnag.android
import com.bugsnag.android.internal.JsonHelper
import java.io.IOException
/**
@ -59,9 +60,9 @@ class NativeStackframe internal constructor(
writer.name("method").value(method)
writer.name("file").value(file)
writer.name("lineNumber").value(lineNumber)
writer.name("frameAddress").value(frameAddress)
writer.name("symbolAddress").value(symbolAddress)
writer.name("loadAddress").value(loadAddress)
frameAddress?.let { writer.name("frameAddress").value(JsonHelper.ulongToHex(frameAddress)) }
symbolAddress?.let { writer.name("symbolAddress").value(JsonHelper.ulongToHex(symbolAddress)) }
loadAddress?.let { writer.name("loadAddress").value(JsonHelper.ulongToHex(loadAddress)) }
writer.name("codeIdentifier").value(codeIdentifier)
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(
var name: String = "Android Bugsnag Notifier",
var version: String = "5.23.0",
var version: String = "5.28.2",
var url: String = "https://bugsnag.com"
) : JsonStream.Streamable {

View File

@ -45,6 +45,8 @@ internal class PluginClient(
}
}
fun getNdkPlugin(): Plugin? = ndkPlugin
fun loadPlugins(client: Client) {
plugins.forEach { plugin ->
try {

View File

@ -1,5 +1,6 @@
package com.bugsnag.android
import com.bugsnag.android.internal.JsonHelper
import java.io.IOException
/**
@ -103,13 +104,13 @@ class Stackframe : JsonStream.Streamable {
internal constructor(json: Map<String, Any?>) {
method = json["method"] as? String
file = json["file"] as? String
lineNumber = json["lineNumber"] as? Number
lineNumber = JsonHelper.jsonToLong(json["lineNumber"])
inProject = json["inProject"] as? Boolean
columnNumber = json["columnNumber"] as? Number
frameAddress = (json["frameAddress"] as? Number)?.toLong()
symbolAddress = (json["symbolAddress"] as? Number)?.toLong()
loadAddress = (json["loadAddress"] as? Number)?.toLong()
codeIdentifier = (json["codeIdentifier"] as? String)
frameAddress = JsonHelper.jsonToLong(json["frameAddress"])
symbolAddress = JsonHelper.jsonToLong(json["symbolAddress"])
loadAddress = JsonHelper.jsonToLong(json["loadAddress"])
codeIdentifier = json["codeIdentifier"] as? String
isPC = json["isPC"] as? Boolean
@Suppress("UNCHECKED_CAST")
@ -128,9 +129,9 @@ class Stackframe : JsonStream.Streamable {
writer.name("columnNumber").value(columnNumber)
frameAddress?.let { writer.name("frameAddress").value(it) }
symbolAddress?.let { writer.name("symbolAddress").value(it) }
loadAddress?.let { writer.name("loadAddress").value(it) }
frameAddress?.let { writer.name("frameAddress").value(JsonHelper.ulongToHex(frameAddress)) }
symbolAddress?.let { writer.name("symbolAddress").value(JsonHelper.ulongToHex(symbolAddress)) }
loadAddress?.let { writer.name("loadAddress").value(JsonHelper.ulongToHex(loadAddress)) }
codeIdentifier?.let { writer.name("codeIdentifier").value(it) }
isPC?.let { writer.name("isPC").value(it) }

View File

@ -8,7 +8,12 @@ enum class Telemetry {
/**
* Errors within the Bugsnag SDK.
*/
INTERNAL_ERRORS;
INTERNAL_ERRORS,
/**
* Differences from the default configuration.
*/
USAGE;
internal companion object {
fun fromString(str: String) = values().find { it.name == str } ?: INTERNAL_ERRORS

View File

@ -52,6 +52,7 @@ data class ImmutableConfig(
val maxReportedThreads: Int,
val persistenceDirectory: Lazy<File>,
val sendLaunchCrashesSynchronously: Boolean,
val attemptDeliveryOnCrash: Boolean,
// results cached here to avoid unnecessary lookups in Client.
val packageInfo: PackageInfo?,
@ -167,6 +168,7 @@ internal fun convertToImmutableConfig(
telemetry = config.telemetry.toSet(),
persistenceDirectory = persistenceDir,
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously,
attemptDeliveryOnCrash = config.isAttemptDeliveryOnCrash,
packageInfo = packageInfo,
appInfo = appInfo,
redactedKeys = config.redactedKeys.toSet()
@ -220,7 +222,12 @@ internal fun sanitiseConfiguration(
@Suppress("SENSELESS_COMPARISON")
if (configuration.delivery == null) {
configuration.delivery = DefaultDelivery(connectivity, configuration.logger!!)
configuration.delivery = DefaultDelivery(
connectivity,
configuration.apiKey,
configuration.maxStringValueLength,
configuration.logger!!
)
}
return convertToImmutableConfig(
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
import com.bugsnag.android.JsonStream
import com.bugsnag.android.repackaged.dslplatform.json.DslJson
import com.bugsnag.android.repackaged.dslplatform.json.JsonWriter
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
@ -9,6 +11,7 @@ import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.PrintWriter
import java.util.Date
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) {
dslJson.serialize(value, stream)
}
@ -76,4 +93,68 @@ internal object JsonHelper {
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) {
logger.w("Unexpected error delivering payload", exception)
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