Updated Bugsnag to 5.10.1

This commit is contained in:
M66B 2021-07-29 21:08:19 +02:00
parent 45db3c29b5
commit d1d70d321f
49 changed files with 825 additions and 570 deletions

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -1,5 +1,6 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.IOException
/**

View File

@ -1,11 +1,14 @@
package com.bugsnag.android
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.Application
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.SystemClock
import com.bugsnag.android.internal.ImmutableConfig
/**
* Collects various data on the application state
@ -22,13 +25,13 @@ internal class AppDataCollector(
var codeBundleId: String? = null
private val packageName: String = appContext.packageName
private var packageInfo = packageManager?.getPackageInfo(packageName, 0)
private var appInfo: ApplicationInfo? = packageManager?.getApplicationInfo(packageName, 0)
private val bgWorkRestricted = isBackgroundWorkRestricted()
private var binaryArch: String? = null
private val appName = getAppName()
private val processName = findProcessName()
private val releaseStage = config.releaseStage
private val versionName = config.appVersion ?: packageInfo?.versionName
private val versionName = config.appVersion ?: config.packageInfo?.versionName
fun generateApp(): App =
App(config, binaryArch, packageName, releaseStage, versionName, codeBundleId)
@ -47,18 +50,19 @@ internal class AppDataCollector(
fun getAppDataMetadata(): MutableMap<String, Any?> {
val map = HashMap<String, Any?>()
map["name"] = appName
map["activeScreen"] = getActiveScreenClass()
map["activeScreen"] = sessionTracker.contextActivity
map["memoryUsage"] = getMemoryUsage()
map["lowMemory"] = isLowMemory()
isBackgroundWorkRestricted()?.let {
map["backgroundWorkRestricted"] = it
bgWorkRestricted?.let {
map["backgroundWorkRestricted"] = bgWorkRestricted
}
processName?.let {
map["processName"] = it
}
return map
}
fun getActiveScreenClass(): String? = sessionTracker.contextActivity
/**
* Get the actual memory used by the VM (which may not be the total used
* by the app in the case of NDK usage).
@ -73,7 +77,7 @@ internal class AppDataCollector(
* https://developer.android.com/reference/android/app/ActivityManager#isBackgroundRestricted()
*/
private fun isBackgroundWorkRestricted(): Boolean? {
return if (activityManager == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return if (activityManager == null || VERSION.SDK_INT < VERSION_CODES.P) {
null
} else if (activityManager.isBackgroundRestricted) {
true // only return non-null value if true to avoid noise in error reports
@ -129,7 +133,7 @@ internal class AppDataCollector(
* AndroidManifest.xml
*/
private fun getAppName(): String? {
val copy = appInfo
val copy = config.appInfo
return when {
packageManager != null && copy != null -> {
packageManager.getApplicationLabel(copy).toString()
@ -138,6 +142,31 @@ internal class AppDataCollector(
}
}
/**
* Finds the name of the current process, or null if this cannot be found.
*/
@SuppressLint("PrivateApi")
private fun findProcessName(): String? {
return runCatching {
when {
VERSION.SDK_INT >= VERSION_CODES.P -> {
Application.getProcessName()
}
else -> {
// see https://stackoverflow.com/questions/19631894
val clz = Class.forName("android.app.ActivityThread")
val methodName = when {
VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2 -> "currentProcessName"
else -> "currentPackageName"
}
val getProcessName = clz.getDeclaredMethod(methodName)
getProcessName.invoke(null) as String
}
}
}.getOrNull()
}
companion object {
internal val startTimeMs = SystemClock.elapsedRealtime()

View File

@ -1,5 +1,7 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
/**
* Stateful information set by the notifier about your app can be found on this class. These values
* can be accessed and amended if necessary.

View File

@ -164,11 +164,20 @@ internal class BackgroundTaskService(
// shutdown the IO executor last.
errorExecutor.shutdown()
sessionExecutor.shutdown()
errorExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)
sessionExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)
errorExecutor.awaitTerminationSafe()
sessionExecutor.awaitTerminationSafe()
// shutdown the IO executor last, waiting for any existing tasks to complete
ioExecutor.shutdown()
ioExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)
ioExecutor.awaitTerminationSafe()
}
private fun ThreadPoolExecutor.awaitTerminationSafe() {
try {
awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)
} catch (ignored: InterruptedException) {
// ignore interrupted exception as the JVM is shutting down
}
}
}

View File

@ -1,10 +1,46 @@
package com.bugsnag.android
import java.util.Observable
import com.bugsnag.android.internal.StateObserver
import java.util.concurrent.CopyOnWriteArrayList
internal open class BaseObservable : Observable() {
fun notifyObservers(event: StateEvent) {
setChanged()
super.notifyObservers(event)
internal open class BaseObservable {
internal val observers = CopyOnWriteArrayList<StateObserver>()
/**
* Adds an observer that can react to [StateEvent] messages.
*/
fun addObserver(observer: StateObserver) {
observers.addIfAbsent(observer)
}
/**
* Removes a previously added observer that reacts to [StateEvent] messages.
*/
fun removeObserver(observer: StateObserver) {
observers.remove(observer)
}
/**
* This method should be invoked when the notifier's state has changed. If an observer
* has been set, it will be notified of the [StateEvent] message so that it can react
* appropriately. If no observer has been set then this method will no-op.
*/
internal inline fun updateState(provider: () -> StateEvent) {
// optimization to avoid unnecessary iterator and StateEvent construction
if (observers.isEmpty()) {
return
}
// construct the StateEvent object and notify observers
val event = provider()
observers.forEach { it.onStateChange(event) }
}
/**
* An eager version of [updateState], which is intended primarily for use in Java code.
* If the event will occur very frequently, you should consider calling the lazy method
* instead.
*/
fun updateState(event: StateEvent) = updateState { event }
}

View File

@ -10,7 +10,8 @@ import java.util.Map;
@SuppressWarnings("ConstantConditions")
public class Breadcrumb implements JsonStream.Streamable {
private final BreadcrumbInternal impl;
// non-private to allow direct field access optimizations
final BreadcrumbInternal impl;
private final Logger logger;
Breadcrumb(@NonNull String message, @NonNull Logger logger) {
@ -36,7 +37,7 @@ public class Breadcrumb implements JsonStream.Streamable {
*/
public void setMessage(@NonNull String message) {
if (message != null) {
impl.setMessage(message);
impl.message = message;
} else {
logNull("message");
}
@ -47,7 +48,7 @@ public class Breadcrumb implements JsonStream.Streamable {
*/
@NonNull
public String getMessage() {
return impl.getMessage();
return impl.message;
}
/**
@ -56,7 +57,7 @@ public class Breadcrumb implements JsonStream.Streamable {
*/
public void setType(@NonNull BreadcrumbType type) {
if (type != null) {
impl.setType(type);
impl.type = type;
} else {
logNull("type");
}
@ -68,14 +69,14 @@ public class Breadcrumb implements JsonStream.Streamable {
*/
@NonNull
public BreadcrumbType getType() {
return impl.getType();
return impl.type;
}
/**
* Sets diagnostic data relating to the breadcrumb
*/
public void setMetadata(@Nullable Map<String, Object> metadata) {
impl.setMetadata(metadata);
impl.metadata = metadata;
}
/**
@ -83,7 +84,7 @@ public class Breadcrumb implements JsonStream.Streamable {
*/
@Nullable
public Map<String, Object> getMetadata() {
return impl.getMetadata();
return impl.metadata;
}
/**
@ -91,12 +92,12 @@ public class Breadcrumb implements JsonStream.Streamable {
*/
@NonNull
public Date getTimestamp() {
return impl.getTimestamp();
return impl.timestamp;
}
@NonNull
String getStringTimestamp() {
return DateUtils.toIso8601(impl.getTimestamp());
return DateUtils.toIso8601(impl.timestamp);
}
@Override

View File

@ -9,11 +9,11 @@ import java.util.Date
* attached to a crash to help diagnose what events lead to the error.
*/
internal class BreadcrumbInternal internal constructor(
var message: String,
var type: BreadcrumbType,
var metadata: MutableMap<String, Any?>?,
val timestamp: Date = Date()
) : JsonStream.Streamable {
@JvmField var message: String,
@JvmField var type: BreadcrumbType,
@JvmField var metadata: MutableMap<String, Any?>?,
@JvmField val timestamp: Date = Date()
) : JsonStream.Streamable { // JvmField allows direct field access optimizations
internal constructor(message: String) : this(
message,

View File

@ -1,55 +1,96 @@
package com.bugsnag.android
import java.io.IOException
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicInteger
/**
* Stores breadcrumbs added to the [Client] in a ring buffer. If the number of breadcrumbs exceeds
* the maximum configured limit then the oldest breadcrumb in the ring buffer will be overwritten.
*
* When the breadcrumbs are required for generation of an event a [List] is constructed and
* breadcrumbs added in the order of their addition.
*/
internal class BreadcrumbState(
maxBreadcrumbs: Int,
val callbackState: CallbackState,
val logger: Logger
private val maxBreadcrumbs: Int,
private val callbackState: CallbackState,
private val logger: Logger
) : BaseObservable(), JsonStream.Streamable {
val store: Queue<Breadcrumb> = ConcurrentLinkedQueue()
/*
* We use the `index` as both a pointer to the tail of our ring-buffer, and also as "cheat"
* semaphore. When the ring-buffer is being copied - the index is set to a negative number,
* which is an invalid array-index. By masking the `expected` value in a `compareAndSet` with
* `validIndexMask`: the CAS operation will only succeed if it wouldn't interrupt a concurrent
* `copy()` call.
*/
private val validIndexMask: Int = Int.MAX_VALUE
private val maxBreadcrumbs: Int
private val store = arrayOfNulls<Breadcrumb?>(maxBreadcrumbs)
private val index = AtomicInteger(0)
init {
when {
maxBreadcrumbs > 0 -> this.maxBreadcrumbs = maxBreadcrumbs
else -> this.maxBreadcrumbs = 0
fun add(breadcrumb: Breadcrumb) {
if (maxBreadcrumbs == 0 || !callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) {
return
}
// store the breadcrumb in the ring buffer
val position = getBreadcrumbIndex()
store[position] = breadcrumb
updateState {
// use direct field access to avoid overhead of accessor method
StateEvent.AddBreadcrumb(
breadcrumb.impl.message,
breadcrumb.impl.type,
DateUtils.toIso8601(breadcrumb.impl.timestamp),
breadcrumb.impl.metadata ?: mutableMapOf()
)
}
}
/**
* Retrieves the index in the ring buffer where the breadcrumb should be stored.
*/
private fun getBreadcrumbIndex(): Int {
while (true) {
val currentValue = index.get() and validIndexMask
val nextValue = (currentValue + 1) % maxBreadcrumbs
if (index.compareAndSet(currentValue, nextValue)) {
return currentValue
}
}
}
/**
* Creates a copy of the breadcrumbs in the order of their addition.
*/
fun copy(): List<Breadcrumb> {
if (maxBreadcrumbs == 0) {
return emptyList()
}
// Set a negative value that stops any other thread from adding a breadcrumb.
// This handles reentrancy by waiting here until the old value has been reset.
var tail = -1
while (tail == -1) {
tail = index.getAndSet(-1)
}
try {
val result = arrayOfNulls<Breadcrumb>(maxBreadcrumbs)
store.copyInto(result, 0, tail, maxBreadcrumbs)
store.copyInto(result, maxBreadcrumbs - tail, 0, tail)
return result.filterNotNull()
} finally {
index.set(tail)
}
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
pruneBreadcrumbs()
val crumbs = copy()
writer.beginArray()
store.forEach { it.toStream(writer) }
crumbs.forEach { it.toStream(writer) }
writer.endArray()
}
fun add(breadcrumb: Breadcrumb) {
if (!callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) {
return
}
store.add(breadcrumb)
pruneBreadcrumbs()
notifyObservers(
StateEvent.AddBreadcrumb(
breadcrumb.message,
breadcrumb.type,
DateUtils.toIso8601(breadcrumb.timestamp),
breadcrumb.metadata ?: mutableMapOf()
)
)
}
private fun pruneBreadcrumbs() {
// Remove oldest breadcrumbState until new max size reached
while (store.size > maxBreadcrumbs) {
store.poll()
}
}
}

View File

@ -33,6 +33,10 @@ internal data class CallbackState(
}
fun runOnErrorTasks(event: Event, logger: Logger): Boolean {
// optimization to avoid construction of iterator when no callbacks set
if (onErrorTasks.isEmpty()) {
return true
}
onErrorTasks.forEach {
try {
if (!it.onError(event)) {
@ -46,6 +50,10 @@ internal data class CallbackState(
}
fun runOnBreadcrumbTasks(breadcrumb: Breadcrumb, logger: Logger): Boolean {
// optimization to avoid construction of iterator when no callbacks set
if (onBreadcrumbTasks.isEmpty()) {
return true
}
onBreadcrumbTasks.forEach {
try {
if (!it.onBreadcrumb(breadcrumb)) {
@ -59,6 +67,10 @@ internal data class CallbackState(
}
fun runOnSessionTasks(session: Session, logger: Logger): Boolean {
// optimization to avoid construction of iterator when no callbacks set
if (onSessionTasks.isEmpty()) {
return true
}
onSessionTasks.forEach {
try {
if (!it.onSession(session)) {

View File

@ -2,14 +2,15 @@ package com.bugsnag.android;
import static com.bugsnag.android.ContextExtensionsKt.getActivityManagerFrom;
import static com.bugsnag.android.ContextExtensionsKt.getStorageManagerFrom;
import static com.bugsnag.android.ImmutableConfigKt.sanitiseConfiguration;
import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION;
import static com.bugsnag.android.internal.ImmutableConfigKt.sanitiseConfiguration;
import com.bugsnag.android.internal.ImmutableConfig;
import com.bugsnag.android.internal.StateObserver;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.os.Environment;
import android.os.storage.StorageManager;
@ -22,13 +23,11 @@ import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.functions.Function2;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observer;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;
@ -72,11 +71,11 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
final SessionTracker sessionTracker;
private final SystemBroadcastReceiver systemBroadcastReceiver;
final SystemBroadcastReceiver systemBroadcastReceiver;
private final ActivityBreadcrumbCollector activityBreadcrumbCollector;
private final SessionLifecycleCallback sessionLifecycleCallback;
private final Connectivity connectivity;
final Connectivity connectivity;
@Nullable
private final StorageManager storageManager;
@ -152,9 +151,11 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
breadcrumbState = new BreadcrumbState(maxBreadcrumbs, callbackState, logger);
storageManager = getStorageManagerFrom(appContext);
contextState = new ContextState();
contextState.setContext(configuration.getContext());
if (configuration.getContext() != null) {
contextState.setManualContext(configuration.getContext());
}
sessionStore = new SessionStore(immutableConfig, logger, null);
sessionTracker = new SessionTracker(immutableConfig, callbackState, this,
@ -186,7 +187,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
sessionLifecycleCallback = new SessionLifecycleCallback(sessionTracker);
application.registerActivityLifecycleCallbacks(sessionLifecycleCallback);
if (immutableConfig.shouldRecordBreadcrumbType(BreadcrumbType.STATE)) {
if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) {
this.activityBreadcrumbCollector = new ActivityBreadcrumbCollector(
new Function2<String, Map<String, ? extends Object>, Unit>() {
@SuppressWarnings("unchecked")
@ -221,12 +222,6 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
exceptionHandler.install();
}
// register a receiver for automatic breadcrumbs
systemBroadcastReceiver = SystemBroadcastReceiver.register(this, logger, bgTaskService);
registerOrientationChangeListener();
registerMemoryTrimListener();
// load last run info
lastRunInfoStore = new LastRunInfoStore(immutableConfig);
lastRunInfo = loadLastRunInfo();
@ -234,13 +229,16 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
// initialise plugins before attempting to flush any errors
loadPlugins(configuration);
connectivity.registerForNetworkChanges();
// Flush any on-disk errors and sessions
eventStore.flushOnLaunch();
eventStore.flushAsync();
sessionTracker.flushAsync();
// register listeners for system events in the background.
systemBroadcastReceiver = new SystemBroadcastReceiver(this, logger);
registerComponentCallbacks();
registerListenersInBackground();
// leave auto breadcrumb
Map<String, Object> data = Collections.emptyMap();
leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data);
@ -299,6 +297,25 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
this.exceptionHandler = exceptionHandler;
}
/**
* Registers listeners for system events in the background. This offloads work from the main
* thread that collects useful information from callbacks, but that don't need to be done
* immediately on client construction.
*/
void registerListenersInBackground() {
try {
bgTaskService.submitTask(TaskType.DEFAULT, new Runnable() {
@Override
public void run() {
connectivity.registerForNetworkChanges();
SystemBroadcastReceiver.register(appContext, systemBroadcastReceiver, logger);
}
});
} catch (RejectedExecutionException ex) {
logger.w("Failed to register for system events", ex);
}
}
private LastRunInfo loadLastRunInfo() {
LastRunInfo lastRunInfo = lastRunInfoStore.load();
LastRunInfo currentRunInfo = new LastRunInfo(0, false, false);
@ -340,10 +357,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
return configuration.impl.metadataState.copy(copy);
}
private void registerOrientationChangeListener() {
IntentFilter configFilter = new IntentFilter();
configFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
ConfigChangeReceiver receiver = new ConfigChangeReceiver(deviceDataCollector,
private void registerComponentCallbacks() {
appContext.registerComponentCallbacks(new ClientComponentCallbacks(
deviceDataCollector,
new Function2<String, String, Unit>() {
@Override
public Unit invoke(String oldOrientation, String newOrientation) {
@ -354,14 +370,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
clientObservable.postOrientationChange(newOrientation);
return null;
}
}
);
ContextExtensionsKt.registerReceiverSafe(appContext, receiver, configFilter, logger);
}
private void registerMemoryTrimListener() {
appContext.registerComponentCallbacks(new ClientComponentCallbacks(
new Function1<Boolean, Unit>() {
}, new Function1<Boolean, Unit>() {
@Override
public Unit invoke(Boolean isLowMemory) {
clientObservable.postMemoryTrimEvent(isLowMemory);
@ -379,7 +388,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
clientObservable.postNdkDeliverPending();
}
void registerObserver(Observer observer) {
void addObserver(StateObserver observer) {
metadataState.addObserver(observer);
breadcrumbState.addObserver(observer);
sessionTracker.addObserver(observer);
@ -390,15 +399,15 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
launchCrashTracker.addObserver(observer);
}
void unregisterObserver(Observer observer) {
metadataState.deleteObserver(observer);
breadcrumbState.deleteObserver(observer);
sessionTracker.deleteObserver(observer);
clientObservable.deleteObserver(observer);
userState.deleteObserver(observer);
contextState.deleteObserver(observer);
deliveryDelegate.deleteObserver(observer);
launchCrashTracker.deleteObserver(observer);
void removeObserver(StateObserver observer) {
metadataState.removeObserver(observer);
breadcrumbState.removeObserver(observer);
sessionTracker.removeObserver(observer);
clientObservable.removeObserver(observer);
userState.removeObserver(observer);
contextState.removeObserver(observer);
deliveryDelegate.removeObserver(observer);
launchCrashTracker.removeObserver(observer);
}
/**
@ -494,7 +503,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
* If you would like to set this value manually, you should alter this property.
*/
public void setContext(@Nullable String context) {
contextState.setContext(context);
contextState.setManualContext(context);
}
/**
@ -656,6 +665,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
*/
public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) {
if (exc != null) {
if (immutableConfig.shouldDiscardError(exc)) {
return;
}
SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION);
Metadata metadata = metadataState.getMetadata();
Event event = new Event(exc, immutableConfig, severityReason, metadata, logger);
@ -706,35 +718,19 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
event.addMetadata("app", appDataCollector.getAppDataMetadata());
// Attach breadcrumbState to the event
event.setBreadcrumbs(new ArrayList<>(breadcrumbState.getStore()));
event.setBreadcrumbs(breadcrumbState.copy());
// Attach user info to the event
User user = userState.getUser();
event.setUser(user.getId(), user.getEmail(), user.getName());
// Attach default context from active activity
if (Intrinsics.isEmpty(event.getContext())) {
String context = contextState.getContext();
event.setContext(context != null ? context : appDataCollector.getActiveScreenClass());
}
// Attach context to the event
event.setContext(contextState.getContext());
notifyInternal(event, onError);
}
void notifyInternal(@NonNull Event event,
@Nullable OnErrorCallback onError) {
String type = event.getImpl().getSeverityReasonType();
logger.d("Client#notifyInternal() - event captured by Client, type=" + type);
// Don't notify if this event class should be ignored
if (event.shouldDiscardClass()) {
logger.d("Skipping notification - should not notify for this class");
return;
}
if (!immutableConfig.shouldNotifyForReleaseStage()) {
logger.d("Skipping notification - should not notify for this release stage");
return;
}
// set the redacted keys on the event as this
// will not have been set for RN/Unity events
Set<String> redactedKeys = metadataState.getMetadata().getRedactedKeys();
@ -773,7 +769,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
*/
@NonNull
public List<Breadcrumb> getBreadcrumbs() {
return new ArrayList<>(breadcrumbState.getStore());
return breadcrumbState.copy();
}
@NonNull
@ -864,9 +860,12 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
}
}
// cast map to retain original signature until next major version bump, as this
// method signature is used by Unity/React native
@NonNull
@SuppressWarnings({"unchecked", "rawtypes"})
Map<String, Object> getMetadata() {
return metadataState.getMetadata().toMap();
return (Map) metadataState.getMetadata().toMap();
}
/**
@ -911,7 +910,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
void leaveAutoBreadcrumb(@NonNull String message,
@NonNull BreadcrumbType type,
@NonNull Map<String, Object> metadata) {
if (immutableConfig.shouldRecordBreadcrumbType(type)) {
if (!immutableConfig.shouldDiscardBreadcrumb(type)) {
breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger));
}
}
@ -1033,6 +1032,10 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
return metadataState;
}
ContextState getContextState() {
return contextState;
}
void setAutoNotify(boolean autoNotify) {
pluginClient.setAutoNotify(this, autoNotify);

View File

@ -4,9 +4,19 @@ import android.content.ComponentCallbacks
import android.content.res.Configuration
internal class ClientComponentCallbacks(
private val deviceDataCollector: DeviceDataCollector,
private val cb: (oldOrientation: String?, newOrientation: String?) -> Unit,
val callback: (Boolean) -> Unit
) : ComponentCallbacks {
override fun onConfigurationChanged(newConfig: Configuration) {}
override fun onConfigurationChanged(newConfig: Configuration) {
val oldOrientation = deviceDataCollector.getOrientationAsString()
if (deviceDataCollector.updateOrientation(newConfig.orientation)) {
val newOrientation = deviceDataCollector.getOrientationAsString()
cb(oldOrientation, newOrientation)
}
}
override fun onLowMemory() {
callback(true)

View File

@ -1,17 +1,23 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
internal class ClientObservable : BaseObservable() {
fun postOrientationChange(orientation: String?) {
notifyObservers(StateEvent.UpdateOrientation(orientation))
updateState { StateEvent.UpdateOrientation(orientation) }
}
fun postMemoryTrimEvent(isLowMemory: Boolean) {
notifyObservers(StateEvent.UpdateMemoryTrimEvent(isLowMemory))
updateState { StateEvent.UpdateMemoryTrimEvent(isLowMemory) }
}
fun postNdkInstall(conf: ImmutableConfig, lastRunInfoPath: String, consecutiveLaunchCrashes: Int) {
notifyObservers(
fun postNdkInstall(
conf: ImmutableConfig,
lastRunInfoPath: String,
consecutiveLaunchCrashes: Int
) {
updateState {
StateEvent.Install(
conf.apiKey,
conf.enabledErrorTypes.ndkCrashes,
@ -21,10 +27,10 @@ internal class ClientObservable : BaseObservable() {
lastRunInfoPath,
consecutiveLaunchCrashes
)
)
}
}
fun postNdkDeliverPending() {
notifyObservers(StateEvent.DeliverPending)
updateState { StateEvent.DeliverPending }
}
}

View File

@ -1,22 +0,0 @@
package com.bugsnag.android
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
internal class ConfigChangeReceiver(
private val deviceDataCollector: DeviceDataCollector,
private val cb: (oldOrientation: String?, newOrientation: String?) -> Unit
) : BroadcastReceiver() {
var orientation = deviceDataCollector.calculateOrientation()
override fun onReceive(context: Context?, intent: Intent?) {
val newOrientation = deviceDataCollector.calculateOrientation()
if (!newOrientation.equals(orientation)) {
cb(orientation, newOrientation)
orientation = newOrientation
}
}
}

View File

@ -37,19 +37,19 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware
var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS
var context: String? = null
var redactedKeys: Set<String> = metadataState.metadata.redactedKeys
var redactedKeys: Set<String>
get() = metadataState.metadata.redactedKeys
set(value) {
metadataState.metadata.redactedKeys = value
field = value
}
var discardClasses: Set<String> = emptySet()
var enabledReleaseStages: Set<String>? = null
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = BreadcrumbType.values().toSet()
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null
var projectPackages: Set<String> = emptySet()
var persistenceDirectory: File? = null
protected val plugins = mutableSetOf<Plugin>()
protected val plugins = HashSet<Plugin>()
override fun addOnError(onError: OnErrorCallback) = callbackState.addOnError(onError)
override fun removeOnError(onError: OnErrorCallback) = callbackState.removeOnError(onError)

View File

@ -4,6 +4,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.io.File;
import java.util.Locale;
@ -19,7 +20,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware {
private static final int MIN_BREADCRUMBS = 0;
private static final int MAX_BREADCRUMBS = 100;
private static final String API_KEY_REGEX = "[A-Fa-f0-9]{32}";
private static final int VALID_API_KEY_LEN = 32;
private static final long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0;
final ConfigInternal impl;
@ -47,14 +48,29 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware {
}
private void validateApiKey(String value) {
if (Intrinsics.isEmpty(value)) {
if (isInvalidApiKey(value)) {
DebugLogger.INSTANCE.w("Invalid configuration. "
+ "apiKey should be a 32-character hexademical string, got " + value);
}
}
@VisibleForTesting
static boolean isInvalidApiKey(String apiKey) {
if (Intrinsics.isEmpty(apiKey)) {
throw new IllegalArgumentException("No Bugsnag API Key set");
}
if (!value.matches(API_KEY_REGEX)) {
DebugLogger.INSTANCE.w(String.format("Invalid configuration. apiKey should be a "
+ "32-character hexademical string, got \"%s\"", value));
if (apiKey.length() != VALID_API_KEY_LEN) {
return true;
}
// check whether each character is hexadecimal (either a digit or a-f).
// this avoids using a regex to improve startup performance.
for (int k = 0; k < VALID_API_KEY_LEN; k++) {
char chr = apiKey.charAt(k);
if (!Character.isDigit(chr) && (chr < 'a' || chr > 'f')) {
return true;
}
}
return false;
}
private void logNull(String property) {
@ -294,9 +310,9 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware {
if (launchDurationMillis >= MIN_LAUNCH_CRASH_THRESHOLD_MS) {
impl.setLaunchDurationMillis(launchDurationMillis);
} else {
getLogger().e(String.format(Locale.US, "Invalid configuration value detected. "
getLogger().e("Invalid configuration value detected. "
+ "Option launchDurationMillis should be a positive long value."
+ "Supplied value is %d", launchDurationMillis));
+ "Supplied value is " + launchDurationMillis);
}
}
@ -513,9 +529,9 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware {
if (maxBreadcrumbs >= MIN_BREADCRUMBS && maxBreadcrumbs <= MAX_BREADCRUMBS) {
impl.setMaxBreadcrumbs(maxBreadcrumbs);
} else {
getLogger().e(String.format(Locale.US, "Invalid configuration value detected. "
getLogger().e("Invalid configuration value detected. "
+ "Option maxBreadcrumbs should be an integer between 0-100. "
+ "Supplied value is %d", maxBreadcrumbs));
+ "Supplied value is " + maxBreadcrumbs);
}
}
@ -539,9 +555,9 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware {
if (maxPersistedEvents >= 0) {
impl.setMaxPersistedEvents(maxPersistedEvents);
} else {
getLogger().e(String.format(Locale.US, "Invalid configuration value detected. "
getLogger().e("Invalid configuration value detected. "
+ "Option maxPersistedEvents should be a positive integer."
+ "Supplied value is %d", maxPersistedEvents));
+ "Supplied value is " + maxPersistedEvents);
}
}
@ -565,9 +581,9 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware {
if (maxPersistedSessions >= 0) {
impl.setMaxPersistedSessions(maxPersistedSessions);
} else {
getLogger().e(String.format(Locale.US, "Invalid configuration value detected. "
getLogger().e("Invalid configuration value detected. "
+ "Option maxPersistedSessions should be a positive integer."
+ "Supplied value is %d", maxPersistedSessions));
+ "Supplied value is " + maxPersistedSessions);
}
}

View File

@ -61,6 +61,14 @@ internal class ConnectivityLegacy(
private val changeReceiver = ConnectivityChangeReceiver(callback)
private val activeNetworkInfo: android.net.NetworkInfo?
get() = try {
cm.activeNetworkInfo
} catch (e: NullPointerException) {
// in some rare cases we get a remote NullPointerException via Parcel.readException
null
}
override fun registerForNetworkChanges() {
val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
context.registerReceiverSafe(changeReceiver, intentFilter)
@ -69,11 +77,11 @@ internal class ConnectivityLegacy(
override fun unregisterForNetworkChanges() = context.unregisterReceiverSafe(changeReceiver)
override fun hasNetworkConnection(): Boolean {
return cm.activeNetworkInfo?.isConnectedOrConnecting ?: false
return activeNetworkInfo?.isConnectedOrConnecting ?: false
}
override fun retrieveNetworkAccessState(): String {
return when (cm.activeNetworkInfo?.type) {
return when (activeNetworkInfo?.type) {
null -> "none"
ConnectivityManager.TYPE_WIFI -> "wifi"
ConnectivityManager.TYPE_ETHERNET -> "ethernet"

View File

@ -1,13 +1,36 @@
package com.bugsnag.android
internal class ContextState(context: String? = null) : BaseObservable() {
var context = context
set(value) {
field = value
/**
* Tracks the current context and allows observers to be notified whenever it changes.
*
* The default behaviour is to track [SessionTracker.getContextActivity]. However, any value
* that the user sets via [Bugsnag.setContext] will override this and be returned instead.
*/
internal class ContextState : BaseObservable() {
companion object {
private const val MANUAL = "__BUGSNAG_MANUAL_CONTEXT__"
}
private var manualContext: String? = null
private var automaticContext: String? = null
fun setManualContext(context: String?) {
manualContext = context
automaticContext = MANUAL
emitObservableEvent()
}
fun setAutomaticContext(context: String?) {
if (automaticContext !== MANUAL) {
automaticContext = context
emitObservableEvent()
}
}
fun emitObservableEvent() = notifyObservers(StateEvent.UpdateContext(context))
fun getContext(): String? {
return automaticContext.takeIf { it !== MANUAL } ?: manualContext
}
fun copy() = ContextState(context)
fun emitObservableEvent() = updateState { StateEvent.UpdateContext(getContext()) }
}

View File

@ -2,6 +2,8 @@ package com.bugsnag.android;
import static com.bugsnag.android.SeverityReason.REASON_PROMISE_REJECTION;
import com.bugsnag.android.internal.ImmutableConfig;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@ -44,10 +46,10 @@ class DeliveryDelegate extends BaseObservable {
if (session != null) {
if (event.isUnhandled()) {
event.setSession(session.incrementUnhandledAndCopy());
notifyObservers(StateEvent.NotifyUnhandled.INSTANCE);
updateState(StateEvent.NotifyUnhandled.INSTANCE);
} else {
event.setSession(session.incrementHandledAndCopy());
notifyObservers(StateEvent.NotifyHandled.INSTANCE);
updateState(StateEvent.NotifyHandled.INSTANCE);
}
}

View File

@ -16,13 +16,14 @@ import java.util.Locale
import java.util.concurrent.Callable
import java.util.concurrent.Future
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.max
import kotlin.math.min
internal class DeviceDataCollector(
private val connectivity: Connectivity,
private val appContext: Context,
private val resources: Resources?,
resources: Resources,
private val deviceId: String?,
private val buildInfo: DeviceBuildInfo,
private val dataDirectory: File,
@ -31,7 +32,7 @@ internal class DeviceDataCollector(
private val logger: Logger
) {
private val displayMetrics = resources?.displayMetrics
private val displayMetrics = resources.displayMetrics
private val emulator = isEmulator()
private val screenDensity = getScreenDensity()
private val dpi = getScreenDensityDpi()
@ -40,6 +41,7 @@ internal class DeviceDataCollector(
private val cpuAbi = getCpuAbi()
private val runtimeVersions: MutableMap<String, Any>
private val rootedFuture: Future<Boolean>?
private var orientation = AtomicInteger(resources.configuration.orientation)
init {
val map = mutableMapOf<String, Any>()
@ -79,7 +81,7 @@ internal class DeviceDataCollector(
runtimeVersions.toMutableMap(),
calculateFreeDisk(),
calculateFreeMemory(),
calculateOrientation(),
getOrientationAsString(),
Date(now)
)
@ -187,7 +189,7 @@ internal class DeviceDataCollector(
return if (displayMetrics != null) {
val max = max(displayMetrics.widthPixels, displayMetrics.heightPixels)
val min = min(displayMetrics.widthPixels, displayMetrics.heightPixels)
String.format(Locale.US, "%dx%d", max, min)
"${max}x$min"
} else {
null
}
@ -235,14 +237,23 @@ internal class DeviceDataCollector(
}
/**
* Get the device orientation, eg. "landscape"
* Get the current device orientation, eg. "landscape"
*/
internal fun calculateOrientation() = when (resources?.configuration?.orientation) {
internal fun getOrientationAsString(): String? = when (orientation.get()) {
ORIENTATION_LANDSCAPE -> "landscape"
ORIENTATION_PORTRAIT -> "portrait"
else -> null
}
/**
* Called whenever the orientation is updated so that the device information is accurate.
* Currently this is only invoked by [ClientComponentCallbacks]. Returns true if the
* orientation has changed, otherwise false.
*/
internal fun updateOrientation(newOrientation: Int): Boolean {
return orientation.getAndSet(newOrientation) != newOrientation
}
fun addRuntimeVersionInfo(key: String, value: String) {
runtimeVersions[key] = value
}

View File

@ -15,8 +15,7 @@ internal class ErrorInternal @JvmOverloads internal constructor(
.mapTo(mutableListOf()) { currentEx ->
// Somehow it's possible for stackTrace to be null in rare cases
val stacktrace = currentEx.stackTrace ?: arrayOf<StackTraceElement>()
val trace =
Stacktrace.stacktraceFromJavaTrace(stacktrace, projectPackages, logger)
val trace = Stacktrace(stacktrace, projectPackages, logger)
val errorInternal =
ErrorInternal(currentEx.javaClass.name, currentEx.localizedMessage, trace)

View File

@ -1,5 +1,7 @@
package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

View File

@ -1,7 +1,7 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.File
import java.util.Locale
import java.util.UUID
/**
@ -27,15 +27,7 @@ internal data class EventFilenameInfo(
* "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json"
*/
fun encode(): String {
return String.format(
Locale.US,
"%d_%s_%s_%s_%s.json",
timestamp,
apiKey,
serializeErrorTypeHeader(errorTypes),
uuid,
suffix
)
return "${timestamp}_${apiKey}_${serializeErrorTypeHeader(errorTypes)}_${uuid}_$suffix.json"
}
fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH

View File

@ -1,5 +1,6 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.IOException
internal class EventInternal @JvmOverloads internal constructor(

View File

@ -1,5 +1,6 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.File
import java.io.IOException

View File

@ -1,5 +1,7 @@
package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -148,8 +150,8 @@ class EventStore extends FileStore {
void flushReports(Collection<File> storedReports) {
if (!storedReports.isEmpty()) {
logger.i(String.format(Locale.US,
"Sending %d saved error(s) to Bugsnag", storedReports.size()));
int size = storedReports.size();
logger.i("Sending " + size + " saved error(s) to Bugsnag");
for (File eventFile : storedReports) {
flushEventFile(eventFile);
@ -200,14 +202,12 @@ class EventStore extends FileStore {
String getFilename(Object object) {
EventFilenameInfo eventInfo
= EventFilenameInfo.Companion.fromEvent(object, null, config);
String encodedInfo = eventInfo.encode();
return String.format(Locale.US, "%s", encodedInfo);
return eventInfo.encode();
}
String getNdkFilename(Object object, String apiKey) {
EventFilenameInfo eventInfo
= EventFilenameInfo.Companion.fromEvent(object, apiKey, config);
String encodedInfo = eventInfo.encode();
return String.format(Locale.US, "%s", encodedInfo);
return eventInfo.encode();
}
}

View File

@ -35,6 +35,9 @@ class ExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
if (client.getConfig().shouldDiscardError(throwable)) {
return;
}
boolean strictModeThrowable = strictModeHandler.isStrictModeThrowable(throwable);
// Notify any subscribed clients of the uncaught exception

View File

@ -104,8 +104,7 @@ abstract class FileStore {
out.close();
}
} catch (Exception exception) {
logger.w(String.format("Failed to close unsent payload writer (%s) ",
filename), exception);
logger.w("Failed to close unsent payload writer: " + filename, exception);
}
lock.unlock();
}
@ -130,7 +129,7 @@ abstract class FileStore {
Writer out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"));
stream = new JsonStream(out);
stream.value(streamable);
logger.i(String.format("Saved unsent payload to disk (%s) ", filename));
logger.i("Saved unsent payload to disk: '" + filename + '\'');
return filename;
} catch (FileNotFoundException exc) {
logger.w("Ignoring FileNotFoundException - unable to create file", exc);
@ -168,8 +167,8 @@ abstract class FileStore {
File oldestFile = files.get(k);
if (!queuedFiles.contains(oldestFile)) {
logger.w(String.format("Discarding oldest error as stored "
+ "error limit reached (%s)", oldestFile.getPath()));
logger.w("Discarding oldest error as stored "
+ "error limit reached: '" + oldestFile.getPath() + '\'');
deleteStoredFiles(Collections.singleton(oldestFile));
files.remove(k);
k--;

View File

@ -3,6 +3,8 @@ package com.bugsnag.android;
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 android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;

View File

@ -1,5 +1,6 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.File
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock

View File

@ -1,5 +1,6 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.TimeUnit
@ -34,7 +35,7 @@ internal class LaunchCrashTracker @JvmOverloads constructor(
fun markLaunchCompleted() {
executor.shutdown()
launching.set(false)
notifyObservers(StateEvent.UpdateIsLaunching(false))
updateState { StateEvent.UpdateIsLaunching(false) }
logger.d("App launch period marked as complete")
}

View File

@ -12,7 +12,7 @@ import java.util.concurrent.ConcurrentHashMap
* Diagnostic information is presented on your Bugsnag dashboard in tabs.
*/
internal data class Metadata @JvmOverloads constructor(
internal val store: ConcurrentHashMap<String, Any> = ConcurrentHashMap()
internal val store: MutableMap<String, MutableMap<String, Any>> = ConcurrentHashMap()
) : JsonStream.Streamable, MetadataAware {
val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer()
@ -38,12 +38,9 @@ internal data class Metadata @JvmOverloads constructor(
if (value == null) {
clearMetadata(section, key)
} else {
var tab = store[section]
if (tab !is MutableMap<*, *>) {
tab = ConcurrentHashMap<Any, Any>()
store[section] = tab
}
insertValue(tab as MutableMap<String, Any>, key, value)
val tab = store[section] ?: ConcurrentHashMap()
store[section] = tab
insertValue(tab, key, value)
}
}
@ -52,7 +49,7 @@ internal data class Metadata @JvmOverloads constructor(
// only merge if both the existing and new value are maps
val existingValue = map[key]
if (obj is MutableMap<*, *> && existingValue is MutableMap<*, *>) {
if (existingValue != null && obj is Map<*, *>) {
val maps = listOf(existingValue as Map<String, Any>, newValue as Map<String, Any>)
obj = mergeMaps(maps)
}
@ -65,49 +62,41 @@ internal data class Metadata @JvmOverloads constructor(
override fun clearMetadata(section: String, key: String) {
val tab = store[section]
tab?.remove(key)
if (tab is MutableMap<*, *>) {
tab.remove(key)
if (tab.isEmpty()) {
store.remove(section)
}
if (tab.isNullOrEmpty()) {
store.remove(section)
}
}
override fun getMetadata(section: String): Map<String, Any>? {
return store[section] as (Map<String, Any>?)
return store[section]
}
override fun getMetadata(section: String, key: String): Any? {
return when (val tab = store[section]) {
is Map<*, *> -> (tab as Map<String, Any>?)!![key]
else -> tab
}
return getMetadata(section)?.get(key)
}
fun toMap(): ConcurrentHashMap<String, Any> {
val hashMap = ConcurrentHashMap(store)
fun toMap(): MutableMap<String, MutableMap<String, Any>> {
val copy = ConcurrentHashMap(store)
// deep copy each section
store.entries.forEach {
if (it.value is ConcurrentHashMap<*, *>) {
hashMap[it.key] = ConcurrentHashMap(it.value as ConcurrentHashMap<*, *>)
}
copy[it.key] = ConcurrentHashMap(it.value)
}
return hashMap
return copy
}
companion object {
fun merge(vararg data: Metadata): Metadata {
val stores = data.map { it.toMap() }
val redactKeys = data.flatMap { it.jsonStreamer.redactedKeys }
val newMeta = Metadata(mergeMaps(stores))
val newMeta = Metadata(mergeMaps(stores) as MutableMap<String, MutableMap<String, Any>>)
newMeta.redactedKeys = redactKeys.toSet()
return newMeta
}
internal fun mergeMaps(data: List<Map<String, Any>>): ConcurrentHashMap<String, Any> {
internal fun mergeMaps(data: List<Map<String, Any>>): MutableMap<String, Any> {
val keys = data.flatMap { it.keys }.toSet()
val result = ConcurrentHashMap<String, Any>()
@ -120,7 +109,7 @@ internal data class Metadata @JvmOverloads constructor(
}
private fun getMergeValue(
result: ConcurrentHashMap<String, Any>,
result: MutableMap<String, Any>,
key: String,
map: Map<String, Any>
) {

View File

@ -28,8 +28,8 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) :
private fun notifyClear(section: String, key: String?) {
when (key) {
null -> notifyObservers(StateEvent.ClearMetadataSection(section))
else -> notifyObservers(StateEvent.ClearMetadataValue(section, key))
null -> updateState { StateEvent.ClearMetadataSection(section) }
else -> updateState { StateEvent.ClearMetadataValue(section, key) }
}
}
@ -55,13 +55,13 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) :
private fun notifyMetadataAdded(section: String, key: String, value: Any?) {
when (value) {
null -> notifyClear(section, key)
else -> notifyObservers(AddMetadata(section, key, metadata.getMetadata(section, key)))
else -> updateState { AddMetadata(section, key, metadata.getMetadata(section, key)) }
}
}
private fun notifyMetadataAdded(section: String, value: Map<String, Any?>) {
value.entries.forEach {
notifyObservers(AddMetadata(section, it.key, metadata.getMetadata(it.key)))
updateState { AddMetadata(section, it.key, metadata.getMetadata(it.key)) }
}
}
}

View File

@ -1,5 +1,7 @@
package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -325,7 +327,7 @@ public class NativeInterface {
ImmutableConfig config = client.getConfig();
if (releaseStage == null
|| releaseStage.length() == 0
|| config.shouldNotifyForReleaseStage()) {
|| !config.shouldDiscardByReleaseStage()) {
EventStore eventStore = client.getEventStore();
String filename = eventStore.getNdkFilename(payload, apiKey);
@ -368,6 +370,9 @@ public class NativeInterface {
@NonNull final String message,
@NonNull final Severity severity,
@NonNull final StackTraceElement[] stacktrace) {
if (getClient().getConfig().shouldDiscardError(name)) {
return;
}
Throwable exc = new RuntimeException();
exc.setStackTrace(stacktrace);

View File

@ -7,7 +7,7 @@ import java.io.IOException
*/
class Notifier @JvmOverloads constructor(
var name: String = "Android Bugsnag Notifier",
var version: String = "5.9.4",
var version: String = "5.10.1",
var url: String = "https://bugsnag.com"
) : JsonStream.Streamable {

View File

@ -1,5 +1,7 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
internal class PluginClient(
userPlugins: Set<Plugin>,
private val immutableConfig: ImmutableConfig,

View File

@ -1,5 +1,7 @@
package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -45,10 +47,7 @@ class SessionStore extends FileStore {
@NonNull
@Override
String getFilename(Object object) {
return String.format(Locale.US,
"%s%d_v2.json",
UUID.randomUUID().toString(),
System.currentTimeMillis());
return UUID.randomUUID().toString() + System.currentTimeMillis() + "_v2.json";
}
}

View File

@ -1,5 +1,7 @@
package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@ -79,6 +81,9 @@ class SessionTracker extends BaseObservable {
@VisibleForTesting
Session startNewSession(@NonNull Date date, @Nullable User user,
boolean autoCaptured) {
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
return null;
}
String id = UUID.randomUUID().toString();
Session session = new Session(id, date, user, autoCaptured, client.getNotifier(), logger);
currentSession.set(session);
@ -87,6 +92,9 @@ class SessionTracker extends BaseObservable {
}
Session startSession(boolean autoCaptured) {
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
return null;
}
return startNewSession(new Date(), client.getUser(), autoCaptured);
}
@ -95,7 +103,7 @@ class SessionTracker extends BaseObservable {
if (session != null) {
session.isPaused.set(true);
notifyObservers(StateEvent.PauseSession.INSTANCE);
updateState(StateEvent.PauseSession.INSTANCE);
}
}
@ -116,10 +124,10 @@ class SessionTracker extends BaseObservable {
return resumed;
}
private void notifySessionStartObserver(Session session) {
String startedAt = DateUtils.toIso8601(session.getStartedAt());
notifyObservers(new StateEvent.StartSession(session.getId(), startedAt,
session.getHandledCount(), session.getUnhandledCount()));
private void notifySessionStartObserver(final Session session) {
final String startedAt = DateUtils.toIso8601(session.getStartedAt());
updateState(new StateEvent.StartSession(session.getId(), startedAt,
session.getHandledCount(), session.getUnhandledCount()));
}
/**
@ -137,13 +145,16 @@ class SessionTracker extends BaseObservable {
Session registerExistingSession(@Nullable Date date, @Nullable String sessionId,
@Nullable User user, int unhandledCount,
int handledCount) {
if (client.getConfig().shouldDiscardSession(false)) {
return null;
}
Session session = null;
if (date != null && sessionId != null) {
session = new Session(sessionId, date, user, unhandledCount, handledCount,
client.getNotifier(), logger);
notifySessionStartObserver(session);
} else {
notifyObservers(StateEvent.PauseSession.INSTANCE);
updateState(StateEvent.PauseSession.INSTANCE);
}
currentSession.set(session);
return session;
@ -157,18 +168,12 @@ class SessionTracker extends BaseObservable {
*/
private void trackSessionIfNeeded(final Session session) {
logger.d("SessionTracker#trackSessionIfNeeded() - session captured by Client");
boolean notifyForRelease = configuration.shouldNotifyForReleaseStage();
session.setApp(client.getAppDataCollector().generateApp());
session.setDevice(client.getDeviceDataCollector().generateDevice());
boolean deliverSession = callbackState.runOnSessionTasks(session, logger);
if (deliverSession && notifyForRelease
&& (configuration.getAutoTrackSessions() || !session.isAutoCaptured())
&& session.isTracked().compareAndSet(false, true)) {
if (deliverSession && session.isTracked().compareAndSet(false, true)) {
notifySessionStartObserver(session);
flushAsync();
flushInMemorySession(session);
}
@ -355,13 +360,14 @@ class SessionTracker extends BaseObservable {
lastExitedForegroundMs.set(nowMs);
}
}
client.getContextState().setAutomaticContext(getContextActivity());
notifyNdkInForeground();
}
private void notifyNdkInForeground() {
Boolean inForeground = isInForeground();
boolean foreground = inForeground != null ? inForeground : false;
notifyObservers(new StateEvent.UpdateInForeground(foreground, getContextActivity()));
final boolean foreground = inForeground != null ? inForeground : false;
updateState(new StateEvent.UpdateInForeground(foreground, getContextActivity()));
}
@Nullable

View File

@ -69,8 +69,7 @@ final class SeverityReason implements JsonStream.Streamable {
case REASON_LOG:
return new SeverityReason(severityReasonType, severity, false, attrVal);
default:
String msg = String.format("Invalid argument '%s' for severityReason",
severityReasonType);
String msg = "Invalid argument for severityReason: '" + severityReasonType + '\'';
throw new IllegalArgumentException(msg);
}
}

View File

@ -20,43 +20,9 @@ internal class Stacktrace : JsonStream.Streamable {
* not.
*/
fun inProject(className: String, projectPackages: Collection<String>): Boolean? {
for (packageName in projectPackages) {
if (className.startsWith(packageName)) {
return true
}
}
return null
}
fun stacktraceFromJavaTrace(
stacktrace: Array<StackTraceElement>,
projectPackages: Collection<String>,
logger: Logger
): Stacktrace {
val frames = stacktrace.mapNotNull { serializeStackframe(it, projectPackages, logger) }
return Stacktrace(frames)
}
private fun serializeStackframe(
el: StackTraceElement,
projectPackages: Collection<String>,
logger: Logger
): Stackframe? {
try {
val methodName = when {
el.className.isNotEmpty() -> el.className + "." + el.methodName
else -> el.methodName
}
return Stackframe(
methodName,
if (el.fileName == null) "Unknown" else el.fileName,
el.lineNumber,
inProject(el.className, projectPackages)
)
} catch (lineEx: Exception) {
logger.w("Failed to serialize stacktrace", lineEx)
return null
return when {
projectPackages.any { className.startsWith(it) } -> true
else -> null
}
}
}
@ -67,13 +33,53 @@ internal class Stacktrace : JsonStream.Streamable {
trace = limitTraceLength(frames)
}
private fun <T> limitTraceLength(frames: List<T>): List<T> {
constructor(
stacktrace: Array<StackTraceElement>,
projectPackages: Collection<String>,
logger: Logger
) {
val frames = limitTraceLength(stacktrace)
trace = frames.mapNotNull { serializeStackframe(it, projectPackages, logger) }
}
private fun limitTraceLength(frames: Array<StackTraceElement>): Array<StackTraceElement> {
return when {
frames.size >= STACKTRACE_TRIM_LENGTH -> frames.sliceArray(0 until STACKTRACE_TRIM_LENGTH)
else -> frames
}
}
private fun limitTraceLength(frames: List<Stackframe>): List<Stackframe> {
return when {
frames.size >= STACKTRACE_TRIM_LENGTH -> frames.subList(0, STACKTRACE_TRIM_LENGTH)
else -> frames
}
}
private fun serializeStackframe(
el: StackTraceElement,
projectPackages: Collection<String>,
logger: Logger
): Stackframe? {
try {
val className = el.className
val methodName = when {
className.isNotEmpty() -> className + "." + el.methodName
else -> el.methodName
}
return Stackframe(
methodName,
el.fileName ?: "Unknown",
el.lineNumber,
inProject(className, projectPackages)
)
} catch (lineEx: Exception) {
logger.w("Failed to serialize stacktrace", lineEx)
return null
}
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginArray()

View File

@ -1,47 +1,66 @@
package com.bugsnag.android
sealed class StateEvent {
sealed class StateEvent { // JvmField allows direct field access optimizations
class Install(
val apiKey: String,
val autoDetectNdkCrashes: Boolean,
val appVersion: String?,
val buildUuid: String?,
val releaseStage: String?,
val lastRunInfoPath: String,
val consecutiveLaunchCrashes: Int
@JvmField val apiKey: String,
@JvmField val autoDetectNdkCrashes: Boolean,
@JvmField val appVersion: String?,
@JvmField val buildUuid: String?,
@JvmField val releaseStage: String?,
@JvmField val lastRunInfoPath: String,
@JvmField val consecutiveLaunchCrashes: Int
) : StateEvent()
object DeliverPending : StateEvent()
class AddMetadata(val section: String, val key: String?, val value: Any?) : StateEvent()
class ClearMetadataSection(val section: String) : StateEvent()
class ClearMetadataValue(val section: String, val key: String?) : StateEvent()
class AddMetadata(
@JvmField val section: String,
@JvmField val key: String?,
@JvmField val value: Any?
) : StateEvent()
class ClearMetadataSection(@JvmField val section: String) : StateEvent()
class ClearMetadataValue(
@JvmField val section: String,
@JvmField val key: String?
) : StateEvent()
class AddBreadcrumb(
val message: String,
val type: BreadcrumbType,
val timestamp: String,
val metadata: MutableMap<String, Any?>
@JvmField val message: String,
@JvmField val type: BreadcrumbType,
@JvmField val timestamp: String,
@JvmField val metadata: MutableMap<String, Any?>
) : StateEvent()
object NotifyHandled : StateEvent()
object NotifyUnhandled : StateEvent()
object PauseSession : StateEvent()
class StartSession(
val id: String,
val startedAt: String,
val handledCount: Int,
@JvmField val id: String,
@JvmField val startedAt: String,
@JvmField val handledCount: Int,
val unhandledCount: Int
) : StateEvent()
class UpdateContext(val context: String?) : StateEvent()
class UpdateInForeground(val inForeground: Boolean, val contextActivity: String?) : StateEvent()
class UpdateLastRunInfo(val consecutiveLaunchCrashes: Int) : StateEvent()
class UpdateIsLaunching(val isLaunching: Boolean) : StateEvent()
class UpdateOrientation(val orientation: String?) : StateEvent()
class UpdateContext(@JvmField val context: String?) : StateEvent()
class UpdateUser(val user: User) : StateEvent()
class UpdateInForeground(
@JvmField val inForeground: Boolean,
val contextActivity: String?
) : StateEvent()
class UpdateMemoryTrimEvent(val isLowMemory: Boolean) : StateEvent()
class UpdateLastRunInfo(@JvmField val consecutiveLaunchCrashes: Int) : StateEvent()
class UpdateIsLaunching(@JvmField val isLaunching: Boolean) : StateEvent()
class UpdateOrientation(@JvmField val orientation: String?) : StateEvent()
class UpdateUser(@JvmField val user: User) : StateEvent()
class UpdateMemoryTrimEvent(@JvmField val isLowMemory: Boolean) : StateEvent()
}

View File

@ -1,191 +0,0 @@
package com.bugsnag.android;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import androidx.annotation.NonNull;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
/**
* Used to automatically create breadcrumbs for system events
* Broadcast actions and categories can be found in text files in the android folder
* e.g. ~/Library/Android/sdk/platforms/android-9/data/broadcast_actions.txt
* See http://stackoverflow.com/a/27601497
*/
class SystemBroadcastReceiver extends BroadcastReceiver {
private static final String INTENT_ACTION_KEY = "Intent Action";
private final Client client;
private final Logger logger;
private final Map<String, BreadcrumbType> actions;
SystemBroadcastReceiver(@NonNull Client client, Logger logger) {
this.client = client;
this.logger = logger;
this.actions = buildActions();
}
static SystemBroadcastReceiver register(final Client client,
final Logger logger,
BackgroundTaskService bgTaskService) {
final SystemBroadcastReceiver receiver = new SystemBroadcastReceiver(client, logger);
if (receiver.getActions().size() > 0) {
try {
bgTaskService.submitTask(TaskType.DEFAULT, new Runnable() {
@Override
public void run() {
IntentFilter intentFilter = receiver.getIntentFilter();
Context context = client.appContext;
ContextExtensionsKt.registerReceiverSafe(context,
receiver, intentFilter, logger);
}
});
} catch (RejectedExecutionException ex) {
logger.w("Failed to register for automatic breadcrumb broadcasts", ex);
}
return receiver;
} else {
return null;
}
}
@Override
public void onReceive(@NonNull Context context, @NonNull Intent intent) {
try {
Map<String, Object> meta = new HashMap<>();
String fullAction = intent.getAction();
if (fullAction == null) {
return;
}
String shortAction = shortenActionNameIfNeeded(fullAction);
meta.put(INTENT_ACTION_KEY, fullAction); // always add the Intent Action
Bundle extras = intent.getExtras();
if (extras != null) {
for (String key : extras.keySet()) {
Object valObj = extras.get(key);
if (valObj == null) {
continue;
}
String val = valObj.toString();
if (isAndroidKey(key)) { // shorten the Intent action
meta.put("Extra", String.format("%s: %s", shortAction, val));
} else {
meta.put(key, val);
}
}
}
BreadcrumbType type = actions.get(fullAction);
if (type == null) {
type = BreadcrumbType.STATE;
}
client.leaveBreadcrumb(shortAction, meta, type);
} catch (Exception ex) {
logger.w("Failed to leave breadcrumb in SystemBroadcastReceiver: "
+ ex.getMessage());
}
}
private static boolean isAndroidKey(@NonNull String actionName) {
return actionName.startsWith("android.");
}
@NonNull
static String shortenActionNameIfNeeded(@NonNull String action) {
if (isAndroidKey(action)) {
return action.substring(action.lastIndexOf(".") + 1);
} else {
return action;
}
}
/**
* Builds a map of intent actions and their breadcrumb type (if enabled).
*
* Noisy breadcrumbs are omitted, along with anything that involves a state change.
* @return the action map
*/
@NonNull
private Map<String, BreadcrumbType> buildActions() {
Map<String, BreadcrumbType> actions = new HashMap<>();
if (client.getConfig().shouldRecordBreadcrumbType(BreadcrumbType.USER)) {
actions.put("android.appwidget.action.APPWIDGET_DELETED", BreadcrumbType.USER);
actions.put("android.appwidget.action.APPWIDGET_DISABLED", BreadcrumbType.USER);
actions.put("android.appwidget.action.APPWIDGET_ENABLED", BreadcrumbType.USER);
actions.put("android.intent.action.CAMERA_BUTTON", BreadcrumbType.USER);
actions.put("android.intent.action.CLOSE_SYSTEM_DIALOGS", BreadcrumbType.USER);
actions.put("android.intent.action.DOCK_EVENT", BreadcrumbType.USER);
}
if (client.getConfig().shouldRecordBreadcrumbType(BreadcrumbType.STATE)) {
actions.put("android.appwidget.action.APPWIDGET_HOST_RESTORED", BreadcrumbType.STATE);
actions.put("android.appwidget.action.APPWIDGET_RESTORED", BreadcrumbType.STATE);
actions.put("android.appwidget.action.APPWIDGET_UPDATE", BreadcrumbType.STATE);
actions.put("android.appwidget.action.APPWIDGET_UPDATE_OPTIONS", BreadcrumbType.STATE);
actions.put("android.intent.action.ACTION_POWER_CONNECTED", BreadcrumbType.STATE);
actions.put("android.intent.action.ACTION_POWER_DISCONNECTED", BreadcrumbType.STATE);
actions.put("android.intent.action.ACTION_SHUTDOWN", BreadcrumbType.STATE);
actions.put("android.intent.action.AIRPLANE_MODE", BreadcrumbType.STATE);
actions.put("android.intent.action.BATTERY_LOW", BreadcrumbType.STATE);
actions.put("android.intent.action.BATTERY_OKAY", BreadcrumbType.STATE);
actions.put("android.intent.action.BOOT_COMPLETED", BreadcrumbType.STATE);
actions.put("android.intent.action.CONFIGURATION_CHANGED", BreadcrumbType.STATE);
actions.put("android.intent.action.CONTENT_CHANGED", BreadcrumbType.STATE);
actions.put("android.intent.action.DATE_CHANGED", BreadcrumbType.STATE);
actions.put("android.intent.action.DEVICE_STORAGE_LOW", BreadcrumbType.STATE);
actions.put("android.intent.action.DEVICE_STORAGE_OK", BreadcrumbType.STATE);
actions.put("android.intent.action.INPUT_METHOD_CHANGED", BreadcrumbType.STATE);
actions.put("android.intent.action.LOCALE_CHANGED", BreadcrumbType.STATE);
actions.put("android.intent.action.REBOOT", BreadcrumbType.STATE);
actions.put("android.intent.action.SCREEN_OFF", BreadcrumbType.STATE);
actions.put("android.intent.action.SCREEN_ON", BreadcrumbType.STATE);
actions.put("android.intent.action.TIMEZONE_CHANGED", BreadcrumbType.STATE);
actions.put("android.intent.action.TIME_SET", BreadcrumbType.STATE);
actions.put("android.os.action.DEVICE_IDLE_MODE_CHANGED", BreadcrumbType.STATE);
actions.put("android.os.action.POWER_SAVE_MODE_CHANGED", BreadcrumbType.STATE);
}
if (client.getConfig().shouldRecordBreadcrumbType(BreadcrumbType.NAVIGATION)) {
actions.put("android.intent.action.DREAMING_STARTED", BreadcrumbType.NAVIGATION);
actions.put("android.intent.action.DREAMING_STOPPED", BreadcrumbType.NAVIGATION);
}
return actions;
}
/**
* @return the enabled actions
*/
public Map<String, BreadcrumbType> getActions() {
return actions;
}
/**
* Creates a new Intent filter with all the intents to record breadcrumbs for
*
* @return The intent filter
*/
@NonNull
public IntentFilter getIntentFilter() {
IntentFilter filter = new IntentFilter();
for (String action : actions.keySet()) {
filter.addAction(action);
}
return filter;
}
}

View File

@ -0,0 +1,130 @@
package com.bugsnag.android
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import java.util.HashMap
/**
* Used to automatically create breadcrumbs for system events
* Broadcast actions and categories can be found in text files in the android folder
* e.g. ~/Library/Android/sdk/platforms/android-9/data/broadcast_actions.txt
* See http://stackoverflow.com/a/27601497
*/
internal class SystemBroadcastReceiver(
private val client: Client,
private val logger: Logger
) : BroadcastReceiver() {
companion object {
private const val INTENT_ACTION_KEY = "Intent Action"
@JvmStatic
fun register(ctx: Context, receiver: SystemBroadcastReceiver, logger: Logger) {
if (receiver.actions.isNotEmpty()) {
val filter = IntentFilter()
receiver.actions.keys.forEach(filter::addAction)
ctx.registerReceiverSafe(receiver, filter, logger)
}
}
fun isAndroidKey(actionName: String): Boolean {
return actionName.startsWith("android.")
}
fun shortenActionNameIfNeeded(action: String): String {
return if (isAndroidKey(action)) {
action.substringAfterLast('.')
} else {
action
}
}
}
val actions: Map<String, BreadcrumbType> = buildActions()
override fun onReceive(context: Context, intent: Intent) {
try {
val meta: MutableMap<String, Any> = HashMap()
val fullAction = intent.action ?: return
val shortAction = shortenActionNameIfNeeded(fullAction)
meta[INTENT_ACTION_KEY] = fullAction // always add the Intent Action
addExtrasToMetadata(intent, meta, shortAction)
val type = actions[fullAction] ?: BreadcrumbType.STATE
client.leaveBreadcrumb(shortAction, meta, type)
} catch (ex: Exception) {
logger.w("Failed to leave breadcrumb in SystemBroadcastReceiver: ${ex.message}")
}
}
private fun addExtrasToMetadata(
intent: Intent,
meta: MutableMap<String, Any>,
shortAction: String
) {
val extras = intent.extras
extras?.keySet()?.forEach { key ->
val valObj = extras[key] ?: return@forEach
val strVal = valObj.toString()
if (isAndroidKey(key)) { // shorten the Intent action
meta["Extra"] = "$shortAction: $strVal"
} else {
meta[key] = strVal
}
}
}
/**
* Builds a map of intent actions and their breadcrumb type (if enabled).
*
* Noisy breadcrumbs are omitted, along with anything that involves a state change.
* @return the action map
*/
private fun buildActions(): Map<String, BreadcrumbType> {
val actions: MutableMap<String, BreadcrumbType> = HashMap()
val config = client.config
if (!config.shouldDiscardBreadcrumb(BreadcrumbType.USER)) {
actions["android.appwidget.action.APPWIDGET_DELETED"] = BreadcrumbType.USER
actions["android.appwidget.action.APPWIDGET_DISABLED"] = BreadcrumbType.USER
actions["android.appwidget.action.APPWIDGET_ENABLED"] = BreadcrumbType.USER
actions["android.intent.action.CAMERA_BUTTON"] = BreadcrumbType.USER
actions["android.intent.action.CLOSE_SYSTEM_DIALOGS"] = BreadcrumbType.USER
actions["android.intent.action.DOCK_EVENT"] = BreadcrumbType.USER
}
if (!config.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) {
actions["android.appwidget.action.APPWIDGET_HOST_RESTORED"] = BreadcrumbType.STATE
actions["android.appwidget.action.APPWIDGET_RESTORED"] = BreadcrumbType.STATE
actions["android.appwidget.action.APPWIDGET_UPDATE"] = BreadcrumbType.STATE
actions["android.appwidget.action.APPWIDGET_UPDATE_OPTIONS"] = BreadcrumbType.STATE
actions["android.intent.action.ACTION_POWER_CONNECTED"] = BreadcrumbType.STATE
actions["android.intent.action.ACTION_POWER_DISCONNECTED"] = BreadcrumbType.STATE
actions["android.intent.action.ACTION_SHUTDOWN"] = BreadcrumbType.STATE
actions["android.intent.action.AIRPLANE_MODE"] = BreadcrumbType.STATE
actions["android.intent.action.BATTERY_LOW"] = BreadcrumbType.STATE
actions["android.intent.action.BATTERY_OKAY"] = BreadcrumbType.STATE
actions["android.intent.action.BOOT_COMPLETED"] = BreadcrumbType.STATE
actions["android.intent.action.CONFIGURATION_CHANGED"] = BreadcrumbType.STATE
actions["android.intent.action.CONTENT_CHANGED"] = BreadcrumbType.STATE
actions["android.intent.action.DATE_CHANGED"] = BreadcrumbType.STATE
actions["android.intent.action.DEVICE_STORAGE_LOW"] = BreadcrumbType.STATE
actions["android.intent.action.DEVICE_STORAGE_OK"] = BreadcrumbType.STATE
actions["android.intent.action.INPUT_METHOD_CHANGED"] = BreadcrumbType.STATE
actions["android.intent.action.LOCALE_CHANGED"] = BreadcrumbType.STATE
actions["android.intent.action.REBOOT"] = BreadcrumbType.STATE
actions["android.intent.action.SCREEN_OFF"] = BreadcrumbType.STATE
actions["android.intent.action.SCREEN_ON"] = BreadcrumbType.STATE
actions["android.intent.action.TIMEZONE_CHANGED"] = BreadcrumbType.STATE
actions["android.intent.action.TIME_SET"] = BreadcrumbType.STATE
actions["android.os.action.DEVICE_IDLE_MODE_CHANGED"] = BreadcrumbType.STATE
actions["android.os.action.POWER_SAVE_MODE_CHANGED"] = BreadcrumbType.STATE
}
if (!config.shouldDiscardBreadcrumb(BreadcrumbType.NAVIGATION)) {
actions["android.intent.action.DREAMING_STARTED"] = BreadcrumbType.NAVIGATION
actions["android.intent.action.DREAMING_STOPPED"] = BreadcrumbType.NAVIGATION
}
return actions
}
}

View File

@ -1,5 +1,6 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.IOException
/**
@ -11,7 +12,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
sendThreads: ThreadSendPolicy,
projectPackages: Collection<String>,
logger: Logger,
currentThread: java.lang.Thread = java.lang.Thread.currentThread(),
currentThread: java.lang.Thread? = null,
stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>>? = null
) : JsonStream.Streamable {
@ -30,7 +31,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
threads = when {
recordThreads -> captureThreadTrace(
stackTraces ?: java.lang.Thread.getAllStackTraces(),
currentThread,
currentThread ?: java.lang.Thread.currentThread(),
exc,
isUnhandled,
projectPackages,
@ -64,7 +65,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
val trace = stackTraces[thread]
if (trace != null) {
val stacktrace = Stacktrace.stacktraceFromJavaTrace(trace, projectPackages, logger)
val stacktrace = Stacktrace(trace, projectPackages, logger)
val errorThread = thread.id == currentThreadId
Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, stacktrace, logger)
} else {

View File

@ -7,5 +7,5 @@ internal class UserState(user: User) : BaseObservable() {
emitObservableEvent()
}
fun emitObservableEvent() = notifyObservers(StateEvent.UpdateUser(user))
fun emitObservableEvent() = updateState { StateEvent.UpdateUser(user) }
}

View File

@ -1,5 +1,7 @@
package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.StateObserver
import java.io.File
import java.io.IOException
import java.util.concurrent.atomic.AtomicReference
@ -55,11 +57,13 @@ internal class UserStore @JvmOverloads constructor(
else -> UserState(User(deviceId, null, null))
}
userState.addObserver { _, arg ->
if (arg is StateEvent.UpdateUser) {
save(arg.user)
userState.addObserver(
StateObserver { event ->
if (event is StateEvent.UpdateUser) {
save(event.user)
}
}
}
)
return userState
}

View File

@ -1,11 +1,30 @@
package com.bugsnag.android
package com.bugsnag.android.internal
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.annotation.VisibleForTesting
import com.bugsnag.android.BreadcrumbType
import com.bugsnag.android.Configuration
import com.bugsnag.android.Connectivity
import com.bugsnag.android.DebugLogger
import com.bugsnag.android.DefaultDelivery
import com.bugsnag.android.Delivery
import com.bugsnag.android.DeliveryParams
import com.bugsnag.android.EndpointConfiguration
import com.bugsnag.android.ErrorTypes
import com.bugsnag.android.EventPayload
import com.bugsnag.android.Logger
import com.bugsnag.android.ManifestConfigLoader
import com.bugsnag.android.NoopLogger
import com.bugsnag.android.ThreadSendPolicy
import com.bugsnag.android.errorApiHeaders
import com.bugsnag.android.safeUnrollCauses
import com.bugsnag.android.sessionApiHeaders
import java.io.File
internal data class ImmutableConfig(
data class ImmutableConfig(
val apiKey: String,
val autoDetectErrors: Boolean,
val enabledErrorTypes: ErrorTypes,
@ -29,22 +48,13 @@ internal data class ImmutableConfig(
val maxPersistedEvents: Int,
val maxPersistedSessions: Int,
val persistenceDirectory: File,
val sendLaunchCrashesSynchronously: Boolean
val sendLaunchCrashesSynchronously: Boolean,
// results cached here to avoid unnecessary lookups in Client.
val packageInfo: PackageInfo?,
val appInfo: ApplicationInfo?
) {
/**
* Checks if the given release stage should be notified or not
*
* @return true if the release state should be notified else false
*/
@JvmName("shouldNotifyForReleaseStage")
internal fun shouldNotifyForReleaseStage() =
enabledReleaseStages == null || enabledReleaseStages.contains(releaseStage)
@JvmName("shouldRecordBreadcrumbType")
internal fun shouldRecordBreadcrumbType(type: BreadcrumbType) =
enabledBreadcrumbTypes == null || enabledBreadcrumbTypes.contains(type)
@JvmName("getErrorApiDeliveryParams")
internal fun getErrorApiDeliveryParams(payload: EventPayload) =
DeliveryParams(endpoints.notify, errorApiHeaders(payload))
@ -52,11 +62,73 @@ internal data class ImmutableConfig(
@JvmName("getSessionApiDeliveryParams")
internal fun getSessionApiDeliveryParams() =
DeliveryParams(endpoints.sessions, sessionApiHeaders(apiKey))
/**
* Returns whether the given throwable should be discarded
* based on the automatic data capture settings in [Configuration].
*/
fun shouldDiscardError(exc: Throwable): Boolean {
return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(exc)
}
/**
* Returns whether the given error should be discarded
* based on the automatic data capture settings in [Configuration].
*/
fun shouldDiscardError(errorClass: String?): Boolean {
return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(errorClass)
}
/**
* Returns whether a session should be discarded based on the
* automatic data capture settings in [Configuration].
*/
fun shouldDiscardSession(autoCaptured: Boolean): Boolean {
return shouldDiscardByReleaseStage() || (autoCaptured && !autoTrackSessions)
}
/**
* Returns whether breadcrumbs with the given type should be discarded or not.
*/
fun shouldDiscardBreadcrumb(type: BreadcrumbType): Boolean {
return enabledBreadcrumbTypes != null && !enabledBreadcrumbTypes.contains(type)
}
/**
* Returns whether errors/sessions should be discarded or not based on the enabled
* release stages.
*/
fun shouldDiscardByReleaseStage(): Boolean {
return enabledReleaseStages != null && !enabledReleaseStages.contains(releaseStage)
}
/**
* Returns whether errors with the given errorClass should be discarded or not.
*/
@VisibleForTesting
internal fun shouldDiscardByErrorClass(errorClass: String?): Boolean {
return discardClasses.contains(errorClass)
}
/**
* Returns whether errors should be discarded or not based on the errorClass, as deduced
* by the Throwable's class name.
*/
@VisibleForTesting
internal fun shouldDiscardByErrorClass(exc: Throwable): Boolean {
return exc.safeUnrollCauses().any { throwable ->
val errorClass = throwable.javaClass.name
shouldDiscardByErrorClass(errorClass)
}
}
}
@JvmOverloads
internal fun convertToImmutableConfig(
config: Configuration,
buildUuid: String? = null
buildUuid: String? = null,
packageInfo: PackageInfo? = null,
appInfo: ApplicationInfo? = null
): ImmutableConfig {
val errorTypes = when {
config.autoDetectErrors -> config.enabledErrorTypes.copy()
@ -87,7 +159,9 @@ internal fun convertToImmutableConfig(
maxPersistedSessions = config.maxPersistedSessions,
enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(),
persistenceDirectory = config.persistenceDirectory!!,
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously,
packageInfo = packageInfo,
appInfo = appInfo
)
}
@ -144,7 +218,7 @@ internal fun sanitiseConfiguration(
if (configuration.persistenceDirectory == null) {
configuration.persistenceDirectory = appContext.cacheDir
}
return convertToImmutableConfig(configuration, buildUuid)
return convertToImmutableConfig(configuration, buildUuid, packageInfo, appInfo)
}
internal const val RELEASE_STAGE_DEVELOPMENT = "development"

View File

@ -0,0 +1,14 @@
package com.bugsnag.android.internal;
import com.bugsnag.android.StateEvent;
import androidx.annotation.NonNull;
public interface StateObserver {
/**
* This is called whenever the notifier's state is altered, so that observers can react
* appropriately. This is intended for internal use only.
*/
void onStateChange(@NonNull StateEvent event);
}

22
patches/Bugsnag.patch Normal file
View File

@ -0,0 +1,22 @@
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