mirror of
https://github.com/M66B/FairEmail.git
synced 2024-12-21 23:32:51 +00:00
Updated Bugsnag
This commit is contained in:
parent
1da91952f6
commit
084d0e1536
35 changed files with 1333 additions and 231 deletions
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
175
app/src/main/java/com/bugsnag/android/DefaultDelivery.kt.orig
Normal file
175
app/src/main/java/com/bugsnag/android/DefaultDelivery.kt.orig
Normal 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
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
101
app/src/main/java/com/bugsnag/android/NdkPluginCaller.kt
Normal file
101
app/src/main/java/com/bugsnag/android/NdkPluginCaller.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -45,6 +45,8 @@ internal class PluginClient(
|
|||
}
|
||||
}
|
||||
|
||||
fun getNdkPlugin(): Plugin? = ndkPlugin
|
||||
|
||||
fun loadPlugins(client: Client) {
|
||||
plugins.forEach { plugin ->
|
||||
try {
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package com.bugsnag.android.internal
|
||||
|
||||
/**
|
||||
* Stores internal metrics for Bugsnag use.
|
||||
*/
|
||||
interface InternalMetrics {
|
||||
/**
|
||||
* Returns a map that can be merged with the top-level JSON report.
|
||||
*/
|
||||
fun toJsonableMap(): Map<String, Any>
|
||||
|
||||
fun setConfigDifferences(differences: Map<String, Any>)
|
||||
|
||||
fun setCallbackCounts(newCallbackCounts: Map<String, Int>)
|
||||
|
||||
fun notifyAddCallback(callback: String)
|
||||
|
||||
fun notifyRemoveCallback(callback: String)
|
||||
|
||||
fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int)
|
||||
|
||||
fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int)
|
||||
}
|
||||
|
||||
internal data class TrimMetrics(
|
||||
val itemsTrimmed: Int, // breadcrumbs, strings, whatever
|
||||
val dataTrimmed: Int // chars, bytes, whatever
|
||||
)
|
|
@ -0,0 +1,111 @@
|
|||
package com.bugsnag.android.internal
|
||||
|
||||
import com.bugsnag.android.NdkPluginCaller
|
||||
|
||||
class InternalMetricsImpl(source: Map<String, Any>? = null) : InternalMetrics {
|
||||
private val configDifferences: MutableMap<String, Any>
|
||||
private val callbackCounts: MutableMap<String, Int>
|
||||
private var metadataStringsTrimmedCount = 0
|
||||
private var metadataCharsTruncatedCount = 0
|
||||
private var breadcrumbsRemovedCount = 0
|
||||
private var breadcrumbBytesRemovedCount = 0
|
||||
|
||||
init {
|
||||
if (source != null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
configDifferences = (source["config"] as MutableMap<String, Any>?) ?: hashMapOf()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
callbackCounts = (source["callbacks"] as MutableMap<String, Int>?) ?: hashMapOf()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val system = source["system"] as MutableMap<String, Any>?
|
||||
if (system != null) {
|
||||
metadataStringsTrimmedCount = (system["stringsTruncated"] as Number?)?.toInt() ?: 0
|
||||
metadataCharsTruncatedCount = (system["stringCharsTruncated"] as Number?)?.toInt() ?: 0
|
||||
breadcrumbsRemovedCount = (system["breadcrumbsRemovedCount"] as Number?)?.toInt() ?: 0
|
||||
breadcrumbBytesRemovedCount = (system["breadcrumbBytesRemoved"] as Number?)?.toInt() ?: 0
|
||||
}
|
||||
} else {
|
||||
configDifferences = hashMapOf()
|
||||
callbackCounts = hashMapOf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun toJsonableMap(): Map<String, Any> {
|
||||
val callbacks = allCallbacks()
|
||||
|
||||
val system = listOfNotNull(
|
||||
if (metadataStringsTrimmedCount > 0) "stringsTruncated" to metadataStringsTrimmedCount else null,
|
||||
if (metadataCharsTruncatedCount > 0) "stringCharsTruncated" to metadataCharsTruncatedCount else null,
|
||||
if (breadcrumbsRemovedCount > 0) "breadcrumbsRemoved" to breadcrumbsRemovedCount else null,
|
||||
if (breadcrumbBytesRemovedCount > 0) "breadcrumbBytesRemoved" to breadcrumbBytesRemovedCount else null,
|
||||
).toMap()
|
||||
|
||||
return listOfNotNull(
|
||||
if (configDifferences.isNotEmpty()) "config" to configDifferences else null,
|
||||
if (callbacks.isNotEmpty()) "callbacks" to callbacks else null,
|
||||
if (system.isNotEmpty()) "system" to system else null,
|
||||
).toMap()
|
||||
}
|
||||
|
||||
override fun setConfigDifferences(differences: Map<String, Any>) {
|
||||
configDifferences.clear()
|
||||
configDifferences.putAll(differences)
|
||||
// This is currently the only place where we set static data.
|
||||
// When that changes in future, we'll need a StaticData object to properly merge data
|
||||
// coming from multiple sources.
|
||||
NdkPluginCaller.setStaticData(mapOf("usage" to mapOf("config" to configDifferences)))
|
||||
}
|
||||
|
||||
override fun setCallbackCounts(newCallbackCounts: Map<String, Int>) {
|
||||
callbackCounts.clear()
|
||||
callbackCounts.putAll(newCallbackCounts)
|
||||
NdkPluginCaller.initCallbackCounts(newCallbackCounts)
|
||||
}
|
||||
|
||||
override fun notifyAddCallback(callback: String) {
|
||||
modifyCallback(callback, 1)
|
||||
NdkPluginCaller.notifyAddCallback(callback)
|
||||
}
|
||||
|
||||
override fun notifyRemoveCallback(callback: String) {
|
||||
modifyCallback(callback, -1)
|
||||
NdkPluginCaller.notifyRemoveCallback(callback)
|
||||
}
|
||||
|
||||
private fun modifyCallback(callback: String, delta: Int) {
|
||||
var currentValue = callbackCounts[callback] ?: 0
|
||||
currentValue += delta
|
||||
callbackCounts[callback] = currentValue.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
private fun allCallbacks(): Map<String, Any> {
|
||||
val result = hashMapOf<String, Any>()
|
||||
result.putAll(callbackCounts)
|
||||
|
||||
val counts = NdkPluginCaller.getCurrentCallbackSetCounts()
|
||||
if (counts != null) {
|
||||
// ndkOnError comes from the native side. The rest we already have.
|
||||
val ndkOnError = counts["ndkOnError"]
|
||||
if (ndkOnError != null) {
|
||||
result["ndkOnError"] = ndkOnError
|
||||
}
|
||||
}
|
||||
|
||||
val usage = NdkPluginCaller.getCurrentNativeApiCallUsage()
|
||||
if (usage != null) {
|
||||
result.putAll(usage)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int) {
|
||||
metadataStringsTrimmedCount = stringsTrimmed
|
||||
metadataCharsTruncatedCount = charsRemoved
|
||||
}
|
||||
|
||||
override fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int) {
|
||||
breadcrumbsRemovedCount = breadcrumbsRemoved
|
||||
breadcrumbBytesRemovedCount = bytesRemoved
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.bugsnag.android.internal
|
||||
|
||||
class InternalMetricsNoop : InternalMetrics {
|
||||
override fun toJsonableMap(): Map<String, Any> = emptyMap()
|
||||
override fun setConfigDifferences(differences: Map<String, Any>) = Unit
|
||||
override fun setCallbackCounts(newCallbackCounts: Map<String, Int>) = Unit
|
||||
override fun notifyAddCallback(callback: String) = Unit
|
||||
override fun notifyRemoveCallback(callback: String) = Unit
|
||||
override fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int) = Unit
|
||||
override fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int) = Unit
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
package com.bugsnag.android.internal
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
107
app/src/main/java/com/bugsnag/android/internal/StringUtils.kt
Normal file
107
app/src/main/java/com/bugsnag/android/internal/StringUtils.kt
Normal 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<*>
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue